본문으로 건너뛰기

Aptos dApp Development Guide

This comprehensive guide covers building modern decentralized applications (dApps) on the Aptos blockchain using the latest @aptos-labs/ts-sdk and development 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)
  • Aptos wallet installed (Petra, Pontem, or other Aptos-compatible wallets)

Core Technologies

Essential Packages

# Core Aptos SDK
npm install @aptos-labs/ts-sdk

# React integration (optional)
npm install react react-dom @types/react @types/react-dom

# State management
npm install @tanstack/react-query

# UI components (optional)
npm install @aptos-labs/wallet-adapter-react

Package Overview

  • @aptos-labs/ts-sdk: Core SDK for blockchain interactions
  • @aptos-labs/wallet-adapter-react: React hooks for wallet integration
  • @tanstack/react-query: State management for async data

Basic Setup

1. Initialize Aptos Client

import { Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk';

// Initialize for different networks
const config = new AptosConfig({
network: Network.MAINNET // Network.TESTNET, Network.DEVNET
});
const aptos = new Aptos(config);

// Using BlockEden.xyz endpoint
const blockEdenConfig = new AptosConfig({
fullnode: 'https://aptos-mainnet.blockeden.xyz/<access_key>',
network: Network.MAINNET,
});
const blockEdenAptos = new Aptos(blockEdenConfig);

2. React dApp Setup with Wallet Integration

// App.tsx
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WalletProvider } from '@aptos-labs/wallet-adapter-react';
import { PetraWallet } from 'petra-plugin-wallet-adapter';
import { PontemWallet } from '@pontem/wallet-adapter-plugin';

const queryClient = new QueryClient();

const wallets = [
new PetraWallet(),
new PontemWallet(),
];

function App() {
return (
<QueryClientProvider client={queryClient}>
<WalletProvider wallets={wallets} autoConnect={true}>
<MyDApp />
</WalletProvider>
</QueryClientProvider>
);
}

export default App;

3. Wallet Connection Component

// WalletConnection.tsx
import React from 'react';
import { useWallet } from '@aptos-labs/wallet-adapter-react';

export function WalletConnection() {
const {
connect,
disconnect,
account,
connected,
wallets,
wallet
} = useWallet();

const handleConnect = async (walletName: string) => {
try {
await connect(walletName);
} catch (error) {
console.error('Failed to connect wallet:', error);
}
};

const handleDisconnect = async () => {
try {
await disconnect();
} catch (error) {
console.error('Failed to disconnect wallet:', error);
}
};

if (connected && account) {
return (
<div className="wallet-connected">
<p>Connected: {account.address}</p>
<p>Wallet: {wallet?.name}</p>
<button onClick={handleDisconnect}>Disconnect</button>
</div>
);
}

return (
<div className="wallet-selection">
<h3>Connect Your Wallet</h3>
{wallets.map((wallet) => (
<button
key={wallet.name}
onClick={() => handleConnect(wallet.name)}
disabled={!wallet.readyState}
>
Connect {wallet.name}
</button>
))}
</div>
);
}

Working with Transactions

Basic APT Transfer

import { Account, Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk';
import { useWallet } from '@aptos-labs/wallet-adapter-react';

export function TransferAPT() {
const { account, signAndSubmitTransaction } = useWallet();
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));

const transferAPT = async (recipient: string, amount: number) => {
if (!account) {
throw new Error('Wallet not connected');
}

try {
const transaction = await aptos.transaction.build.simple({
sender: account.address,
data: {
function: '0x1::aptos_account::transfer',
functionArguments: [
recipient, // recipient address
amount * 100000000, // amount in Octas (1 APT = 100,000,000 Octas)
],
},
});

const response = await signAndSubmitTransaction({
transaction,
});

// Wait for transaction confirmation
const executedTransaction = await aptos.waitForTransaction({
transactionHash: response.hash,
});

console.log('Transfer successful:', executedTransaction);
return executedTransaction;
} catch (error) {
console.error('Transfer failed:', error);
throw error;
}
};

