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
- Use TypeScript for better type safety with Move contracts
- Implement proper error handling for all contract interactions
- Use view functions for reading data efficiently
- Validate inputs before sending transactions
- Handle resource states (existence checks) properly
- Monitor events for real-time updates
- Test thoroughly with both unit and integration tests
- Use React Query for efficient data fetching and caching
- Secure deployment using environment variables
- 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