Skip to main content

Sui Move Integration Guide

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

Share
Last updated on August 20, 2026

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

Prerequisites

  • Sui CLI installed for Move development
  • Understanding of Move language basics
  • TypeScript/React knowledge for frontend integration
  • @mysten/sui SDK for blockchain interactions

Move Contract Structure

Basic Move Module Example

move
// sources/counter.move
module my_package::counter {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
 
    public struct Counter has key, store {
        id: UID,
        value: u64,
    }
 
    public struct CounterCreated has copy, drop {
        counter_id: ID,
        initial_value: u64,
    }
 
    public fun create_counter(ctx: &mut TxContext): Counter {
        let counter = Counter {
            id: object::new(ctx),
            value: 0,
        };
        
        sui::event::emit(CounterCreated {
            counter_id: object::id(&counter),
            initial_value: 0,
        });
        
        counter
    }
 
    public entry fun increment(counter: &mut Counter) {
        counter.value = counter.value + 1;
    }
 
    public fun get_value(counter: &Counter): u64 {
        counter.value
    }
 
    public entry fun set_value(counter: &mut Counter, new_value: u64) {
        counter.value = new_value;
    }
}

Move.toml Configuration

toml
[package]
name = "my_package"
version = "1.0.0"
 
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" }
 
[addresses]
my_package = "0x0"

TypeScript Integration Patterns

Contract Interaction Utilities

typescript
// utils/contract.ts
import { Transaction } from '@mysten/sui/transactions';
import { SuiClient } from '@mysten/sui/client';
 
export class CounterContract {
  constructor(
    private client: SuiClient,
    private packageId: string
  ) {}
 
  // Create a new counter
  createCounter(): Transaction {
    const tx = new Transaction();
    
    const [counter] = tx.moveCall({
      target: `${this.packageId}::counter::create_counter`,
    });
    
    // Transfer the counter to the user
    tx.transferObjects([counter], tx.pure.address('{{user_address}}'));
    
    return tx;
  }
 
  // Increment counter value
  incrementCounter(counterId: string): Transaction {
    const tx = new Transaction();
    
    tx.moveCall({
      target: `${this.packageId}::counter::increment`,
      arguments: [tx.object(counterId)],
    });
    
    return tx;
  }
 
  // Set counter to specific value
  setCounterValue(counterId: string, value: number): Transaction {
    const tx = new Transaction();
    
    tx.moveCall({
      target: `${this.packageId}::counter::set_value`,
      arguments: [
        tx.object(counterId),
        tx.pure.u64(value),
      ],
    });
    
    return tx;
  }
 
  // Read counter value (view function)
  async getCounterValue(counterId: string): Promise<number> {
    try {
      const result = await this.client.getObject({
        id: counterId,
        options: { showContent: true },
      });
 
      if (result.data?.content?.dataType === 'moveObject') {
        const fields = result.data.content.fields as any;
        return Number(fields.value);
      }
      
      throw new Error('Counter object not found or invalid');
    } catch (error) {
      console.error('Error reading counter value:', error);
      throw error;
    }
  }
}

React Hook for Contract Interaction

typescript
// hooks/useCounter.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useSignAndExecuteTransaction, useSuiClient, useCurrentAccount } from '@mysten/dapp-kit';
import { CounterContract } from '../utils/contract';
 
const PACKAGE_ID = '0x...'; // Your deployed package ID
 
