Saltar al contenido principal

Aptos Move Integration Guide

This guide covers integrating Aptos Move smart contracts with TypeScript frontends, including contract interaction patterns, resource management, event handling, and testing strategies for 2025.

Prerequisites

  • Aptos CLI installed for Move development
  • Understanding of Move language basics
  • TypeScript/React knowledge for frontend integration
  • @aptos-labs/ts-sdk for blockchain interactions

Move Contract Structure

Basic Move Module Example

// sources/counter.move
module my_address::counter {
use std::signer;
use aptos_framework::event;

struct Counter has key {
value: u64,
}

#[event]
struct CounterCreated has drop, store {
account: address,
initial_value: u64,
}

#[event]
struct CounterIncremented has drop, store {
account: address,
old_value: u64,
new_value: u64,
}

public entry fun initialize_counter(account: &signer) {
let account_addr = signer::address_of(account);

assert!(!exists<Counter>(account_addr), 1);

let counter = Counter { value: 0 };
move_to(account, counter);

event::emit(CounterCreated {
account: account_addr,
initial_value: 0,
});
}

public entry fun increment(account: &signer) acquires Counter {
let account_addr = signer::address_of(account);
let counter = borrow_global_mut<Counter>(account_addr);

let old_value = counter.value;
counter.value = counter.value + 1;

event::emit(CounterIncremented {
account: account_addr,
old_value,
new_value: counter.value,
});
}

#[view]
public fun get_value(account_addr: address): u64 acquires Counter {
borrow_global<Counter>(account_addr).value
}

public entry fun set_value(account: &signer, new_value: u64) acquires Counter {
let account_addr = signer::address_of(account);
let counter = borrow_global_mut<Counter>(account_addr);
counter.value = new_value;
}
}

Move.toml Configuration

[package]
name = "counter"
version = "1.0.0"
authors = ["your-email@example.com"]

[dependencies.AptosFramework]
git = "https://github.com/aptos-labs/aptos-core.git"
rev = "mainnet"
subdir = "aptos-move/framework/aptos-framework"

[addresses]
my_address = "_"

TypeScript Integration Patterns

Contract Interaction Utilities

// utils/counterContract.ts
import {
Aptos,
AptosConfig,
Network,
InputViewFunctionData,
Account,
AccountAddress,
} from '@aptos-labs/ts-sdk';

export class CounterContract {
private moduleAddress: string;
private moduleName: string = 'counter';

constructor(
private aptos: Aptos,
moduleAddress: string
) {
this.moduleAddress = moduleAddress;
}

// Initialize counter for an account
async initializeCounter(account: Account) {
const transaction = await this.aptos.transaction.build.simple({
sender: account.accountAddress,
data: {
function: `${this.moduleAddress}::${this.moduleName}::initialize_counter`,
functionArguments: [],
},
});

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

return this.aptos.waitForTransaction({
transactionHash: response.hash,
});
}

// Increment counter
async incrementCounter(account: Account) {
const transaction = await this.aptos.transaction.build.simple({
sender: account.accountAddress,
data: {
function: `${this.moduleAddress}::${this.moduleName}::increment`,
functionArguments: [],
},
});

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

return this.aptos.waitForTransaction({
transactionHash: response.hash,
});
}

// Set counter value
async setCounterValue(account: Account, value: number) {
const transaction = await this.aptos.transaction.build.simple({
sender: account.accountAddress,
data: {
function: `${this.moduleAddress}::${this.moduleName}::set_value`,
functionArguments: [value],
},
});

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

return this.aptos.waitForTransaction({
transactionHash: response.hash,
});
}

// Read counter value using view function
async getCounterValue(accountAddress: string): Promise<number> {
try {
const result = await this.aptos.view({
payload: {
function: `${this.moduleAddress}::${this.moduleName}::get_value`,
functionArguments: [accountAddress],
},
});

return Number(result[0]);
} catch (error) {
console.error('Error reading counter value:', error);
throw error;
}
}

// Check if counter exists for account
async hasCounter(accountAddress: string): Promise<boolean> {
try {
const resource = await this.aptos.getAccountResource({
accountAddress,
resourceType: `${this.moduleAddress}::${this.moduleName}::Counter`,
});
return !!resource;
} catch (error) {
return false;
}
}

// Get counter resource directly
async getCounterResource(accountAddress: string) {
return this.aptos.getAccountResource({
accountAddress,
resourceType: `${this.moduleAddress}::${this.moduleName}::Counter`,
});
}
}

