How to Integrate the FootballBets Contract with a Next.js Frontend
Integrate the FootballBets smart contract with a Next.js frontend by deploying the Python contract to GenLayer, exposing the address via NEXT_PUBLIC_CONTRACT_ADDRESS, and consuming it through strongly-typed React hooks that wrap the genlayer-js client.
The genlayerlabs/genlayer-project-boilerplate repository provides a complete reference implementation demonstrating how to bridge GenLayer's Python smart contracts with modern React applications. This guide walks through the exact architecture used to connect the FootballBets contract to a Next.js 19 interface, covering everything from environment configuration to transaction management.
Deploy the Contract
Before integrating the frontend, you must deploy the FootballBets contract to the GenLayer network. Run the deployment command from the project root:
genlayer deploy
After deployment completes, copy the emitted contract address. You will need this value to configure the frontend environment.
Configure Environment Variables
Expose the deployed contract address to the Next.js application by adding it to frontend/.env.example (and your actual .env.local):
NEXT_PUBLIC_CONTRACT_ADDRESS=0x1234567890abcdef...
The frontend reads this variable through getContractAddress() defined in frontend/lib/genlayer/client.ts. This helper validates that the address is configured and returns it for client initialization.
Create the GenLayer Client Wrapper
The file frontend/lib/contracts/FootballBets.ts provides a TypeScript abstraction over the raw genlayer-js client. The constructor accepts three parameters: the contract address, the user’s wallet address, and an optional Studio URL. It initializes the client using createClient() with the studionet chain configuration:
// frontend/lib/contracts/FootballBets.ts
import { createClient, studionet } from 'genlayer-js';
export class FootballBets {
private client;
constructor(
contractAddress: string,
account: `0x${string}`,
studioUrl?: string
) {
this.client = createClient({
chain: studionet,
account,
endpoint: studioUrl
});
}
// Data conversion methods handle GenLayer Map → plain arrays
async getBets() {
const result = await this.client.readContract({
address: this.contractAddress,
functionName: 'get_bets'
});
return Array.from(result.values()); // Convert Map to array
}
async createBet(params: { gameDate: string; team1: string; team2: string; predictedWinner: string }) {
// Implementation details below
}
}
Build React Query Hooks
All contract interactions flow through frontend/lib/hooks/useFootballBets.ts. This file exports useFootballBetsContract() to manage the client instance, alongside specialized hooks for reading and writing data.
Instantiate the Contract Client
The useFootballBetsContract() hook creates a singleton FootballBets instance whenever the wallet address or configured contract address changes. It validates that NEXT_PUBLIC_CONTRACT_ADDRESS is set and displays a toast notification if configuration is missing.
Reading Contract State
The hooks useBets(), usePlayerPoints(), and useLeaderboard() wrap the contract’s view methods (get_bets, get_player_points, and get_points). They utilize React Query to keep the UI synchronized and automatically refetch after mutations invalidate the cache:
useBets()→ Callscontract.getBets()returning active betting marketsusePlayerPoints()→ Callscontract.getPlayerPoints(address)for individual scoresuseLeaderboard()→ Callscontract.getLeaderboard()for global rankings
Executing Write Operations
For state-changing operations, useCreateBet() and useResolveBet() handle the full transaction lifecycle:
- Estimate gas fees using
estimateWriteFeePreset - Send the transaction via
client.writeContract - Wait for the transaction receipt
- Invalidate relevant React Query caches to trigger UI updates
Both hooks accept the necessary parameters (bet details for creation, bet ID for resolution) and return loading states for UI feedback.
Implement Frontend Components
With the hooks in place, UI components remain purely presentational. Here are the implementation patterns used in the boilerplate:
Display the Leaderboard
// frontend/components/Leaderboard.tsx
import { useLeaderboard } from "@/lib/hooks/useFootballBets";
export default function Leaderboard() {
const { data: leaderboard = [], isLoading } = useLeaderboard();
if (isLoading) return <div>Loading…</div>;
return (
<ul>
{leaderboard.map(({ address, points }) => (
<li key={address}>
{address.slice(0, 6)}… – {points} pts
</li>
))}
</ul>
);
}
Create a New Bet
// frontend/components/CreateBetModal.tsx
import { useCreateBet } from "@/lib/hooks/useFootballBets";
export default function NewBetForm() {
const { createBet, isLoading } = useCreateBet();
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const gameDate = form.gameDate.value;
const team1 = form.team1.value;
const team2 = form.team2.value;
const predictedWinner = form.predictedWinner.value;
await createBet({ gameDate, team1, team2, predictedWinner });
};
return (
<form onSubmit={onSubmit}>
{/* inputs for gameDate, team1, team2, predictedWinner */}
<button type="submit" disabled={isLoading}>
{isLoading ? "Creating…" : "Create Bet"}
</button>
</form>
);
}
Resolve an Existing Bet
// frontend/components/BetsTable.tsx
import { useResolveBet } from "@/lib/hooks/useFootballBets";
export function ResolveButton({ betId }: { betId: string }) {
const { resolveBet, isLoading } = useResolveBet();
return (
<button onClick={() => resolveBet(betId)} disabled={isLoading}>
{isLoading ? "Resolving…" : "Resolve"}
</button>
);
}
Summary
- Deploy the
contracts/football_bets.pycontract usinggenlayer deployto obtain the contract address - Configure
NEXT_PUBLIC_CONTRACT_ADDRESSinfrontend/.env.examplesogetContractAddress()can read it - Wrap the
genlayer-jsclient infrontend/lib/contracts/FootballBets.tsto handle type conversions and method calls - Hook into the contract with
useFootballBetsContract()and specialized React Query hooks for reads (useBets,useLeaderboard) and writes (useCreateBet,useResolveBet) - Render using presentational components that consume the hooks, maintaining separation between blockchain logic and UI
Frequently Asked Questions
What is the FootballBets contract?
The FootballBets contract is a Python-based smart contract located at contracts/football_bets.py in the repository. It implements on-chain betting logic including creating bets, resolving markets, and tracking player points on the GenLayer network.
How do I handle the contract address in Next.js?
Store the deployed address in the NEXT_PUBLIC_CONTRACT_ADDRESS environment variable. The frontend/lib/genlayer/client.ts file provides getContractAddress() to safely retrieve this value at runtime, ensuring the application fails gracefully with a toast notification if the variable is undefined.
How are transactions handled in the frontend?
Write operations follow a three-step pattern implemented in useCreateBet() and useResolveBet(): first calling estimateWriteFeePreset to calculate gas costs, then executing client.writeContract to submit the transaction, and finally waiting for the receipt before invalidating React Query caches to refresh the UI state.
What dependencies are required for this integration?
The integration requires genlayer-js for blockchain connectivity, React Query (@tanstack/react-query) for server-state management, and a Next.js 19 environment with TypeScript. The FootballBets.ts wrapper handles all data structure conversions between GenLayer's native Map types and JavaScript arrays.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →