Sui dApp Development Guide
This comprehensive guide covers building modern decentralized applications (dApps) on the Sui blockchain using the latest tools and best practices for 2025.
Prerequisites
- Node.js 18+ for running the development environment
- Basic TypeScript/JavaScript knowledge for frontend development
- Understanding of React for UI components (optional but recommended)
- Sui wallet installed (Sui Wallet, Suiet, or other Sui-compatible wallets)
Core Technologies
Essential Packages
# Core Sui SDK (modular imports)
npm install @mysten/sui
# dApp Kit for React applications
npm install @mysten/dapp-kit
# Additional utilities
npm install @mysten/dapp-kit @tanstack/react-query
Package Overview
- @mysten/sui: Core SDK for blockchain interactions
- @mysten/dapp-kit: React hooks and components for wallet integration
- @tanstack/react-query: State management for async data (recommended)
Basic Setup
1. Initialize Sui Client
import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
// Initialize client for different networks
const client = new SuiClient({
url: getFullnodeUrl('mainnet') // 'testnet', 'devnet', 'localnet'
});
// Using BlockEden.xyz endpoint
const blockEdenClient = new SuiClient({
url: 'https://api.blockeden.xyz/sui/<access_key>'
});
2. React dApp Setup with dApp Kit
// App.tsx
import { SuiClientProvider, WalletProvider } from '@mysten/dapp-kit';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { getFullnodeUrl } from '@mysten/sui/client';
const queryClient = new QueryClient();
const networks = {
mainnet: { url: getFullnodeUrl('mainnet') },
testnet: { url: getFullnodeUrl('testnet') },
};
function App() {
return (
<QueryClientProvider client={queryClient}>
<SuiClientProvider networks={networks} defaultNetwork="testnet">
<WalletProvider>
<MyDApp />
</WalletProvider>
</SuiClientProvider>
</QueryClientProvider>
);
}
3. Wallet Connection Component
// WalletConnection.tsx
import { ConnectButton, useCurrentAccount } from '@mysten/dapp-kit';
export function WalletConnection() {
const currentAccount = useCurrentAccount();
return (
<div className="wallet-section">
<ConnectButton />
{currentAccount && (
<div>
<p>Connected: {currentAccount.address}</p>
</div>
)}
</div>
);
}
Working with Transactions
Building and Executing Transactions
import { Transaction } from '@mysten/sui/transactions';
import { useSignAndExecuteTransaction, useSuiClient } from '@mysten/dapp-kit';
export function TransferSui() {
const client = useSuiClient();
const { mutate: signAndExecute } = useSignAndExecuteTransaction();
const transferSui = async (recipient: string, amount: number) => {
const tx = new Transaction();
// Convert SUI to MIST (1 SUI = 1,000,000,000 MIST)
const amountMIST = amount * 1_000_000_000;
// Split coins and transfer
const [coin] = tx.splitCoins(tx.gas, [amountMIST]);
tx.transferObjects([coin], recipient);
// Execute transaction
signAndExecute(
{ transaction: tx },
{
onSuccess: (result) => {
console.log('Transfer successful:', result.digest);
},
onError: (error) => {
console.error('Transfer failed:', error);
},
}
);
};
return (
<button onClick={() => transferSui('0x...', 1)}>
Send 1 SUI
</button>
);
}
Move Call Transactions
import { Transaction } from '@mysten/sui/transactions';
export function MoveCallExample() {
const { mutate: signAndExecute } = useSignAndExecuteTransaction();
const callMoveFunction = async () => {
const tx = new Transaction();
// Call a Move function
tx.moveCall({
target: '0x2::coin::split',
arguments: [
tx.object('0x...'), // coin object
tx.pure.u64(1000000000), // amount in MIST
],
});
signAndExecute({ transaction: tx });
};
return <button onClick={callMoveFunction}>Call Move Function</button>;
}
Data Fetching Patterns
Using React Query with Sui Data
import { useQuery } from '@tanstack/react-query';
import { useSuiClient } from '@mysten/dapp-kit';
export function AccountBalance({ address }: { address: string }) {
const client = useSuiClient();
const { data: balance, isLoading, error } = useQuery({
queryKey: ['balance', address],
queryFn: () => client.getBalance({ owner: address }),
enabled: !!address,
refetchInterval: 10000, // Refetch every 10 seconds
});
if (isLoading) return <div>Loading balance...</div>;
if (error) return <div>Error loading balance</div>;
return (
<div>
Balance: {balance ? Number(balance.totalBalance) / 1_000_000_000 : 0} SUI
</div>
);
}
Fetching Objects and Events
export function ObjectsDisplay({ address }: { address: string }) {
const client = useSuiClient();
const { data: objects } = useQuery({
queryKey: ['objects', address],
queryFn: () => client.getOwnedObjects({
owner: address,
options: {
showContent: true,
showType: true,
},
}),
enabled: !!address,
});
return (
<div>
<h3>Owned Objects</h3>
{objects?.data.map((obj) => (
<div key={obj.data?.objectId}>
<p>Object ID: {obj.data?.objectId}</p>
<p>Type: {obj.data?.type}</p>
</div>
))}
</div>
);
}
Event Handling and Real-time Updates
Listening to Events
import { useEffect, useState } from 'react';
import { useSuiClient } from '@mysten/dapp-kit';
export function EventListener() {
const client = useSuiClient();
const [events, setEvents] = useState<any[]>([]);
useEffect(() => {
const fetchEvents = async () => {
try {
const eventData = await client.queryEvents({
query: { MoveModule: { package: '0x2', module: 'coin' } },
limit: 10,
order: 'descending',
});
setEvents(eventData.data);
} catch (error) {
console.error('Error fetching events:', error);
}
};
fetchEvents();
// Poll for new events every 5 seconds
const interval = setInterval(fetchEvents, 5000);
return () => clearInterval(interval);
}, [client]);
return (
<div>
<h3>Recent Events</h3>
{events.map((event, index) => (
<div key={index}>
<p>Type: {event.type}</p>
<p>Timestamp: {event.timestampMs}</p>
</div>
))}
</div>
);
}
Error Handling Best Practices
Comprehensive Error Handling
import { SuiError } from '@mysten/sui/client';
export function RobustTransaction() {
const { mutate: signAndExecute } = useSignAndExecuteTransaction();
const executeWithErrorHandling = async () => {
const tx = new Transaction();
// ... build transaction
signAndExecute(
{ transaction: tx },
{
onSuccess: (result) => {
if (result.effects?.status?.status === 'success') {
console.log('Transaction succeeded:', result.digest);
} else {
console.error('Transaction failed:', result.effects?.status);
}
},
onError: (error) => {
if (error instanceof SuiError) {
// Handle Sui-specific errors
console.error('Sui error:', error.message);
} else if (error.message.includes('User rejected')) {
// Handle user rejection
console.log('User cancelled transaction');
} else {
// Handle other errors
console.error('Unexpected error:', error);
}
},
}
);
};
return <button onClick={executeWithErrorHandling}>Execute Transaction</button>;
}
Network Error Handling
export function useResilientQuery<T>(
queryKey: any[],
queryFn: () => Promise<T>,
options?: any
) {
return useQuery({
queryKey,
queryFn,
retry: (failureCount, error) => {
// Retry up to 3 times for network errors
if (failureCount < 3 && error.message.includes('network')) {
return true;
}
return false;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
...options,
});
}
Performance Optimization
Efficient Data Loading
// Batch multiple queries for better performance
export function BatchedDataLoader({ addresses }: { addresses: string[] }) {
const client = useSuiClient();
const { data: balances } = useQuery({
queryKey: ['batchedBalances', addresses],
queryFn: async () => {
// Use Promise.all for concurrent requests
const balancePromises = addresses.map(address =>
client.getBalance({ owner: address })
);
return Promise.all(balancePromises);
},
enabled: addresses.length > 0,
});
return (
<div>
{balances?.map((balance, index) => (
<div key={addresses[index]}>
Address: {addresses[index]} -
Balance: {Number(balance.totalBalance) / 1_000_000_000} SUI
</div>
))}
</div>
);
}
Memoization for Heavy Computations
import { useMemo } from 'react';
export function ProcessedData({ objects }: { objects: any[] }) {
const processedData = useMemo(() => {
return objects
.filter(obj => obj.data?.type?.includes('::coin::'))
.map(obj => ({
id: obj.data.objectId,
balance: obj.data.content?.fields?.balance || 0,
}))
.sort((a, b) => b.balance - a.balance);
}, [objects]);
return (
<div>
{processedData.map(item => (
<div key={item.id}>
Coin: {item.id} - Balance: {item.balance}
</div>
))}
</div>
);
}
Testing Strategies
Unit Testing with Jest
// __tests__/utils.test.ts
import { describe, it, expect } from '@jest/globals';
import { formatSuiAmount, validateSuiAddress } from '../utils';
describe('Sui utilities', () => {
it('should format SUI amounts correctly', () => {
expect(formatSuiAmount(1000000000)).toBe('1.00');
expect(formatSuiAmount(1500000000)).toBe('1.50');
});
it('should validate Sui addresses', () => {
expect(validateSuiAddress('0x1')).toBe(true);
expect(validateSuiAddress('invalid')).toBe(false);
});
});
Integration Testing
// __tests__/integration.test.ts
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient } from '@tanstack/react-query';
import { SuiClient } from '@mysten/sui/client';
import { WalletConnection } from '../components/WalletConnection';
describe('Wallet Integration', () => {
it('should display connection status', async () => {
render(<WalletConnection />);
await waitFor(() => {
expect(screen.getByText('Connect Wallet')).toBeInTheDocument();
});
});
});