React Hook for Contract Interaction

// hooks/useCounter.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useWallet } from '@aptos-labs/wallet-adapter-react';
import { Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk';
import { CounterContract } from '../utils/counterContract';

const MODULE_ADDRESS = '0x...'; // Your deployed module address

export function useCounter(accountAddress?: string) {
const { account, signAndSubmitTransaction } = useWallet();
const queryClient = useQueryClient();

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

// Query if counter exists
const {
data: hasCounter,
isLoading: checkingCounter
} = useQuery({
queryKey: ['hasCounter', accountAddress],
queryFn: () => contract.hasCounter(accountAddress!),
enabled: !!accountAddress,
});

// Query counter value
const {
data: counterValue,
isLoading: loadingValue,
error
} = useQuery({
queryKey: ['counterValue', accountAddress],
queryFn: () => contract.getCounterValue(accountAddress!),
enabled: !!accountAddress && hasCounter,
refetchInterval: 5000, // Refresh every 5 seconds
});

// Initialize counter mutation
const initializeCounter = useMutation({
mutationFn: async () => {
if (!account) throw new Error('Wallet not connected');

const transaction = await aptos.transaction.build.simple({
sender: account.address,
data: {
function: `${MODULE_ADDRESS}::counter::initialize_counter`,
functionArguments: [],
},
});

const response = await signAndSubmitTransaction({ transaction });
return aptos.waitForTransaction({ transactionHash: response.hash });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['hasCounter', account?.address] });
queryClient.invalidateQueries({ queryKey: ['counterValue', account?.address] });
},
});

// Increment mutation
const incrementCounter = useMutation({
mutationFn: async () => {
if (!account) throw new Error('Wallet not connected');

const transaction = await aptos.transaction.build.simple({
sender: account.address,
data: {
function: `${MODULE_ADDRESS}::counter::increment`,
functionArguments: [],
},
});

const response = await signAndSubmitTransaction({ transaction });
return aptos.waitForTransaction({ transactionHash: response.hash });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['counterValue', account?.address] });
},
});

// Set value mutation
const setCounterValue = useMutation({
mutationFn: async (value: number) => {
if (!account) throw new Error('Wallet not connected');

const transaction = await aptos.transaction.build.simple({
sender: account.address,
data: {
function: `${MODULE_ADDRESS}::counter::set_value`,
functionArguments: [value],
},
});

const response = await signAndSubmitTransaction({ transaction });
return aptos.waitForTransaction({ transactionHash: response.hash });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['counterValue', account?.address] });
},
});

return {
hasCounter,
checkingCounter,
counterValue,
loadingValue,
error,
initializeCounter,
incrementCounter,
setCounterValue,
};
}

Counter Component Implementation

// components/Counter.tsx
import React, { useState } from 'react';
import { useWallet } from '@aptos-labs/wallet-adapter-react';
import { useCounter } from '../hooks/useCounter';

