Saltar al contenido principal

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();
});
});
});

Deployment Considerations

Environment Configuration

// config/sui.ts
export const getSuiConfig = () => {
const environment = process.env.NODE_ENV;

switch (environment) {
case 'production':
return {
network: 'mainnet',
rpcUrl: process.env.NEXT_PUBLIC_SUI_MAINNET_URL,
};
case 'staging':
return {
network: 'testnet',
rpcUrl: process.env.NEXT_PUBLIC_SUI_TESTNET_URL,
};
default:
return {
network: 'devnet',
rpcUrl: getFullnodeUrl('devnet'),
};
}
};

Production Optimizations

// Use environment variables for API keys
const client = new SuiClient({
url: process.env.NEXT_PUBLIC_BLOCKEDEN_SUI_URL || getFullnodeUrl('mainnet')
});

// Implement proper error boundaries
export class SuiErrorBoundary extends React.Component {
constructor(props: any) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error: Error) {
return { hasError: true };
}

componentDidCatch(error: Error, errorInfo: any) {
console.error('Sui dApp error:', error, errorInfo);
// Log to error reporting service
}

render() {
if (this.state.hasError) {
return <h1>Something went wrong with the Sui connection.</h1>;
}

return this.props.children;
}
}

Best Practices Summary

  1. Use TypeScript for better type safety and developer experience
  2. Implement proper error handling for all blockchain interactions
  3. Cache data effectively using React Query or similar libraries
  4. Test thoroughly with both unit and integration tests
  5. Monitor performance and optimize data fetching patterns
  6. Handle network failures gracefully with retry logic
  7. Secure API keys using environment variables
  8. Use modular imports from @mysten/sui for smaller bundle sizes

Next Steps

Resources