return (
<div>
<button onClick={() => transferAPT('0x...', 1)}>
Send 1 APT
</button>
</div>
);
}

Multi-Agent Transactions

export function MultiAgentTransaction() {
const { account, signAndSubmitTransaction } = useWallet();
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));

const createMultiAgentTx = async (secondaryAccount: string) => {
if (!account) throw new Error('Wallet not connected');

try {
const transaction = await aptos.transaction.build.multiAgent({
sender: account.address,
secondarySignerAddresses: [secondaryAccount],
data: {
function: '0x1::aptos_account::transfer',
functionArguments: [
account.address, // Transfer back to primary account
1000000, // 0.01 APT
],
},
});

const response = await signAndSubmitTransaction({
transaction,
});

return await aptos.waitForTransaction({
transactionHash: response.hash,
});
} catch (error) {
console.error('Multi-agent transaction failed:', error);
throw error;
}
};

return (
<button onClick={() => createMultiAgentTx('0x...')}>
Create Multi-Agent Transaction
</button>
);
}

Data Fetching Patterns

Account Information and Balance

import { useQuery } from '@tanstack/react-query';
import { Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk';

const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));

export function AccountBalance({ address }: { address: string }) {
const { data: balance, isLoading, error } = useQuery({
queryKey: ['balance', address],
queryFn: async () => {
const resources = await aptos.getAccountResources({
accountAddress: address,
});

const coinStore = resources.find(
(resource) => resource.type === '0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>'
);

if (coinStore?.data) {
return (coinStore.data as any).coin.value;
}
return '0';
},
enabled: !!address,
refetchInterval: 10000, // Refresh every 10 seconds
});

if (isLoading) return <div>Loading balance...</div>;
if (error) return <div>Error loading balance</div>;

const aptBalance = balance ? Number(balance) / 100000000 : 0;

return (
<div>
Balance: {aptBalance.toFixed(8)} APT
</div>
);
}

Account Resources and Modules

export function AccountResources({ address }: { address: string }) {
const { data: resources } = useQuery({
queryKey: ['resources', address],
queryFn: () => aptos.getAccountResources({ accountAddress: address }),
enabled: !!address,
});

const { data: modules } = useQuery({
queryKey: ['modules', address],
queryFn: () => aptos.getAccountModules({ accountAddress: address }),
enabled: !!address,
});

return (
<div>
<h3>Account Resources</h3>
<div>Total Resources: {resources?.length || 0}</div>
<div>Total Modules: {modules?.length || 0}</div>

{resources?.slice(0, 5).map((resource, index) => (
<div key={index}>
<strong>Type:</strong> {resource.type}
</div>
))}
</div>
);
}

Transaction History

export function TransactionHistory({ address }: { address: string }) {
const { data: transactions, isLoading } = useQuery({
queryKey: ['transactions', address],
queryFn: () => aptos.getAccountTransactions({
accountAddress: address,
options: {
limit: 10,
orderBy: [{ transaction_version: 'desc' }],
},
}),
enabled: !!address,
refetchInterval: 30000, // Refresh every 30 seconds
});

if (isLoading) return <div>Loading transactions...</div>;

return (
<div>
<h3>Recent Transactions</h3>
{transactions?.map((tx) => (
<div key={tx.version} className="transaction-item">
<div>Version: {tx.version}</div>
<div>Hash: {tx.hash}</div>
<div>Type: {tx.type}</div>
<div>Success: {tx.success ? 'Yes' : 'No'}</div>
</div>
))}
</div>
);
}

Event Handling and Monitoring

Event Listening with React

import { useEffect, useState } from 'react';
import { Event } from '@aptos-labs/ts-sdk';

export function EventMonitor({ eventType }: { eventType: string }) {
const [events, setEvents] = useState<Event[]>([]);
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));

useEffect(() => {
const fetchEvents = async () => {
try {
const eventData = await aptos.getEvents({
eventType,
options: {
limit: 10,
orderBy: [{ transaction_version: 'desc' }],
},
});

setEvents(eventData);
} catch (error) {
console.error('Error fetching events:', error);
}
};

fetchEvents();

// Poll for new events
const interval = setInterval(fetchEvents, 5000);
return () => clearInterval(interval);
}, [eventType]);