export function Counter() {
const { account } = useWallet();
const [newValue, setNewValue] = useState<number>(0);

const {
hasCounter,
checkingCounter,
counterValue,
loadingValue,
error,
initializeCounter,
incrementCounter,
setCounterValue,
} = useCounter(account?.address);

if (!account) {
return <div>Please connect your wallet</div>;
}

if (checkingCounter) {
return <div>Checking if counter exists...</div>;
}

if (!hasCounter) {
return (
<div className="counter-container">
<h3>Initialize Your Counter</h3>
<button
onClick={() => initializeCounter.mutate()}
disabled={initializeCounter.isPending}
>
{initializeCounter.isPending ? 'Initializing...' : 'Initialize Counter'}
</button>
{initializeCounter.error && (
<div className="error">
Error: {initializeCounter.error.message}
</div>
)}
</div>
);
}

if (loadingValue) return <div>Loading counter value...</div>;
if (error) return <div>Error: {error.message}</div>;

return (
<div className="counter-container">
<h3>Your Counter</h3>
<div className="counter-display">
<span className="counter-value">{counterValue}</span>
</div>

<div className="counter-actions">
<button
onClick={() => incrementCounter.mutate()}
disabled={incrementCounter.isPending}
>
{incrementCounter.isPending ? 'Incrementing...' : '+1'}
</button>

<div className="set-value-section">
<input
type="number"
value={newValue}
onChange={(e) => setNewValue(Number(e.target.value))}
placeholder="New value"
/>
<button
onClick={() => setCounterValue.mutate(newValue)}
disabled={setCounterValue.isPending}
>
{setCounterValue.isPending ? 'Setting...' : 'Set Value'}
</button>
</div>
</div>

{(incrementCounter.error || setCounterValue.error) && (
<div className="error">
Error: {(incrementCounter.error || setCounterValue.error)?.message}
</div>
)}
</div>
);
}

Advanced Patterns

Working with Complex Resources

// sources/marketplace.move
module my_address::marketplace {
use std::signer;
use std::string::String;
use aptos_framework::coin;
use aptos_framework::aptos_coin::AptosCoin;

struct Item has key, store {
name: String,
description: String,
price: u64,
owner: address,
for_sale: bool,
}

struct Marketplace has key {
items: vector<Item>,
fee_percentage: u8,
}

public entry fun create_marketplace(account: &signer, fee_percentage: u8) {
let marketplace = Marketplace {
items: vector::empty(),
fee_percentage,
};
move_to(account, marketplace);
}

public entry fun list_item(
seller: &signer,
marketplace_addr: address,
name: String,
description: String,
price: u64,
) acquires Marketplace {
let seller_addr = signer::address_of(seller);
let marketplace = borrow_global_mut<Marketplace>(marketplace_addr);

let item = Item {
name,
description,
price,
owner: seller_addr,
for_sale: true,
};

vector::push_back(&mut marketplace.items, item);
}

public entry fun buy_item(
buyer: &signer,
marketplace_addr: address,
item_index: u64,
payment: coin::Coin<AptosCoin>,
) acquires Marketplace {
let marketplace = borrow_global_mut<Marketplace>(marketplace_addr);
let item = vector::borrow_mut(&mut marketplace.items, item_index);

assert!(item.for_sale, 1);
assert!(coin::value(&payment) >= item.price, 2);

// Transfer payment to seller (minus fee)
let fee = (item.price * (marketplace.fee_percentage as u64)) / 100;
let seller_amount = item.price - fee;

// In a real implementation, you'd split the payment appropriately
coin::deposit(item.owner, payment);

item.owner = signer::address_of(buyer);
item.for_sale = false;
}
}
// TypeScript integration for marketplace
export class MarketplaceContract {
constructor(
private aptos: Aptos,
private moduleAddress: string
) {}

async createMarketplace(account: Account, feePercentage: number) {
const transaction = await this.aptos.transaction.build.simple({
sender: account.accountAddress,
data: {
function: `${this.moduleAddress}::marketplace::create_marketplace`,
functionArguments: [feePercentage],
},
});

return this.aptos.signAndSubmitTransaction({
signer: account,
transaction,
});
}

async listItem(
seller: Account,
marketplaceAddress: string,
name: string,
description: string,
priceInApt: number
) {
const priceInOctas = priceInApt * 100000000; // Convert APT to Octas

const transaction = await this.aptos.transaction.build.simple({
sender: seller.accountAddress,
data: {
function: `${this.moduleAddress}::marketplace::list_item`,
functionArguments: [
marketplaceAddress,
name,
description,
priceInOctas,
],
},
});

return this.aptos.signAndSubmitTransaction({
signer: seller,
transaction,
});
}

async getMarketplaceItems(marketplaceAddress: string) {
try {
const resource = await this.aptos.getAccountResource({
accountAddress: marketplaceAddress,
resourceType: `${this.moduleAddress}::marketplace::Marketplace`,
});

return (resource.data as any).items;
} catch (error) {
console.error('Error fetching marketplace items:', error);
throw error;
}
}
}

