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