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