Event Handling and Processing

// utils/eventProcessor.ts
import { Aptos, Event } from '@aptos-labs/ts-sdk';

export class EventProcessor {
constructor(
private aptos: Aptos,
private moduleAddress: string
) {}

async getCounterEvents(accountAddress?: string) {
const events = await this.aptos.getEvents({
eventType: `${this.moduleAddress}::counter::CounterCreated`,
options: {
limit: 100,
orderBy: [{ transaction_version: 'desc' }],
where: accountAddress ? {
account: { _eq: accountAddress }
} : undefined,
},
});

return events.map(event => ({
...event,
data: event.data as {
account: string;
initial_value: string;
},
}));
}

async getIncrementEvents(accountAddress?: string) {
const events = await this.aptos.getEvents({
eventType: `${this.moduleAddress}::counter::CounterIncremented`,
options: {
limit: 100,
orderBy: [{ transaction_version: 'desc' }],
where: accountAddress ? {
account: { _eq: accountAddress }
} : undefined,
},
});

return events.map(event => ({
...event,
data: event.data as {
account: string;
old_value: string;
new_value: string;
},
}));
}

async subscribeToEvents(
eventType: string,
callback: (event: Event) => void,
pollInterval: number = 5000
) {
let lastVersion = 0;

const poll = async () => {
try {
const events = await this.aptos.getEvents({
eventType,
options: {
limit: 50,
orderBy: [{ transaction_version: 'asc' }],
where: {
transaction_version: { _gt: lastVersion }
},
},
});

events.forEach(event => {
if (Number(event.transaction_version) > lastVersion) {
lastVersion = Number(event.transaction_version);
callback(event);
}
});
} catch (error) {
console.error('Error polling events:', error);
}
};

const intervalId = setInterval(poll, pollInterval);

// Initial poll
poll();

return () => clearInterval(intervalId);
}
}

React Hook for Event Monitoring

// hooks/useContractEvents.ts
import { useEffect, useState } from 'react';
import { Event } from '@aptos-labs/ts-sdk';
import { EventProcessor } from '../utils/eventProcessor';

export function useContractEvents(
moduleAddress: string,
eventType: string,
accountAddress?: string
) {
const [events, setEvents] = useState<Event[]>([]);
const [isListening, setIsListening] = useState(false);

useEffect(() => {
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));
const eventProcessor = new EventProcessor(aptos, moduleAddress);

setIsListening(true);

const unsubscribe = eventProcessor.subscribeToEvents(
eventType,
(event) => {
setEvents(prev => [event, ...prev.slice(0, 99)]); // Keep latest 100 events
}
);

// Load initial events
const loadInitialEvents = async () => {
try {
if (eventType.includes('CounterCreated')) {
const initialEvents = await eventProcessor.getCounterEvents(accountAddress);
setEvents(initialEvents);
} else if (eventType.includes('CounterIncremented')) {
const initialEvents = await eventProcessor.getIncrementEvents(accountAddress);
setEvents(initialEvents);
}
} catch (error) {
console.error('Error loading initial events:', error);
}
};

loadInitialEvents();

return () => {
unsubscribe();
setIsListening(false);
};
}, [moduleAddress, eventType, accountAddress]);

return { events, isListening };
}

Testing Strategies

Unit Tests for Contract Utilities

// __tests__/counterContract.test.ts
import { describe, it, expect, beforeEach } from '@jest/globals';
import { Aptos, AptosConfig, Network, Account } from '@aptos-labs/ts-sdk';
import { CounterContract } from '../utils/counterContract';

