Blockchain Interaction
Patterns and examples for interacting with Move contracts from the frontend.
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:
useMoveContract: Base hook providing low-level SDK access- Contract-specific hooks: Higher-level hooks for each contract module
- 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
- Use View Functions: Always prefer view functions over fetching resources directly when available
- Handle Errors: Wrap blockchain calls in try-catch blocks and provide fallback values
- Type Conversion: Convert u64 values from strings to numbers using
parseInt() - Caching: Use TanStack Query to cache blockchain data and reduce redundant calls
- Optimistic Updates: Update UI immediately, then sync with blockchain in the background
