Blockchain Interaction

Patterns and examples for interacting with Move contracts from the frontend.

Created: 1/11/2026

Our applications interact with Move contracts through custom React hooks that abstract the complexity of blockchain operations. These hooks provide a clean API for both reading data and submitting transactions.

Architecture Overview

The interaction layer consists of:

  1. useMoveContract: Base hook providing low-level SDK access
  2. Contract-specific hooks: Higher-level hooks for each contract module
  3. TanStack Query: Caching and state management for blockchain data

Reading Blockchain Data

Fetching All Gigs

From apps/native/src/hooks/use-gig.ts:

const getAllGigs = async (): Promise<(Gig & { id: string })[]> => {
  try {
    // Use view function to fetch paginated gigs
    const result = await viewFunction(
      GigContract.GET_GIGS_PAGINATED_FUNCTION,
      [],
      ["0", "50"] // Start, Limit (Strings for u64)
    ) as any[];

    if (!result || !result[0]) return [];

    // Map the view struct to our Gig interface
    return result[0].map((item: any) => ({
      poster: item.poster,
      applicant: item.applicant,
      description: item.description,
      budget: parseInt(item.budget),
      state: item.state,
      evidence_hash: item.evidence_hash,
      poster_rated: item.poster_rated,
      applicant_rated: item.applicant_rated,
      contract_ipfs_hash: item.contract_ipfs_hash,
      contract_signatures: item.contract_signatures,
      signed_applicants: item.signed_applicants,
      is_advertised: item.is_advertised,
      advertisement_image: item.advertisement_image,
      id: item.gig_address // The view struct includes the gig_address
    }));
  } catch (error) {
    console.error("Failed to fetch all gigs from contract:", error);
    return [];
  }
};

Fetching User Ratings

From apps/native/src/hooks/use-gig.ts:

const getUserRating = async (userAddress: string): Promise<number> => {
  try {
    const result = await viewFunction(
      RatingModule.GET_AVERAGE_FUNCTION,
      [],
      [userAddress]
    );

    if (!result || result.length === 0) return 0;

    // Result is u64 (scaled x10)
    const averageScaled = typeof result[0] === 'string' 
      ? parseInt(result[0]) 
      : result[0];

    return averageScaled / 10;
  } catch (e: any) {
    console.error("Failed to fetch user rating", e);
    return 0;
  }
};

Admin Dashboard Data Fetching

From apps/admin/hooks/use-admin-data.ts with TanStack Query:

export function useAdminData() {
  const { viewFunction } = useAdminMovement();

  const fetchGigsFromIndexer = async (): Promise<Gig[]> => {
    try {
      const result = await viewFunction(
        GigContract.GET_GIGS_PAGINATED_FUNCTION,
        [],
        ["0", "50"]
      ) as any[];

      if (!result || !result[0]) return [];

      return result[0].map((item: any) => ({
        id: item.gig_address,
        poster: item.poster,
        applicant: item.applicant,
        description: item.description,
        budget: parseInt(item.budget),
        state: item.state,
        // ... other fields
      }));
    } catch (error) {
      console.error("Failed to fetch gigs from contract:", error);
      return [];
    }
  };

  // Use TanStack Query for caching
  const gigsQuery = useQuery({
    queryKey: ['admin', 'gigs'],
    queryFn: fetchGigsFromIndexer,
  });

  return { gigsQuery };
}

Writing to the Blockchain

Creating a Standard Gig

From apps/native/src/hooks/use-gig.ts:

const createGig = async (
  description: string, 
  budget: number, 
  contractIpfsHash: string
) => {
  return await submitTransaction(
    GigContract.CREATE_GIG_FUNCTION,
    [description, budget, contractIpfsHash]
  );
};

// Usage in a component
const { createGig } = useGig();

await createGig(
  "Build a website",
  1000,
  "ipfs://QmHash..."
);

Creating a Location-Based Gig

From apps/native/src/hooks/use-location-based-gig.ts:

const createLocationGig = async (
  description: string,
  totalAmount: number,
  maxOpportunities: number,
  lat: number,
  long: number
) => {
  return await submitTransaction(
    LocationBasedGig.CREATE_LOCATION_GIG_FUNCTION,
    [
      description,
      totalAmount.toString(),
      maxOpportunities.toString(),
      lat.toString(),
      long.toString()
    ]
  );
};

// Usage
const { createLocationGig } = useLocationBasedGig();

await createLocationGig(
  "Deliver package",
  500,
  10,
  -33.8688,
  151.2093
);

Applying to a Gig

const applyBySigning = async (gigAddress: string) => {
  return await submitTransaction(
    GigContract.APPLY_BY_SIGNING_FUNCTION,
    [gigAddress]
  );
};

Rating a Counterparty

const rateCounterparty = async (
  gigAddress: string, 
  rating: number, 
  commentHash: string
) => {
  return await submitTransaction(
    GigContract.RATE_COUNTERPARTY_FUNCTION,
    [gigAddress, rating, commentHash]
  );
};

Arbiter Management (Dispute Module)

For the dispute_module, the Arbiter plays a critical role in resolving conflicts. By default, the contract deployer is the arbiter.

1. Check Current Arbiter

To verify the current arbiter address, query the DisputeConfig resource:

movement account list \
  --account <CONTRACT_ADDRESS> \
  --query resources

Look for ...::dispute_module::DisputeConfig.

2. Assign New Arbiter

To transfer arbiter rights to a new address (e.g., an address created via Privy for the Admin Panel), the current arbiter (or deployer) must execute:

movement move run \
  --function-id <CONTRACT_ADDRESS>::dispute_module::assign_new_arbiter \
  --args address:<NEW_ARBITER_ADDRESS> \
  --profile default

3. Verify Update

Run the check command again to confirm the arbiter field in DisputeConfig has been updated.

Best Practices

  1. Use View Functions: Always prefer view functions over fetching resources directly when available
  2. Handle Errors: Wrap blockchain calls in try-catch blocks and provide fallback values
  3. Type Conversion: Convert u64 values from strings to numbers using parseInt()
  4. Caching: Use TanStack Query to cache blockchain data and reduce redundant calls
  5. Optimistic Updates: Update UI immediately, then sync with blockchain in the background