describe('CounterContract', () => {
let aptos: Aptos;
let contract: CounterContract;
let account: Account;

beforeEach(() => {
aptos = new Aptos(new AptosConfig({ network: Network.DEVNET }));
contract = new CounterContract(aptos, '0x123');
account = Account.generate();
});

it('should check if counter exists', async () => {
const hasCounter = await contract.hasCounter(account.accountAddress.toString());
expect(typeof hasCounter).toBe('boolean');
});

it('should get counter value for existing counter', async () => {
// Mock the aptos.view method
jest.spyOn(aptos, 'view').mockResolvedValue([42]);

const value = await contract.getCounterValue(account.accountAddress.toString());
expect(value).toBe(42);
});
});

Integration Tests with Local Network

// __tests__/integration.test.ts
import { describe, it, expect, beforeAll } from '@jest/globals';
import { Aptos, AptosConfig, Network, Account } from '@aptos-labs/ts-sdk';
import { CounterContract } from '../utils/counterContract';

describe('Counter Integration Tests', () => {
let aptos: Aptos;
let account: Account;
let contract: CounterContract;
let moduleAddress: string;

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

// Deploy contract and get module address
moduleAddress = await deployCounterModule(aptos, account);
contract = new CounterContract(aptos, moduleAddress);
});

it('should initialize and increment counter', async () => {
// Check counter doesn't exist initially
const initialExists = await contract.hasCounter(account.accountAddress.toString());
expect(initialExists).toBe(false);

// Initialize counter
const initResult = await contract.initializeCounter(account);
expect(initResult.success).toBe(true);

// Check counter now exists
const existsAfterInit = await contract.hasCounter(account.accountAddress.toString());
expect(existsAfterInit).toBe(true);

// Check initial value is 0
const initialValue = await contract.getCounterValue(account.accountAddress.toString());
expect(initialValue).toBe(0);

// Increment counter
const incrementResult = await contract.incrementCounter(account);
expect(incrementResult.success).toBe(true);

// Check value is now 1
const newValue = await contract.getCounterValue(account.accountAddress.toString());
expect(newValue).toBe(1);
});

it('should set counter to specific value', async () => {
const setValue = 42;
const setResult = await contract.setCounterValue(account, setValue);
expect(setResult.success).toBe(true);

const value = await contract.getCounterValue(account.accountAddress.toString());
expect(value).toBe(setValue);
});
});

async function deployCounterModule(aptos: Aptos, account: Account): Promise<string> {
// Implementation would involve:
// 1. Compiling Move code
// 2. Publishing the module
// 3. Returning the module address

// For testing, return a mock address
return account.accountAddress.toString();
}

Component Testing with React Testing Library

// __tests__/Counter.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WalletProvider } from '@aptos-labs/wallet-adapter-react';
import { Counter } from '../components/Counter';
import { useCounter } from '../hooks/useCounter';

// Mock the useCounter hook
jest.mock('../hooks/useCounter');
const mockUseCounter = useCounter as jest.MockedFunction<typeof useCounter>;

// Mock wallet context
jest.mock('@aptos-labs/wallet-adapter-react', () => ({
...jest.requireActual('@aptos-labs/wallet-adapter-react'),
useWallet: () => ({
account: { address: '0x123' },
connected: true,
}),
}));

describe('Counter Component', () => {
let queryClient: QueryClient;

beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
});

it('should display initialize button when counter does not exist', async () => {
mockUseCounter.mockReturnValue({
hasCounter: false,
checkingCounter: false,
counterValue: undefined,
loadingValue: false,
error: null,
initializeCounter: { mutate: jest.fn(), isPending: false, error: null },
incrementCounter: { mutate: jest.fn(), isPending: false, error: null },
setCounterValue: { mutate: jest.fn(), isPending: false, error: null },
});

render(
<QueryClientProvider client={queryClient}>
<Counter />
</QueryClientProvider>
);

expect(screen.getByText('Initialize Counter')).toBeInTheDocument();
});

