Skip to main content

Aptos Move Integration Guide

Complete guide to integrating Aptos Move smart contracts with TypeScript applications using modern SDK patterns and best practices.

Share
Last updated on October 5, 2026
On this page

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​

move
// 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​

toml
[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​

typescript
// 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​

typescript
// 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​

typescript
// 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​

move
// 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
// 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​

typescript
// 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​

typescript
// 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​

typescript
// __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​

typescript
// __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​

typescript
// __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​

typescript
// 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​

typescript
// 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​

Was this page helpful?

One API key for 30+ chains

bex router is a single multi-chain gateway — one key, one bill, metered in compute units instead of an account per network.

Explore bex router