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 October 5, 2026
On this page

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​

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