return (
<div>
<h3>Events: {eventType}</h3>
{events.map((event, index) => (
<div key={index} className="event-item">
<div>Version: {event.transaction_version}</div>
<div>Sequence: {event.sequence_number}</div>
<div>Data: {JSON.stringify(event.data)}</div>
</div>
))}
</div>
);
}

Custom Hook for Event Subscription

import { useQuery } from '@tanstack/react-query';

export function useEvents(eventType: string, enabled: boolean = true) {
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));

return useQuery({
queryKey: ['events', eventType],
queryFn: () => aptos.getEvents({
eventType,
options: {
limit: 20,
orderBy: [{ transaction_version: 'desc' }],
},
}),
enabled,
refetchInterval: 5000,
});
}

Error Handling and Validation

Comprehensive Error Handling

import { AptosApiError } from '@aptos-labs/ts-sdk';

export function RobustTransaction() {
const { signAndSubmitTransaction } = useWallet();
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));

const executeTransaction = async (transaction: any) => {
try {
// Simulate transaction first
const simulation = await aptos.transaction.simulate.simple({
signerPublicKey: account.publicKey,
transaction,
});

if (!simulation[0].success) {
throw new Error(`Simulation failed: ${simulation[0].vm_status}`);
}

// Execute the transaction
const response = await signAndSubmitTransaction({ transaction });

// Wait for confirmation
const confirmedTx = await aptos.waitForTransaction({
transactionHash: response.hash,
options: {
checkSuccess: true,
},
});

return confirmedTx;
} catch (error) {
if (error instanceof AptosApiError) {
console.error('Aptos API Error:', error.message);
console.error('Status:', error.status);
console.error('Data:', error.data);
} else if (error.message.includes('User rejected')) {
console.log('User cancelled transaction');
} else {
console.error('Unexpected error:', error);
}
throw error;
}
};

return (
<button onClick={() => executeTransaction(/* transaction */)}>
Execute Transaction
</button>
);
}

Input Validation

import { AccountAddress } from '@aptos-labs/ts-sdk';

export function validateAptosAddress(address: string): boolean {
try {
AccountAddress.fromString(address);
return true;
} catch {
return false;
}
}

export function validateAmount(amount: string): { isValid: boolean; error?: string } {
const numAmount = parseFloat(amount);

if (isNaN(numAmount)) {
return { isValid: false, error: 'Invalid number format' };
}

if (numAmount <= 0) {
return { isValid: false, error: 'Amount must be positive' };
}

if (numAmount > 1000000) {
return { isValid: false, error: 'Amount too large' };
}

return { isValid: true };
}

Performance Optimization

Batch Requests

export function BatchedAccountData({ addresses }: { addresses: string[] }) {
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));

const { data: accountData } = useQuery({
queryKey: ['batchedAccounts', addresses],
queryFn: async () => {
// Batch multiple account queries
const accountPromises = addresses.map(address =>
aptos.getAccount({ accountAddress: address })
.catch(error => ({ error, address }))
);

const results = await Promise.allSettled(accountPromises);

return results.map((result, index) => ({
address: addresses[index],
data: result.status === 'fulfilled' ? result.value : null,
error: result.status === 'rejected' ? result.reason : null,
}));
},
enabled: addresses.length > 0,
});

return (
<div>
<h3>Account Data</h3>
{accountData?.map((item, index) => (
<div key={index}>
<strong>{item.address}:</strong>
{item.error ? (
<span> Error loading account</span>
) : (
<span> Sequence: {item.data?.sequence_number}</span>
)}
</div>
))}
</div>
);
}

Caching and Memoization

import { useMemo } from 'react';