export function useCounter(counterId?: string) {
  const client = useSuiClient();
  const account = useCurrentAccount();
  const queryClient = useQueryClient();
  const { mutate: signAndExecute } = useSignAndExecuteTransaction();
 
  const contract = new CounterContract(client, PACKAGE_ID);
 
  // Query counter value
  const {
    data: counterValue,
    isLoading,
    error
  } = useQuery({
    queryKey: ['counter', counterId],
    queryFn: () => contract.getCounterValue(counterId!),
    enabled: !!counterId,
    refetchInterval: 5000, // Refresh every 5 seconds
  });
 
  // Create counter mutation
  const createCounter = useMutation({
    mutationFn: async () => {
      if (!account) throw new Error('Wallet not connected');
      
      const tx = contract.createCounter();
      
      return new Promise((resolve, reject) => {
        signAndExecute(
          { 
            transaction: tx.replaceTemplateValue('{{user_address}}', account.address) 
          },
          {
            onSuccess: (result) => {
              queryClient.invalidateQueries({ queryKey: ['counters'] });
              resolve(result);
            },
            onError: reject,
          }
        );
      });
    },
  });
 
  // Increment mutation
  const incrementCounter = useMutation({
    mutationFn: async () => {
      if (!counterId) throw new Error('No counter ID provided');
      
      const tx = contract.incrementCounter(counterId);
      
      return new Promise((resolve, reject) => {
        signAndExecute(
          { transaction: tx },
          {
            onSuccess: (result) => {
              queryClient.invalidateQueries({ queryKey: ['counter', counterId] });
              resolve(result);
            },
            onError: reject,
          }
        );
      });
    },
  });
 
  // Set value mutation
  const setCounterValue = useMutation({
    mutationFn: async (value: number) => {
      if (!counterId) throw new Error('No counter ID provided');
      
      const tx = contract.setCounterValue(counterId, value);
      
      return new Promise((resolve, reject) => {
        signAndExecute(
          { transaction: tx },
          {
            onSuccess: (result) => {
              queryClient.invalidateQueries({ queryKey: ['counter', counterId] });
              resolve(result);
            },
            onError: reject,
          }
        );
      });
    },
  });
 
  return {
    counterValue,
    isLoading,
    error,
    createCounter,
    incrementCounter,
    setCounterValue,
  };
}

Counter Component Implementation

typescript
// components/Counter.tsx
import React, { useState } from 'react';
import { useCounter } from '../hooks/useCounter';
 
interface CounterProps {
  counterId?: string;
}
 
export function Counter({ counterId }: CounterProps) {
  const [newValue, setNewValue] = useState<number>(0);
  const {
    counterValue,
    isLoading,
    error,
    createCounter,
    incrementCounter,
    setCounterValue,
  } = useCounter(counterId);
 
  if (!counterId) {
    return (
      <div className="counter-container">
        <h3>Create a Counter</h3>
        <button
          onClick={() => createCounter.mutate()}
          disabled={createCounter.isPending}
        >
          {createCounter.isPending ? 'Creating...' : 'Create Counter'}
        </button>
      </div>
    );
  }
 
  if (isLoading) return <div>Loading counter...</div>;
  if (error) return <div>Error: {error.message}</div>;
 
  return (
    <div className="counter-container">
      <h3>Counter: {counterId.slice(0, 8)}...</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>
    </div>
  );
}

Advanced Patterns

Complex Move Calls with Multiple Arguments

move
// sources/marketplace.move
module my_package::marketplace {
    use sui::coin::{Self, Coin};
    use sui::sui::SUI;
    use sui::transfer;
 
    public struct Listing has key, store {
        id: UID,
        item: ID,
        price: u64,
        seller: address,
    }
 
    public entry fun create_listing(
        item_id: ID,
        price: u64,
        ctx: &mut TxContext
    ) {
        let listing = Listing {
            id: object::new(ctx),
            item: item_id,
            price,
            seller: tx_context::sender(ctx),
        };
        
        transfer::share_object(listing);
    }
 
    public entry fun purchase_item(
        listing: &mut Listing,
        payment: Coin<SUI>,
        ctx: &mut TxContext
    ) {
        assert!(coin::value(&payment) >= listing.price, 0);
        
        // Transfer payment to seller
        transfer::public_transfer(payment, listing.seller);
        
        // Additional marketplace logic...
    }
}
typescript
// TypeScript integration for marketplace
export class MarketplaceContract {
  constructor(private client: SuiClient, private packageId: string) {}
 
  createListing(itemId: string, priceInSui: number): Transaction {
    const tx = new Transaction();
    
    tx.moveCall({
      target: `${this.packageId}::marketplace::create_listing`,
      arguments: [
        tx.pure.id(itemId),
        tx.pure.u64(priceInSui * 1_000_000_000), // Convert SUI to MIST
      ],
    });
    
    return tx;
  }
 
  purchaseItem(listingId: string, priceInSui: number): Transaction {
    const tx = new Transaction();
    
    // Split SUI for payment
    const [payment] = tx.splitCoins(tx.gas, [priceInSui * 1_000_000_000]);
    
    tx.moveCall({
      target: `${this.packageId}::marketplace::purchase_item`,
      arguments: [
        tx.object(listingId),
        payment,
      ],
    });
    
    return tx;
  }
}

Event Listening and Processing

