This dApp was created using @mysten/create-dapp that sets up a basic React
Client dApp using the following tools:
This guide will help you set up and run this Sui dApp template from scratch, even if you're completely new to development.
This project includes a .devcontainer configuration for GitHub Codespaces that automatically sets up the correct Node.js and pnpm versions.
To use GitHub Codespaces:
- Open in Codespace: Click the green "Code" button on GitHub → "Codespaces" → "Create codespace on main"
- Wait for Setup: The devcontainer will automatically:
- Install Node.js 18.12+
- Install pnpm latest version
- Install all project dependencies
- Set up VS Code extensions for optimal development
- Start Development: Once setup is complete, run
pnpm devto start the development server
Manual Setup (if not using Codespaces):
Before you begin, you'll need to install the following software on your computer:
Node.js is a JavaScript runtime that allows you to run JavaScript applications on your computer.
- Download: https://nodejs.org/
- Recommended Version: LTS (Long Term Support) - currently v20.x or v22.x
- Installation: Download the installer for your operating system and follow the setup wizard
Verify Installation:
node --versionpnpm is a fast, disk space efficient package manager that we use for this project.
Installation methods:
- Windows: Download from https://pnpm.io/installation
- macOS:
brew install pnpm(if you have Homebrew) - Linux:
curl -fsSL https://get.pnpm.io/install.sh | sh -
Verify Installation:
pnpm --versionGit is used to clone and manage the project code.
- Download: https://git-scm.com/downloads
- Installation: Follow the installation guide for your operating system
Verify Installation:
git --versionWhile not strictly required, a good code editor will make development much easier:
- Visual Studio Code: https://code.visualstudio.com/ (Recommended)
- WebStorm: https://www.jetbrains.com/webstorm/
- Sublime Text: https://www.sublimetext.com/
Option A: Clone with Git (Recommended)
git clone <repository-url>
cd bsa-2025-frontend-templateOption B: Download ZIP
- Download the project as a ZIP file
- Extract it to your desired location
- Open terminal/command prompt in the project folder
Navigate to the project directory and install all required packages:
pnpm installThis command will:
- Download all necessary packages listed in
package.json - Create a
node_modulesfolder with all dependencies - Generate a
pnpm-lock.yamlfile to lock dependency versions
If you plan to deploy your own smart contracts, you'll need:
- Sui CLI: Follow the Sui installation guide
- Wallet: Install a Sui wallet like Sui Wallet
Start the development server with hot reload:
pnpm devThis will:
- Start the Next.js development server
- Open your browser to
http://localhost:3000 - Automatically reload when you make changes to the code
To build the project for production:
pnpm buildAfter building, start the production server:
pnpm start- Make sure Node.js and pnpm are properly installed
- Restart your terminal after installation
- Check your PATH environment variable
If port 3000 is busy, the development server will automatically use the next available port (3001, 3002, etc.)
You might need to use sudo for global installations. Refer to the pnpm installation guide above for alternative installation methods.
If you get execution policy errors on Windows:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserOnce you have the project running:
- Explore the Code: Look at the files in the
app/directory - Connect a Wallet: Use the "Connect Wallet" button to connect your Sui wallet
- Try the Counter: Create and interact with counter objects
- Read the Documentation: Continue reading this README for advanced features
- React (v19.1.1) as the UI framework
- Next.js (v15.5.3) for the React framework with SSR support
- TypeScript (v5.9.2) for type checking
- Tailwind CSS (v4.1.13) for styling
- ShadCN UI for pre-built accessible UI components
- ESLint (v9.17.0) for linting
@mysten/dapp-kit(v0.18.0) for connecting to wallets and loading data@mysten/sui(v1.38.0) for Sui blockchain interactions- React Query (v5.87.1) for data fetching and caching
- pnpm for package management
- React: v19.1.1 - The main UI library
- Next.js: v15.5.3 - React framework with SSR, routing, and build optimization
- TypeScript: v5.9.2 - Type safety and better development experience
- @mysten/dapp-kit: v0.18.0 - Wallet connection and dApp utilities
- @mysten/sui: v1.38.0 - Sui SDK for blockchain interactions
- @tanstack/react-query: v5.87.1 - Data fetching and state management
- @shadcn:Accessible navigation components
- Tailwind CSS: v4.1.13 - Utility-first CSS framework
- tailwindcss-animate: v1.0.7 - Animation utilities
- lucide-react: v0.544.0 - Icon library
- react-spinners: v0.14.1 - Loading spinners
- class-variance-authority: v0.7.1 - Component variant management
- clsx: v2.1.1 - Conditional className utility
- tailwind-merge: v3.3.1 - Tailwind class merging utility
For a full guide on how to build this dApp from scratch, visit this guide.
Before diving into deployment, let's understand what's happening behind the scenes:
Think of a smart contract as a program that lives on the blockchain. Unlike traditional applications that run on servers, smart contracts:
- Live on the Blockchain: Once deployed, the code is stored permanently on the Sui blockchain
- Are Immutable: The code cannot be changed after deployment (ensuring trust and security)
- Execute Automatically: They run exactly as programmed, without human intervention
- Are Transparent: Anyone can verify what the code does
Here's how your React frontend communicates with smart contracts:
┌─────────────────┐ ┌──────────────┐ ┌─────────────────────┐
│ Your React │ │ Sui │ │ Smart Contract │
│ Frontend │◄──►│ Network │◄──►│ (on Blockchain) │
│ (This Project) │ │ │ │ │
└─────────────────┘ └──────────────┘ └─────────────────────┘
Step-by-Step Process:
- User clicks a button in your React app (e.g., "Create Counter")
- Frontend creates a transaction using the Sui SDK
- Transaction is sent to the Sui network via your wallet
- Smart contract executes the requested function on the blockchain
- Result is returned to your frontend and displayed to the user
- Testnet = Practice blockchain (free, safe for testing)
- Mainnet = Real blockchain (costs real money, permanent)
Always test on testnet before going to mainnet!
The Sui CLI is a command-line tool that lets you interact with the Sui blockchain. Think of it as your "control panel" for deploying and managing smart contracts.
Download and Install:
- Official Guide: https://docs.sui.io/build/install
- Quick Install (Linux/macOS):
curl -fLJO https://github.com/MystenLabs/sui/releases/latest/download/sui-mainnet-v1.38.0-ubuntu-x86_64.tgz tar -xf sui-mainnet-v1.38.0-ubuntu-x86_64.tgz sudo mv sui /usr/local/bin
Verify Installation:
sui --versionThe testnet is a "practice" version of the Sui blockchain where you can test your smart contracts without spending real money.
Configure Testnet:
# Add testnet environment
sui client new-env --alias testnet --rpc https://fullnode.testnet.sui.io:443
# Switch to testnet
sui client switch --env testnetCreate a New Wallet Address:
# Generate a new address
sui client new-address secp256k1This will output something like:
Created new keypair and saved it to keystore.
- Address: 0x1234567890abcdef...
- Alias: <none>
Set Your Active Address:
# Replace with your actual address from above
sui client switch --address 0x1234567890abcdef...To deploy smart contracts, you need SUI tokens to pay for "gas" (transaction fees). On testnet, these are free!
Get Free Testnet SUI:
- Visit: https://faucet.sui.io
- Enter your wallet address (from Step 2)
- Click "Request SUI"
- Wait a few seconds for the tokens to arrive
Check Your Balance:
sui client balanceNow comes the exciting part - putting your smart contract on the blockchain!
Navigate to the Move Code:
cd moveDeploy the Counter Smart Contract:
sui client publish --gas-budget 100000000 counterWhat This Command Does:
publish: Tells Sui to deploy your smart contract--gas-budget 100000000: Sets the maximum gas you're willing to paycounter: The name of your Move package (folder)
Understanding the Output:
After deployment, you'll see a lot of output. Look for something like this:
{
"packageId": "0xcea82fb908b9d9566b1c7977491e76901ed167978a1ecd6053a994881c0ea9b5",
"version": "1",
"digest": "...",
"modules": ["counter"],
...
}🎯 IMPORTANT: Copy the packageId value - you'll need it in the next step!
This is where the magic happens - connecting your React app to your deployed smart contract.
Open in your code editor. You'll see:
export const DEVNET_COUNTER_PACKAGE_ID = "0xTODO";
export const TESTNET_COUNTER_PACKAGE_ID = "0xcea82fb908b9d9566b1c7977491e76901ed167978a1ecd6053a994881c0ea9b5";
export const MAINNET_COUNTER_PACKAGE_ID = "0xTODO";What These Mean:
- DEVNET: Local development network (for advanced users)
- TESTNET: Practice network (what you just deployed to)
- MAINNET: Real network (costs real money)
Replace the TESTNET_COUNTER_PACKAGE_ID with your actual package ID from Step 4:
export const TESTNET_COUNTER_PACKAGE_ID = "0xYOUR_ACTUAL_PACKAGE_ID_HERE";Example:
export const TESTNET_COUNTER_PACKAGE_ID = "0xcea82fb908b9d9566b1c7977491e76901ed167978a1ecd6053a994881c0ea9b5";Your React components use this package ID to know which smart contract to interact with:
// In your React components
const counterPackageId = useNetworkVariable("counterPackageId");
// When calling smart contract functions
tx.moveCall({
target: `${counterPackageId}::counter::create`, // This becomes: 0xYOUR_ID::counter::create
arguments: [],
});The Connection Process:
- User clicks "Create Counter" in your React app
- Frontend reads the package ID from constants.ts
- Creates a transaction targeting your specific smart contract
- Sends transaction to the Sui testnet
- Your deployed smart contract executes the function
- Result is returned and displayed in your app
Now let's make sure everything works!
Start Your Development Server:
# Make sure you're in the project root directory
cd .. # if you're still in the move/ folder
pnpm devTest the Connection:
- Open your browser to
http://localhost:3000 - Connect your wallet (install Sui Wallet browser extension if needed)
- Switch your wallet to testnet (in wallet settings)
- Try creating a counter - click the "Create Counter" button
- Interact with the counter - increment, reset, etc.
What's Happening Behind the Scenes:
Your React App → Sui Wallet → Testnet → Your Smart Contract → Back to Your App
- Double-check your package ID in constants.ts
- Make sure you're connected to testnet (not mainnet or devnet)
- Get more testnet SUI from the faucet
- Check your wallet balance:
sui client balance
- Make sure you've created a counter object first
- Check that you're using the correct object ID
- Install the Sui Wallet browser extension
- Make sure your wallet is set to testnet
- Refresh the page and try reconnecting
Here's what happens when you click "Create Counter":
- Frontend (React) creates a transaction
- Wallet signs the transaction with your private key
- Transaction is sent to Sui testnet
- Validators on the network verify and execute the transaction
- Smart contract runs the
createfunction - New counter object is created on the blockchain
- Object ID is returned to your frontend
- UI updates to show the new counter
This is the power of blockchain - your data is now stored permanently and securely on a decentralized network!
The file is the bridge between your frontend and your deployed smart contracts. Let's break down exactly how it works and why it's crucial.
When you deploy a smart contract to the Sui blockchain, it gets assigned a unique Package ID. Think of this as the "address" where your smart contract lives on the blockchain. Just like how your house has a unique address, your smart contract has a unique Package ID.
Example Package ID:
0xcea82fb908b9d9566b1c7977491e76901ed167978a1ecd6053a994881c0ea9b5
This long hexadecimal string uniquely identifies your deployed smart contract among millions of others on the network.
Ready to move beyond the counter example? Here's how to create your own custom smart contract and integrate it with your frontend.
First, let's look at the current counter smart contract to understand the structure:
File: move/counter/sources/counter.move
module counter::counter {
use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
/// A counter object
public struct Counter has key {
id: UID,
value: u64,
}
/// Create a new counter
public fun create(ctx: &mut TxContext) {
let counter = Counter {
id: object::new(ctx),
value: 0,
};
transfer::share_object(counter);
}
/// Increment the counter
public fun increment(counter: &mut Counter) {
counter.value = counter.value + 1;
}
}Key Components:
- Module Declaration:
module counter::counterdefines the module name - Struct Definition:
Counteris the main data structure - Functions:
create()andincrement()are the public functions you can call
Let's create a simple Task Manager smart contract as an example:
File: move/task_manager/sources/task_manager.move
module task_manager::task_manager {
use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
use std::string::{Self, String};
use std::vector;
/// A task list object
public struct TaskList has key {
id: UID,
tasks: vector<Task>,
owner: address,
}
/// Individual task structure
public struct Task has store, copy, drop {
id: u64,
title: String,
completed: bool,
}
/// Create a new task list
public fun create_task_list(ctx: &mut TxContext) {
let task_list = TaskList {
id: object::new(ctx),
tasks: vector::empty<Task>(),
owner: tx_context::sender(ctx),
};
transfer::transfer(task_list, tx_context::sender(ctx));
}
/// Add a new task
public fun add_task(
task_list: &mut TaskList,
title: String,
ctx: &mut TxContext
) {
assert!(task_list.owner == tx_context::sender(ctx), 0);
let task = Task {
id: vector::length(&task_list.tasks),
title,
completed: false,
};
vector::push_back(&mut task_list.tasks, task);
}
/// Mark task as completed
public fun complete_task(
task_list: &mut TaskList,
task_id: u64,
ctx: &mut TxContext
) {
assert!(task_list.owner == tx_context::sender(ctx), 0);
let task = vector::borrow_mut(&mut task_list.tasks, task_id);
task.completed = true;
}
/// Get task count
public fun get_task_count(task_list: &TaskList): u64 {
vector::length(&task_list.tasks)
}
}What's Different:
- Multiple Functions:
create_task_list(),add_task(),complete_task() - Complex Data: Uses vectors and custom structs
- Access Control: Only the owner can modify tasks
- String Handling: Tasks have titles
File: move/task_manager/Move.toml
[package]
name = "task_manager"
version = "1.0.0"
edition = "2024.beta"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }
[addresses]
task_manager = "0x0"Key Changes:
- Package Name: Changed from "counter" to "task_manager"
- Module Address: Updated to match your module name
Navigate to Your Move Directory:
cd move/task_managerDeploy to Testnet:
sui client publish --gas-budget 100000000 .Save Your Package ID:
After deployment, copy the packageId from the output and update your constants:
// In constants.ts
export const TESTNET_TASK_MANAGER_PACKAGE_ID = "0xYOUR_NEW_PACKAGE_ID";File: app/constants.ts
// Keep the counter package ID
export const TESTNET_COUNTER_PACKAGE_ID = "0xcea82fb908b9d9566b1c7977491e76901ed167978a1ecd6053a994881c0ea9b5";
// Add your new package ID
export const TESTNET_TASK_MANAGER_PACKAGE_ID = "0xYOUR_NEW_PACKAGE_ID";File: app/networkConfig.ts
import { getFullnodeUrl } from "@mysten/sui/client";
import { createNetworkConfig } from "@mysten/dapp-kit";
import {
TESTNET_COUNTER_PACKAGE_ID,
TESTNET_TASK_MANAGER_PACKAGE_ID
} from "./constants";
const { networkConfig, useNetworkVariable, useNetworkVariables } =
createNetworkConfig({
testnet: {
url: getFullnodeUrl("testnet"),
variables: {
counterPackageId: TESTNET_COUNTER_PACKAGE_ID,
taskManagerPackageId: TESTNET_TASK_MANAGER_PACKAGE_ID, // Add this line
},
},
mainnet: {
url: getFullnodeUrl("mainnet"),
variables: {
counterPackageId: "0xTODO",
taskManagerPackageId: "0xTODO", // Add this line
},
},
});
export { networkConfig, useNetworkVariable, useNetworkVariables };File: app/TaskManager.tsx
import { useState } from "react";
import { Transaction } from "@mysten/sui/transactions";
import { useSignAndExecuteTransaction, useSuiClient, useSuiClientQuery } from "@mysten/dapp-kit";
import { useNetworkVariable } from "./networkConfig";
import { Button } from "./components/ui/button";
import { Input } from "./components/ui/input";
export function TaskManager({ taskListId }: { taskListId?: string }) {
const taskManagerPackageId = useNetworkVariable("taskManagerPackageId");
const suiClient = useSuiClient();
const { mutate: signAndExecute } = useSignAndExecuteTransaction();
const [newTaskTitle, setNewTaskTitle] = useState("");
const [isLoading, setIsLoading] = useState(false);
// Fetch task list data
const { data: taskListData, refetch } = useSuiClientQuery(
"getObject",
{
id: taskListId!,
options: { showContent: true },
},
{ enabled: !!taskListId }
);
// Create new task list
const createTaskList = () => {
setIsLoading(true);
const tx = new Transaction();
tx.moveCall({
arguments: [],
target: `${taskManagerPackageId}::task_manager::create_task_list`,
});
signAndExecute(
{ transaction: tx },
{
onSuccess: async ({ digest }) => {
const { effects } = await suiClient.waitForTransaction({
digest,
options: { showEffects: true },
});
const createdObjectId = effects?.created?.[0]?.reference?.objectId;
console.log("Task list created:", createdObjectId);
setIsLoading(false);
},
onError: () => setIsLoading(false),
}
);
};
// Add new task
const addTask = () => {
if (!newTaskTitle.trim() || !taskListId) return;
setIsLoading(true);
const tx = new Transaction();
tx.moveCall({
arguments: [
tx.object(taskListId),
tx.pure.string(newTaskTitle),
],
target: `${taskManagerPackageId}::task_manager::add_task`,
});
signAndExecute(
{ transaction: tx },
{
onSuccess: async ({ digest }) => {
await suiClient.waitForTransaction({ digest });
await refetch();
setNewTaskTitle("");
setIsLoading(false);
},
onError: () => setIsLoading(false),
}
);
};
// Complete task
const completeTask = (taskId: number) => {
if (!taskListId) return;
setIsLoading(true);
const tx = new Transaction();
tx.moveCall({
arguments: [
tx.object(taskListId),
tx.pure.u64(taskId),
],
target: `${taskManagerPackageId}::task_manager::complete_task`,
});
signAndExecute(
{ transaction: tx },
{
onSuccess: async ({ digest }) => {
await suiClient.waitForTransaction({ digest });
await refetch();
setIsLoading(false);
},
onError: () => setIsLoading(false),
}
);
};
// Parse task list data
const getTaskListFields = (data: any) => {
if (data?.content?.dataType !== "moveObject") return null;
return data.content.fields as {
tasks: Array<{ id: string; title: string; completed: boolean }>;
owner: string;
};
};
const taskListFields = taskListData ? getTaskListFields(taskListData) : null;
if (!taskListId) {
return (
<div className="p-4">
<h2 className="text-xl font-bold mb-4">Task Manager</h2>
<Button onClick={createTaskList} disabled={isLoading}>
{isLoading ? "Creating..." : "Create Task List"}
</Button>
</div>
);
}
return (
<div className="p-4">
<h2 className="text-xl font-bold mb-4">My Tasks</h2>
{/* Add new task */}
<div className="flex gap-2 mb-4">
<Input
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
placeholder="Enter task title..."
onKeyPress={(e) => e.key === "Enter" && addTask()}
/>
<Button onClick={addTask} disabled={isLoading || !newTaskTitle.trim()}>
Add Task
</Button>
</div>
{/* Task list */}
<div className="space-y-2">
{taskListFields?.tasks.map((task, index) => (
<div
key={index}
className={`flex items-center justify-between p-3 border rounded ${
task.completed ? "bg-green-50 text-green-800" : "bg-white"
}`}
>
<span className={task.completed ? "line-through" : ""}>
{task.title}
</span>
{!task.completed && (
<Button
size="sm"
onClick={() => completeTask(parseInt(task.id))}
disabled={isLoading}
>
Complete
</Button>
)}
</div>
))}
</div>
{taskListFields?.tasks.length === 0 && (
<p className="text-gray-500 text-center py-8">
No tasks yet. Add your first task above!
</p>
)}
</div>
);
}- Define your data structures (what objects will you store?)
- List the functions you need (create, update, delete, query)
- Consider access control (who can call which functions?)
module your_module::your_contract {
// Import necessary modules
use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
// Define your data structures
public struct YourObject has key {
id: UID,
// your fields here
}
// Create functions
public fun create_object(ctx: &mut TxContext) {
// implementation
}
// Other functions
public fun update_object(obj: &mut YourObject, /* params */) {
// implementation
}
}# Deploy
sui client publish --gas-budget 100000000 your_module
# Update constants.ts
export const TESTNET_YOUR_PACKAGE_ID = "0xYOUR_PACKAGE_ID";
# Update networkConfig.ts
variables: {
yourPackageId: TESTNET_YOUR_PACKAGE_ID,
}// Get package ID
const yourPackageId = useNetworkVariable("yourPackageId");
// Create transactions
const tx = new Transaction();
tx.moveCall({
arguments: [/* your arguments */],
target: `${yourPackageId}::your_contract::your_function`,
});
// Execute transactions
signAndExecute({ transaction: tx }, {
onSuccess: async ({ digest }) => {
// Handle success
}
});Here are some popular smart contract patterns you can implement:
module nft_collection::nft {
public struct NFT has key, store {
id: UID,
name: String,
description: String,
image_url: String,
creator: address,
}
public fun mint_nft(
name: String,
description: String,
image_url: String,
ctx: &mut TxContext
) {
let nft = NFT {
id: object::new(ctx),
name,
description,
image_url,
creator: tx_context::sender(ctx),
};
transfer::public_transfer(nft, tx_context::sender(ctx));
}
}module voting::poll {
public struct Poll has key {
id: UID,
question: String,
options: vector<String>,
votes: vector<u64>,
voters: vector<address>,
creator: address,
end_time: u64,
}
public fun create_poll(
question: String,
options: vector<String>,
duration_ms: u64,
ctx: &mut TxContext
) {
let poll = Poll {
id: object::new(ctx),
question,
options,
votes: vector::empty<u64>(),
voters: vector::empty<address>(),
creator: tx_context::sender(ctx),
end_time: tx_context::epoch_timestamp_ms(ctx) + duration_ms,
};
transfer::share_object(poll);
}
public fun vote(poll: &mut Poll, option_index: u64, ctx: &mut TxContext) {
let voter = tx_context::sender(ctx);
assert!(!vector::contains(&poll.voters, &voter), 0); // No double voting
assert!(tx_context::epoch_timestamp_ms(ctx) < poll.end_time, 1); // Poll not ended
vector::push_back(&mut poll.voters, voter);
let current_votes = vector::borrow_mut(&mut poll.votes, option_index);
*current_votes = *current_votes + 1;
}
}module marketplace::shop {
public struct Item has key, store {
id: UID,
name: String,
price: u64,
seller: address,
for_sale: bool,
}
public fun list_item(
name: String,
price: u64,
ctx: &mut TxContext
) {
let item = Item {
id: object::new(ctx),
name,
price,
seller: tx_context::sender(ctx),
for_sale: true,
};
transfer::share_object(item);
}
public fun buy_item(
item: &mut Item,
payment: Coin<SUI>,
ctx: &mut TxContext
) {
assert!(item.for_sale, 0);
assert!(coin::value(&payment) >= item.price, 1);
transfer::public_transfer(payment, item.seller);
item.for_sale = false;
// Transfer ownership logic here
}
}# 1. Test your Move code locally
sui move test
# 2. Build without publishing to check for errors
sui move build
# 3. Deploy to devnet first for testing
sui client switch --env devnet
sui client publish --gas-budget 100000000 .
# 4. Test thoroughly on devnet
# 5. Deploy to testnet for public testing
sui client switch --env testnet
sui client publish --gas-budget 100000000 .
# 6. Finally deploy to mainnet when ready
sui client switch --env mainnet
sui client publish --gas-budget 100000000 .- Simple contracts: 10,000,000 (10M)
- Medium complexity: 50,000,000 (50M)
- Complex contracts: 100,000,000 (100M)
- Very large contracts: 200,000,000 (200M)
# In Move.toml, always increment version
[package]
name = "your_contract"
version = "1.1.0" # Increment this for updates
edition = "2024.beta"Error: "Insufficient gas"
# Solution: Increase gas budget
sui client publish --gas-budget 200000000 .Error: "Module already exists"
# Solution: You can't redeploy the same module. Create a new version or use upgrade
# For new version, change the module name:
module your_contract_v2::contract {
// your code
}Error: "Invalid address"
# Solution: Make sure addresses in Move.toml are correct
[addresses]
your_contract = "0x0" # This should always be 0x0 for new deploymentsError: "Compilation failed"
# Solution: Check your Move syntax
sui move build # This will show detailed error messages- Use
sui move testto run unit tests before deployment - Check dependencies in Move.toml match your Sui version
- Verify imports - make sure all
usestatements are correct - Test functions individually before deploying the full contract
- Use
assert!statements for input validation in your Move code
// For functions that don't return data
const callFunction = () => {
const tx = new Transaction();
tx.moveCall({
arguments: [tx.pure.string("hello")],
target: `${packageId}::module::function_name`,
});
signAndExecute({ transaction: tx });
};// For reading object data
const { data } = useSuiClientQuery(
"getObject",
{
id: objectId,
options: { showContent: true },
},
{ enabled: !!objectId }
);
// Parse the data
const objectFields = data?.content?.dataType === "moveObject"
? data.content.fields
: null;// For listening to contract events
const { data: events } = useSuiClientQuery(
"queryEvents",
{
query: {
MoveModule: {
package: packageId,
module: "your_module",
},
},
}
);// For complex operations requiring multiple calls
const complexOperation = () => {
const tx = new Transaction();
// Step 1: Create object
const [obj] = tx.moveCall({
arguments: [],
target: `${packageId}::module::create_object`,
});
// Step 2: Use the created object
tx.moveCall({
arguments: [obj, tx.pure.string("data")],
target: `${packageId}::module::update_object`,
});
signAndExecute({ transaction: tx });
};Your constants.ts file defines Package IDs for three different Sui networks:
export const DEVNET_COUNTER_PACKAGE_ID = "0xTODO";
export const TESTNET_COUNTER_PACKAGE_ID = "0xcea82fb908b9d9566b1c7977491e76901ed167978a1ecd6053a994881c0ea9b5";
export const MAINNET_COUNTER_PACKAGE_ID = "0xTODO";Why Three Different Networks?
-
DEVNET (Development Network)
- Purpose: Local development and testing
- Cost: Free
- Speed: Very fast
- Use Case: When you're building and testing locally
- Data Persistence: May be reset frequently
-
TESTNET (Test Network)
- Purpose: Public testing environment
- Cost: Free (test tokens from faucet)
- Speed: Similar to mainnet
- Use Case: Testing with real network conditions
- Data Persistence: More stable than devnet
-
MAINNET (Main Network)
- Purpose: Production environment
- Cost: Real SUI tokens (costs real money)
- Speed: Standard network speed
- Use Case: Live applications with real users
- Data Persistence: Permanent
Your React components don't directly use these constants. Instead, they use a smart helper function that automatically selects the right Package ID based on your current network:
// In your React components
const counterPackageId = useNetworkVariable("counterPackageId");The Magic Behind useNetworkVariable:
This function looks at:
- Which network your wallet is connected to (devnet/testnet/mainnet)
- Automatically selects the corresponding Package ID from constants.ts
- Returns the correct ID for your current network
Example Flow:
Wallet connected to testnet → useNetworkVariable returns → TESTNET_COUNTER_PACKAGE_ID
Wallet connected to mainnet → useNetworkVariable returns → MAINNET_COUNTER_PACKAGE_ID
- Deploy to Testnet (as shown in the deployment guide above)
- Copy your Package ID from the deployment output
- Update constants.ts:
export const TESTNET_COUNTER_PACKAGE_ID = "0xYOUR_ACTUAL_PACKAGE_ID";
- Deploy to Mainnet (costs real SUI tokens)
- Update constants.ts:
export const MAINNET_COUNTER_PACKAGE_ID = "0xYOUR_MAINNET_PACKAGE_ID";
// Your wallet is on testnet, but you're using mainnet Package ID
export const TESTNET_COUNTER_PACKAGE_ID = "0xMAINNET_PACKAGE_ID"; // Wrong!Result: "Package not found" errors
// Missing a character or wrong character
export const TESTNET_COUNTER_PACKAGE_ID = "0xcea82fb908b9d9566b1c7977491e76901ed167978a1ecd6053a994881c0ea9b"; // Missing last character!Result: "Package not found" errors
// Forgetting to replace the placeholder
export const TESTNET_COUNTER_PACKAGE_ID = "0xTODO"; // Still a placeholder!Result: "Package not found" errors
// Properly deployed and configured
export const TESTNET_COUNTER_PACKAGE_ID = "0xcea82fb908b9d9566b1c7977491e76901ed167978a1ecd6053a994881c0ea9b5";When you interact with your smart contract, here's what happens:
- User Action: User clicks "Create Counter" in your app
- Package ID Lookup:
useNetworkVariablegets the correct Package ID - Transaction Creation: Your code creates a transaction like:
tx.moveCall({ target: `${counterPackageId}::counter::create`, arguments: [], });
- Target Resolution: This becomes something like:
0xcea82fb908b9d9566b1c7977491e76901ed167978a1ecd6053a994881c0ea9b5::counter::create - Blockchain Execution: The Sui network finds your smart contract and executes the
createfunction
If you're getting errors, check these in order:
-
Verify your Package ID is correct:
# Check if your package exists on testnet sui client object 0xYOUR_PACKAGE_ID --json -
Confirm your wallet network:
- Open Sui Wallet extension
- Check the network dropdown (should say "Testnet")
-
Check your constants.ts file:
- Make sure there are no typos
- Ensure you're not using "0xTODO"
- Verify the Package ID matches your deployment output
-
Clear browser cache:
- Sometimes old Package IDs get cached
- Hard refresh (Ctrl+Shift+R) or clear browser cache
As your dApp grows, you might deploy multiple smart contracts. You can organize them like this:
// Multiple contracts for different features
export const TESTNET_COUNTER_PACKAGE_ID = "0xabc123...";
export const TESTNET_MARKETPLACE_PACKAGE_ID = "0xdef456...";
export const TESTNET_NFT_PACKAGE_ID = "0xghi789...";
// Or organize by environment
export const TESTNET_PACKAGES = {
counter: "0xabc123...",
marketplace: "0xdef456...",
nft: "0xghi789...",
};This configuration system ensures your frontend always connects to the right smart contracts on the right network, making your dApp robust and reliable across different environments.
To install dependencies you can run
pnpm installTo start your dApp in development mode run
pnpm devTo build your app for deployment you can run
pnpm buildThis template demonstrates how to integrate Move smart contracts with your React frontend. The examples below show how to create and interact with a counter smart contract.
Before integrating Move smart contracts, ensure you have:
- Sui CLI installed - Follow the Sui installation guide
- Published Move package - Your smart contract deployed on the Sui network
- Package ID - The unique identifier of your deployed Move package
Set up your network configuration to handle different environments:
export const TESTNET_COUNTER_PACKAGE_ID = "YOUR_PACKAGE_ID_HERE";For Move smart contract integration, you'll typically need these imports:
import { Transaction } from "@mysten/sui/transactions";
import { useSignAndExecuteTransaction, useSuiClient } from "@mysten/dapp-kit";
import { useNetworkVariable } from "./networkConfig";File: app/CreateCounter.tsx
This pattern shows how to call a Move function that creates a new object:
export function CreateCounter({ onCreated }: { onCreated: (id: string) => void }) {
const counterPackageId = useNetworkVariable("counterPackageId");
const suiClient = useSuiClient();
const { mutate: signAndExecute, isSuccess, isPending } = useSignAndExecuteTransaction();
function create() {
// 1. Create a new transaction
const tx = new Transaction();
// 2. Add a moveCall to the transaction
tx.moveCall({
arguments: [], // No arguments needed for counter::create
target: `${counterPackageId}::counter::create`, // module::function format
});
// 3. Sign and execute the transaction
signAndExecute(
{ transaction: tx },
{
onSuccess: async ({ digest }) => {
// 4. Wait for transaction completion and get effects
const { effects } = await suiClient.waitForTransaction({
digest: digest,
options: { showEffects: true },
});
// 5. Extract the created object ID
const createdObjectId = effects?.created?.[0]?.reference?.objectId;
if (createdObjectId) {
onCreated(createdObjectId);
}
},
},
);
}
return (
<Button
onClick={create}
disabled={isSuccess || isPending}
>
{isPending ? "Creating..." : "Create Counter"}
</Button>
);
}Key Points:
- Use
Transaction()to build your transaction tx.moveCall()specifies the Move function to calltargetformat:${packageId}::${module}::${function}- Handle success callback to get created object IDs
- Use loading states (
isPending,isSuccess) for UX
File: app/Counter.tsx
This pattern shows how to call Move functions on existing objects:
export function Counter({ id }: { id: string }) {
const counterPackageId = useNetworkVariable("counterPackageId");
const suiClient = useSuiClient();
const { mutate: signAndExecute } = useSignAndExecuteTransaction();
// Query object data
const { data, refetch } = useSuiClientQuery("getObject", {
id,
options: { showContent: true, showOwner: true },
});
const [waitingForTxn, setWaitingForTxn] = useState("");
const executeMoveCall = (method: "increment" | "reset") => {
setWaitingForTxn(method);
const tx = new Transaction();
if (method === "reset") {
// Move call with multiple arguments
tx.moveCall({
arguments: [
tx.object(id), // Object reference
tx.pure.u64(0) // Pure value (u64 type)
],
target: `${counterPackageId}::counter::set_value`,
});
} else {
// Move call with single object argument
tx.moveCall({
arguments: [tx.object(id)],
target: `${counterPackageId}::counter::increment`,
});
}
signAndExecute(
{ transaction: tx },
{
onSuccess: (tx) => {
// Wait for transaction and refresh data
suiClient.waitForTransaction({ digest: tx.digest }).then(async () => {
await refetch(); // Refresh object data
setWaitingForTxn("");
});
},
},
);
};
return (
<div>
<p>Count: {getCounterFields(data.data)?.value}</p>
<Button onClick={() => executeMoveCall("increment")}>
{waitingForTxn === "increment" ? "Processing..." : "Increment"}
</Button>
<Button onClick={() => executeMoveCall("reset")}>
{waitingForTxn === "reset" ? "Processing..." : "Reset"}
</Button>
</div>
);
}Key Points:
- Use
useSuiClientQueryto fetch object data tx.object(id)for object referencestx.pure.u64(value)for pure values with specific types- Always
refetch()after successful transactions to update UI - Track transaction states for better UX
tx.moveCall({
arguments: [tx.object(objectId)],
target: `${packageId}::module::function`,
});tx.moveCall({
arguments: [
tx.pure.u64(123), // 64-bit unsigned integer
tx.pure.string("hello"), // String
tx.pure.bool(true), // Boolean
tx.pure.address(address), // Sui address
],
target: `${packageId}::module::function`,
});tx.moveCall({
arguments: [
tx.object(objectId), // Object reference
tx.pure.u64(amount), // Pure value
tx.pure.address(recipient), // Another pure value
],
target: `${packageId}::module::transfer`,
});signAndExecute(
{ transaction: tx },
{
onSuccess: (result) => {
console.log("Transaction successful:", result.digest);
// Handle success
},
onError: (error) => {
console.error("Transaction failed:", error);
// Handle error - show user feedback
setWaitingForTxn("");
},
},
);const { data, isPending, error, refetch } = useSuiClientQuery("getObject", {
id: objectId,
options: {
showContent: true, // Include object content
showOwner: true, // Include owner information
showType: true, // Include type information
},
});function getCounterFields(data: SuiObjectData) {
if (data.content?.dataType !== "moveObject") {
return null;
}
return data.content.fields as { value: number; owner: string };
}-
Start the development server:
pnpm dev
-
Connect your wallet using the Connect Wallet button
-
Test contract interactions:
- Create new objects
- Call functions on existing objects
- Verify state changes in the UI
- "Package not found": Verify your package ID is correct
- "Function not found": Check the module and function names
- "Insufficient gas": Ensure your wallet has enough SUI for gas fees
- "Object not found": Verify object IDs and ownership
This integration pattern can be extended to work with any Move smart contract by adjusting the function calls, arguments, and data parsing logic.