it('should display counter value when counter exists', async () => {
mockUseCounter.mockReturnValue({
hasCounter: true,
checkingCounter: false,
counterValue: 42,
loadingValue: false,
error: null,
initializeCounter: { mutate: jest.fn(), isPending: false, error: null },
incrementCounter: { mutate: jest.fn(), isPending: false, error: null },
setCounterValue: { mutate: jest.fn(), isPending: false, error: null },
});

render(
<QueryClientProvider client={queryClient}>
<Counter />
</QueryClientProvider>
);

await waitFor(() => {
expect(screen.getByText('42')).toBeInTheDocument();
});
});

it('should call increment when +1 button is clicked', async () => {
const mockIncrement = jest.fn();
mockUseCounter.mockReturnValue({
hasCounter: true,
checkingCounter: false,
counterValue: 0,
loadingValue: false,
error: null,
initializeCounter: { mutate: jest.fn(), isPending: false, error: null },
incrementCounter: { mutate: mockIncrement, isPending: false, error: null },
setCounterValue: { mutate: jest.fn(), isPending: false, error: null },
});

render(
<QueryClientProvider client={queryClient}>
<Counter />
</QueryClientProvider>
);

fireEvent.click(screen.getByText('+1'));
expect(mockIncrement).toHaveBeenCalled();
});
});

Deployment and Production Considerations

Module Publishing Script

// scripts/deploy.ts
import { Aptos, AptosConfig, Network, Account } from '@aptos-labs/ts-sdk';
import * as fs from 'fs';
import * as path from 'path';

async function deployModule() {
const network = process.env.APTOS_NETWORK as Network || Network.DEVNET;
const aptos = new Aptos(new AptosConfig({ network }));

// Load deployer account from environment
const privateKey = process.env.DEPLOYER_PRIVATE_KEY;
if (!privateKey) {
throw new Error('DEPLOYER_PRIVATE_KEY environment variable not set');
}

const account = Account.fromPrivateKey({ privateKey });

// Read compiled module bytecode
const packagePath = path.join(__dirname, '../move/build/counter');
const packageMetadata = JSON.parse(
fs.readFileSync(path.join(packagePath, 'package-metadata.json'), 'utf8')
);

const modules = packageMetadata.modules.map((module: any) => ({
bytecode: module.bytecode,
}));

// Publish the module
const transaction = await aptos.transaction.build.simple({
sender: account.accountAddress,
data: {
function: '0x1::code::publish_package_txn',
functionArguments: [
packageMetadata.source_digest,
modules,
],
},
});

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

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

console.log('Module deployed successfully!');
console.log('Transaction hash:', response.hash);
console.log('Module address:', account.accountAddress.toString());

return {
moduleAddress: account.accountAddress.toString(),
transactionHash: response.hash,
};
}

if (require.main === module) {
deployModule().catch(console.error);
}

Environment Configuration

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

export const getAptosConfig = () => {
const environment = process.env.NODE_ENV;
const network = process.env.NEXT_PUBLIC_APTOS_NETWORK as Network;

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 || Network.DEVNET,
});
}
};

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

switch (environment) {
case 'production':
return process.env.NEXT_PUBLIC_COUNTER_MODULE_MAINNET;
case 'staging':
return process.env.NEXT_PUBLIC_COUNTER_MODULE_TESTNET;
default:
return process.env.NEXT_PUBLIC_COUNTER_MODULE_DEVNET;
}
};

Best Practices Summary

  1. Use TypeScript for better type safety with Move contracts
  2. Implement proper error handling for all contract interactions
  3. Use view functions for reading data efficiently
  4. Validate inputs before sending transactions
  5. Handle resource states (existence checks) properly
  6. Monitor events for real-time updates
  7. Test thoroughly with both unit and integration tests
  8. Use React Query for efficient data fetching and caching
  9. Secure deployment using environment variables
  10. Version your contracts for smooth upgrades

Next Steps

  • Explore Multi-Chain dApp Architecture for cross-chain applications
  • Implement performance optimizations for production applications
  • Follow security best practices for production deployment

Resources