typescript
// utils/eventListener.ts
import { SuiClient } from '@mysten/sui/client';
import { SuiEvent } from '@mysten/sui/client';
 
export class ContractEventListener {
  constructor(
    private client: SuiClient,
    private packageId: string
  ) {}
 
  async subscribeToCounterEvents(callback: (event: SuiEvent) => void) {
    try {
      // Query recent events
      const events = await this.client.queryEvents({
        query: {
          MoveModule: {
            package: this.packageId,
            module: 'counter',
          },
        },
        limit: 50,
        order: 'descending',
      });
 
      // Process existing events
      events.data.forEach(callback);
 
      // Set up polling for new events
      return setInterval(async () => {
        const newEvents = await this.client.queryEvents({
          query: {
            MoveModule: {
              package: this.packageId,
              module: 'counter',
            },
          },
          limit: 10,
          order: 'descending',
        });
 
        newEvents.data.forEach(callback);
      }, 5000);
    } catch (error) {
      console.error('Error setting up event subscription:', error);
      throw error;
    }
  }
 
  async getCounterCreatedEvents(): Promise<SuiEvent[]> {
    const events = await this.client.queryEvents({
      query: {
        MoveEventType: `${this.packageId}::counter::CounterCreated`,
      },
      limit: 100,
      order: 'descending',
    });
 
    return events.data;
  }
}

React Hook for Event Listening

typescript
// hooks/useContractEvents.ts
import { useEffect, useState } from 'react';
import { useSuiClient } from '@mysten/dapp-kit';
import { SuiEvent } from '@mysten/sui/client';
import { ContractEventListener } from '../utils/eventListener';
 
export function useContractEvents(packageId: string) {
  const client = useSuiClient();
  const [events, setEvents] = useState<SuiEvent[]>([]);
  const [isListening, setIsListening] = useState(false);
 
  useEffect(() => {
    let intervalId: NodeJS.Timeout;
    
    const startListening = async () => {
      setIsListening(true);
      const eventListener = new ContractEventListener(client, packageId);
      
      try {
        intervalId = await eventListener.subscribeToCounterEvents((event) => {
          setEvents(prev => {
            // Avoid duplicates
            const exists = prev.some(e => e.id.txDigest === event.id.txDigest);
            if (!exists) {
              return [event, ...prev].slice(0, 100); // Keep latest 100 events
            }
            return prev;
          });
        });
      } catch (error) {
        console.error('Failed to start event listening:', error);
        setIsListening(false);
      }
    };
 
    startListening();
 
    return () => {
      if (intervalId) {
        clearInterval(intervalId);
      }
      setIsListening(false);
    };
  }, [client, packageId]);
 
  return { events, isListening };
}

Testing Strategies

Unit Tests for Contract Utilities

typescript
// __tests__/contract.test.ts
import { describe, it, expect, beforeEach } from '@jest/globals';
import { SuiClient } from '@mysten/sui/client';
import { CounterContract } from '../utils/contract';
 
describe('CounterContract', () => {
  let client: SuiClient;
  let contract: CounterContract;
 
  beforeEach(() => {
    client = new SuiClient({ url: 'http://localhost:9000' });
    contract = new CounterContract(client, '0x123');
  });
 
  it('should create counter transaction', () => {
    const tx = contract.createCounter();
    expect(tx).toBeDefined();
    // Add more specific transaction validation
  });
 
  it('should create increment transaction', () => {
    const counterId = '0xabc123';
    const tx = contract.incrementCounter(counterId);
    expect(tx).toBeDefined();
  });
});

Integration Tests with Local Network

typescript
// __tests__/integration.test.ts
import { describe, it, expect, beforeAll } from '@jest/globals';
import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { CounterContract } from '../utils/contract';
 
describe('Counter Integration Tests', () => {
  let client: SuiClient;
  let keypair: Ed25519Keypair;
  let contract: CounterContract;
  let packageId: string;
 
  beforeAll(async () => {
    // Setup local network client
    client = new SuiClient({ url: getFullnodeUrl('localnet') });
    keypair = new Ed25519Keypair();
    
    // Deploy contract and get package ID
    packageId = await deployContract();
    contract = new CounterContract(client, packageId);
  });
 
  it('should create and increment counter', async () => {
    // Create counter
    const createTx = contract.createCounter();
    const createResult = await client.signAndExecuteTransaction({
      transaction: createTx,
      signer: keypair,
    });
 
    expect(createResult.effects?.status?.status).toBe('success');
 
    // Extract counter ID from created objects
    const counterId = createResult.effects?.created?.[0]?.reference?.objectId;
    expect(counterId).toBeDefined();
 
    // Increment counter
    const incrementTx = contract.incrementCounter(counterId!);
    const incrementResult = await client.signAndExecuteTransaction({
      transaction: incrementTx,
      signer: keypair,
    });
 
    expect(incrementResult.effects?.status?.status).toBe('success');
 
    // Verify counter value
    const value = await contract.getCounterValue(counterId!);
    expect(value).toBe(1);
  });
});
 