export function ProcessedTransactions({ transactions }: { transactions: any[] }) {
const processedData = useMemo(() => {
return transactions
.filter(tx => tx.success)
.map(tx => ({
hash: tx.hash,
version: tx.version,
timestamp: new Date(Number(tx.timestamp) / 1000),
gasUsed: tx.gas_used,
}))
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}, [transactions]);

return (
<div>
{processedData.map(tx => (
<div key={tx.hash}>
<div>Hash: {tx.hash}</div>
<div>Gas Used: {tx.gasUsed}</div>
<div>Time: {tx.timestamp.toLocaleString()}</div>
</div>
))}
</div>
);
}

Testing Strategies

Unit Testing

// __tests__/aptosUtils.test.ts
import { describe, it, expect } from '@jest/globals';
import { validateAptosAddress, validateAmount } from '../utils/validation';

describe('Aptos Utilities', () => {
describe('validateAptosAddress', () => {
it('should validate correct Aptos addresses', () => {
expect(validateAptosAddress('0x1')).toBe(true);
expect(validateAptosAddress('0x1::coin::CoinStore')).toBe(false);
});

it('should reject invalid addresses', () => {
expect(validateAptosAddress('invalid')).toBe(false);
expect(validateAptosAddress('')).toBe(false);
});
});

describe('validateAmount', () => {
it('should validate positive numbers', () => {
expect(validateAmount('1.5')).toEqual({ isValid: true });
expect(validateAmount('100')).toEqual({ isValid: true });
});

it('should reject invalid amounts', () => {
expect(validateAmount('-1')).toEqual({
isValid: false,
error: 'Amount must be positive'
});
expect(validateAmount('abc')).toEqual({
isValid: false,
error: 'Invalid number format'
});
});
});
});

Integration Testing

// __tests__/integration.test.ts
import { Aptos, AptosConfig, Network, Account } from '@aptos-labs/ts-sdk';

describe('Aptos Integration Tests', () => {
let aptos: Aptos;
let account: Account;

beforeAll(async () => {
aptos = new Aptos(new AptosConfig({ network: Network.DEVNET }));
account = Account.generate();

// Fund account for testing
await aptos.fundAccount({
accountAddress: account.accountAddress,
amount: 100000000
});
});

it('should transfer APT successfully', async () => {
const recipient = Account.generate();

const transaction = await aptos.transaction.build.simple({
sender: account.accountAddress,
data: {
function: '0x1::aptos_account::transfer',
functionArguments: [recipient.accountAddress, 1000000],
},
});

const response = await aptos.signAndSubmitTransaction({
signer: account,
transaction,
});

const confirmedTx = await aptos.waitForTransaction({
transactionHash: response.hash,
});

expect(confirmedTx.success).toBe(true);
});
});

Deployment Configuration

Environment-Specific Setup

// config/aptos.ts
import { AptosConfig, Network } from '@aptos-labs/ts-sdk';

export const getAptosConfig = () => {
const environment = process.env.NODE_ENV;

switch (environment) {
case 'production':
return new AptosConfig({
network: Network.MAINNET,
fullnode: process.env.NEXT_PUBLIC_APTOS_MAINNET_URL,
});
case 'staging':
return new AptosConfig({
network: Network.TESTNET,
fullnode: process.env.NEXT_PUBLIC_APTOS_TESTNET_URL,
});
default:
return new AptosConfig({
network: Network.DEVNET,
});
}
};

Production Monitoring

// utils/monitoring.ts
export function logTransaction(hash: string, status: 'success' | 'failure', error?: any) {
const logData = {
transactionHash: hash,
status,
timestamp: new Date().toISOString(),
error: error?.message,
};

// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.log('Transaction log:', logData);
}

// Send to monitoring service in production
if (process.env.NODE_ENV === 'production') {
// Example: send to DataDog, Sentry, etc.
// monitoringService.log('aptos_transaction', logData);
}
}

Best Practices Summary

  1. Use TypeScript for better type safety and developer experience
  2. Implement proper error handling for all blockchain interactions
  3. Validate inputs before sending transactions
  4. Simulate transactions before execution when possible
  5. Use React Query for efficient data fetching and caching
  6. Handle wallet connection states gracefully
  7. Test thoroughly with both unit and integration tests
  8. Monitor transactions in production environments
  9. Secure API endpoints using environment variables

Next Steps

Resources