async function deployContract(): Promise<string> {
  // Implementation for deploying contract in test environment
  // This would involve publishing the Move package
  return '0x...'; // Return actual package ID
}

Mock 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 { 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>;
 
describe('Counter Component', () => {
  let queryClient: QueryClient;
 
  beforeEach(() => {
    queryClient = new QueryClient({
      defaultOptions: {
        queries: { retry: false },
        mutations: { retry: false },
      },
    });
  });
 
  it('should display counter value', async () => {
    mockUseCounter.mockReturnValue({
      counterValue: 42,
      isLoading: false,
      error: null,
      createCounter: { mutate: jest.fn(), isPending: false },
      incrementCounter: { mutate: jest.fn(), isPending: false },
      setCounterValue: { mutate: jest.fn(), isPending: false },
    });
 
    render(
      <QueryClientProvider client={queryClient}>
        <Counter counterId="0x123" />
      </QueryClientProvider>
    );
 
    await waitFor(() => {
      expect(screen.getByText('42')).toBeInTheDocument();
    });
  });
 
  it('should call increment when button is clicked', async () => {
    const mockIncrement = jest.fn();
    mockUseCounter.mockReturnValue({
      counterValue: 0,
      isLoading: false,
      error: null,
      createCounter: { mutate: jest.fn(), isPending: false },
      incrementCounter: { mutate: mockIncrement, isPending: false },
      setCounterValue: { mutate: jest.fn(), isPending: false },
    });
 
    render(
      <QueryClientProvider client={queryClient}>
        <Counter counterId="0x123" />
      </QueryClientProvider>
    );
 
    fireEvent.click(screen.getByText('+1'));
    expect(mockIncrement).toHaveBeenCalled();
  });
});

Deployment and Production Considerations

Environment-Specific Configuration

typescript
// config/contracts.ts
export const getContractConfig = () => {
  const environment = process.env.NODE_ENV;
  
  switch (environment) {
    case 'production':
      return {
        packageId: process.env.NEXT_PUBLIC_COUNTER_PACKAGE_MAINNET!,
        network: 'mainnet',
      };
    case 'staging':
      return {
        packageId: process.env.NEXT_PUBLIC_COUNTER_PACKAGE_TESTNET!,
        network: 'testnet',
      };
    default:
      return {
        packageId: process.env.NEXT_PUBLIC_COUNTER_PACKAGE_DEVNET!,
        network: 'devnet',
      };
  }
};

Error Handling and Monitoring

typescript
// utils/errorHandling.ts
import { SuiTransactionBlockResponse } from '@mysten/sui/client';
 
export function handleTransactionResult(result: SuiTransactionBlockResponse) {
  if (result.effects?.status?.status === 'success') {
    console.log('Transaction successful:', result.digest);
    return result;
  } else {
    const error = result.effects?.status?.error;
    console.error('Transaction failed:', error);
    
    // Log to monitoring service
    if (process.env.NODE_ENV === 'production') {
      // logToMonitoringService('transaction_error', { error, digest: result.digest });
    }
    
    throw new Error(`Transaction failed: ${error}`);
  }
}

Best Practices Summary

  1. Type Safety: Use TypeScript for all contract interactions
  2. Error Handling: Implement comprehensive error handling for all Move calls
  3. Testing: Write both unit and integration tests for contract logic
  4. Event Monitoring: Set up proper event listening for real-time updates
  5. Performance: Use React Query for efficient data fetching and caching
  6. Security: Validate all inputs before sending to smart contracts
  7. Documentation: Document all contract interfaces and expected behaviors

Next Steps

  • Implement performance optimization techniques for advanced patterns
  • Learn about Multi-Chain dApp Architecture for cross-chain applications
  • Follow security best practices for production deployment

Resources

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