-
-
-
-
- {points}
- pts
-
+
+ {/* Show points when connected */}
+ {address && (
+
+ {points}
+ pts
-
-
-
-
-
-
-
-
-
- Wallet Details
-
-
- Your connected MetaMask wallet information
-
-
-
-
-
-
Your Address
-
{address}
-
-
-
-
Your Points
-
{points}
-
-
-
-
Network Status
-
-
-
- {isOnCorrectNetwork
- ? "Connected to GenLayer"
- : "Wrong Network"}
-
-
-
-
- {!isOnCorrectNetwork && (
-
-
- Network Warning
-
- You're not on the GenLayer network. Please switch networks in
- MetaMask or try reconnecting.
-
-
- )}
-
- {connectionError && (
-
-
- Error
- {connectionError}
-
- )}
-
-
-
-
-
-
-
+ {(() => {
+ if (!connected) {
+ return (
+
+ );
+ }
+
+ if (chain.unsupported) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+ })()}
+
+ );
+ }}
+
+
);
}
diff --git a/frontend/components/BetsTable.tsx b/frontend/components/BetsTable.tsx
index d588f12..068297c 100644
--- a/frontend/components/BetsTable.tsx
+++ b/frontend/components/BetsTable.tsx
@@ -3,7 +3,6 @@
import { Loader2, Trophy, Clock, AlertCircle } from "lucide-react";
import { useBets, useResolveBet, useFootballBetsContract } from "@/lib/hooks/useFootballBets";
import { useWallet } from "@/lib/genlayer/wallet";
-import { error } from "@/lib/utils/toast";
import { AddressDisplay } from "./AddressDisplay";
import { Button } from "./ui/button";
import { Badge } from "./ui/badge";
@@ -12,16 +11,15 @@ import type { Bet } from "@/lib/contracts/types";
export function BetsTable() {
const contract = useFootballBetsContract();
const { data: bets, isLoading, isError } = useBets();
- const { address, isConnected, isLoading: isWalletLoading } = useWallet();
+ const { address, isConnected, isLoading: isWalletLoading, connectWallet } = useWallet();
const { resolveBet, isResolving, resolvingBetId } = useResolveBet();
const handleResolve = (betId: string) => {
if (!address) {
- error("Please connect your wallet to resolve bets");
+ connectWallet();
return;
}
- // Confirmation popup
const confirmed = confirm("Are you sure you want to resolve this bet? This action will determine the winner.");
if (confirmed) {
diff --git a/frontend/components/CreateBetModal.tsx b/frontend/components/CreateBetModal.tsx
index dd7e337..2013ca4 100644
--- a/frontend/components/CreateBetModal.tsx
+++ b/frontend/components/CreateBetModal.tsx
@@ -4,14 +4,14 @@ import { useState, useEffect } from "react";
import { Plus, Loader2, Calendar, Users } from "lucide-react";
import { useCreateBet } from "@/lib/hooks/useFootballBets";
import { useWallet } from "@/lib/genlayer/wallet";
-import { success, error } from "@/lib/utils/toast";
+import { error } from "@/lib/utils/toast";
import { Button } from "./ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
export function CreateBetModal() {
- const { isConnected, address, isLoading } = useWallet();
+ const { isConnected, address, isLoading, connectWallet } = useWallet();
const { createBet, isCreating, isSuccess } = useCreateBet();
const [isOpen, setIsOpen] = useState(false);
@@ -67,7 +67,7 @@ export function CreateBetModal() {
e.preventDefault();
if (!isConnected || !address) {
- error("Please connect your wallet first");
+ connectWallet();
return;
}
@@ -79,7 +79,7 @@ export function CreateBetModal() {
gameDate,
team1,
team2,
- predictedWinner: predictedWinner, // Send "1", "2", or "0" directly
+ predictedWinner: predictedWinner,
});
};
diff --git a/frontend/lib/genlayer/WalletProvider.tsx b/frontend/lib/genlayer/WalletProvider.tsx
index 44dae0a..919de0b 100644
--- a/frontend/lib/genlayer/WalletProvider.tsx
+++ b/frontend/lib/genlayer/WalletProvider.tsx
@@ -1,312 +1,70 @@
"use client";
-import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from "react";
-import {
- isMetaMaskInstalled,
- connectMetaMask,
- switchAccount,
- getAccounts,
- getCurrentChainId,
- isOnGenLayerNetwork,
- getEthereumProvider,
- GENLAYER_CHAIN_ID,
-} from "./client";
-import { error, userRejected, warning } from "../utils/toast";
-
-// localStorage key for tracking user's disconnect intent
-const DISCONNECT_FLAG = "wallet_disconnected";
+import React, { createContext, useContext, ReactNode } from "react";
+import { useAccount, useChainId, useDisconnect } from "wagmi";
+import { useConnectModal, useAccountModal } from "@rainbow-me/rainbowkit";
+import { GENLAYER_CHAIN_ID } from "@/lib/wagmi/config";
export interface WalletState {
address: string | null;
- chainId: string | null;
+ chainId: number | null;
isConnected: boolean;
isLoading: boolean;
- isMetaMaskInstalled: boolean;
isOnCorrectNetwork: boolean;
}
interface WalletContextValue extends WalletState {
- connectWallet: () => Promise
;
+ connectWallet: () => void;
disconnectWallet: () => void;
- switchWalletAccount: () => Promise;
+ openAccountModal: () => void;
}
-// Create context with undefined default (will error if used outside Provider)
const WalletContext = createContext(undefined);
/**
- * WalletProvider component that manages wallet state and provides it to all children
- * This ensures all components share the same wallet state and react to changes
+ * WalletProvider component that wraps wagmi/RainbowKit hooks
+ * Provides a simplified interface for wallet interactions
*/
export function WalletProvider({ children }: { children: ReactNode }) {
- const [state, setState] = useState({
- address: null,
- chainId: null,
- isConnected: false,
- isLoading: true,
- isMetaMaskInstalled: false,
- isOnCorrectNetwork: false,
- });
-
- // Check MetaMask installation and load account on mount
- useEffect(() => {
- const initWallet = async () => {
- const installed = isMetaMaskInstalled();
-
- if (!installed) {
- setState({
- address: null,
- chainId: null,
- isConnected: false,
- isLoading: false,
- isMetaMaskInstalled: false,
- isOnCorrectNetwork: false,
- });
- return;
- }
-
- // Check if user intentionally disconnected
- // If they did, don't auto-reconnect even if MetaMask has permissions
- if (typeof window !== "undefined") {
- const wasDisconnected =
- localStorage.getItem(DISCONNECT_FLAG) === "true";
-
- if (wasDisconnected) {
- // User explicitly disconnected, don't auto-reconnect
- setState({
- address: null,
- chainId: null,
- isConnected: false,
- isLoading: false,
- isMetaMaskInstalled: true,
- isOnCorrectNetwork: false,
- });
- return;
- }
- }
-
- try {
- // Get current accounts (without requesting)
- // This will auto-reconnect if MetaMask has existing permissions
- // and user didn't explicitly disconnect
- const accounts = await getAccounts();
- const chainId = await getCurrentChainId();
- const correctNetwork = await isOnGenLayerNetwork();
-
- setState({
- address: accounts[0] || null,
- chainId,
- isConnected: accounts.length > 0,
- isLoading: false,
- isMetaMaskInstalled: true,
- isOnCorrectNetwork: correctNetwork,
- });
- } catch (error) {
- console.error("Error initializing wallet:", error);
- setState({
- address: null,
- chainId: null,
- isConnected: false,
- isLoading: false,
- isMetaMaskInstalled: true,
- isOnCorrectNetwork: false,
- });
- }
- };
-
- initWallet();
- }, []);
-
- // Set up MetaMask event listeners (ONCE for entire app)
- useEffect(() => {
- const provider = getEthereumProvider();
-
- if (!provider) {
- return;
- }
-
- const handleAccountsChanged = async (accounts: string[]) => {
- const chainId = await getCurrentChainId();
- const correctNetwork = await isOnGenLayerNetwork();
-
- // If user connected via MetaMask UI, clear the disconnect flag
- // This allows future auto-reconnects
- if (accounts.length > 0 && typeof window !== "undefined") {
- localStorage.removeItem(DISCONNECT_FLAG);
- }
-
- setState((prev) => ({
- ...prev,
- address: accounts[0] || null,
- chainId,
- isConnected: accounts.length > 0,
- isOnCorrectNetwork: correctNetwork,
- }));
- };
-
- const handleChainChanged = async (chainId: string) => {
- // MetaMask recommends reloading the page on chain change
- // but we'll update state instead for better UX
- const correctNetwork = parseInt(chainId, 16) === GENLAYER_CHAIN_ID;
- const accounts = await getAccounts();
-
- setState((prev) => ({
- ...prev,
- chainId,
- address: accounts[0] || null,
- isConnected: accounts.length > 0,
- isOnCorrectNetwork: correctNetwork,
- }));
- };
-
- const handleDisconnect = () => {
- setState((prev) => ({
- ...prev,
- address: null,
- isConnected: false,
- }));
- };
-
- // Add event listeners
- provider.on("accountsChanged", handleAccountsChanged);
- provider.on("chainChanged", handleChainChanged);
- provider.on("disconnect", handleDisconnect);
-
- // Cleanup
- return () => {
- provider.removeListener("accountsChanged", handleAccountsChanged);
- provider.removeListener("chainChanged", handleChainChanged);
- provider.removeListener("disconnect", handleDisconnect);
- };
- }, []);
-
- /**
- * Connect to MetaMask
- */
- const connectWallet = useCallback(async () => {
- try {
- setState((prev) => ({ ...prev, isLoading: true }));
-
- const address = await connectMetaMask();
- const chainId = await getCurrentChainId();
- const correctNetwork = await isOnGenLayerNetwork();
-
- // User is connecting, clear the disconnect flag
- // This allows auto-reconnect on future page loads
- if (typeof window !== "undefined") {
- localStorage.removeItem(DISCONNECT_FLAG);
- }
-
- setState({
- address,
- chainId,
- isConnected: true,
- isLoading: false,
- isMetaMaskInstalled: true,
- isOnCorrectNetwork: correctNetwork,
- });
-
- return address;
- } catch (err: any) {
- console.error("Error connecting wallet:", err);
- setState((prev) => ({ ...prev, isLoading: false }));
-
- // Handle specific error types with appropriate toasts
- if (err.message?.includes("rejected")) {
- userRejected("Connection cancelled");
- } else if (err.message?.includes("MetaMask is not installed")) {
- error("MetaMask not found", {
- description: "Please install MetaMask to connect your wallet.",
- action: {
- label: "Install MetaMask",
- onClick: () => window.open("https://metamask.io/download/", "_blank")
- }
- });
- } else {
- error("Failed to connect wallet", {
- description: err.message || "Please check your MetaMask and try again."
- });
- }
-
- throw err;
+ const { address, isConnected, isConnecting, isReconnecting } = useAccount();
+ const chainId = useChainId();
+ const { disconnect } = useDisconnect();
+ const { openConnectModal } = useConnectModal();
+ const { openAccountModal } = useAccountModal();
+
+ const isLoading = isConnecting || isReconnecting;
+ const isOnCorrectNetwork = chainId === GENLAYER_CHAIN_ID;
+
+ const connectWallet = () => {
+ if (openConnectModal) {
+ openConnectModal();
}
- }, []);
-
- /**
- * Disconnect wallet (clear local state and persist disconnect intent)
- * Sets a flag in localStorage to prevent auto-reconnect on page refresh
- */
- const disconnectWallet = useCallback(() => {
- // Persist user's intent to disconnect
- // This prevents auto-reconnect on page refresh
- if (typeof window !== "undefined") {
- localStorage.setItem(DISCONNECT_FLAG, "true");
- }
-
- setState((prev) => ({
- ...prev,
- address: null,
- isConnected: false,
- }));
- }, []);
-
- /**
- * Request user to switch to different MetaMask account
- * Shows MetaMask account picker even if already connected
- */
- const switchWalletAccount = useCallback(async () => {
- try {
- setState((prev) => ({ ...prev, isLoading: true }));
-
- // Request account switch via MetaMask picker
- const newAddress = await switchAccount();
-
- // Get updated state
- const chainId = await getCurrentChainId();
- const correctNetwork = await isOnGenLayerNetwork();
-
- // Clear disconnect flag - user is actively connecting
- if (typeof window !== "undefined") {
- localStorage.removeItem(DISCONNECT_FLAG);
- }
-
- // Update state immediately for better UX
- // accountsChanged event will also fire, but that's okay
- setState({
- address: newAddress,
- chainId,
- isConnected: true,
- isLoading: false,
- isMetaMaskInstalled: true,
- isOnCorrectNetwork: correctNetwork,
- });
-
- return newAddress;
- } catch (err: any) {
- console.error("Error switching account:", err);
- setState((prev) => ({ ...prev, isLoading: false }));
+ };
- // Handle specific error types
- if (err.message?.includes("rejected")) {
- userRejected("Account switch cancelled");
- } else {
- error("Failed to switch account", {
- description: err.message || "Please try again."
- });
- }
+ const disconnectWallet = () => {
+ disconnect();
+ };
- throw err;
+ const handleOpenAccountModal = () => {
+ if (openAccountModal) {
+ openAccountModal();
}
- }, []);
+ };
const value: WalletContextValue = {
- ...state,
+ address: address ?? null,
+ chainId: chainId ?? null,
+ isConnected,
+ isLoading,
+ isOnCorrectNetwork,
connectWallet,
disconnectWallet,
- switchWalletAccount,
+ openAccountModal: handleOpenAccountModal,
};
- return {children};
+ return (
+ {children}
+ );
}
/**
diff --git a/frontend/lib/genlayer/client.ts b/frontend/lib/genlayer/client.ts
index c2b493f..37fb300 100644
--- a/frontend/lib/genlayer/client.ts
+++ b/frontend/lib/genlayer/client.ts
@@ -2,37 +2,9 @@
import { createClient } from "genlayer-js";
import { studionet } from "genlayer-js/chains";
-import { createWalletClient, custom, type WalletClient } from "viem";
-// GenLayer Network Configuration (from environment variables with fallbacks)
-export const GENLAYER_CHAIN_ID = parseInt(process.env.NEXT_PUBLIC_GENLAYER_CHAIN_ID || "61999");
-export const GENLAYER_CHAIN_ID_HEX = `0x${GENLAYER_CHAIN_ID.toString(16).toUpperCase()}`;
-
-export const GENLAYER_NETWORK = {
- chainId: GENLAYER_CHAIN_ID_HEX,
- chainName: process.env.NEXT_PUBLIC_GENLAYER_CHAIN_NAME || "GenLayer Studio",
- nativeCurrency: {
- name: process.env.NEXT_PUBLIC_GENLAYER_SYMBOL || "GEN",
- symbol: process.env.NEXT_PUBLIC_GENLAYER_SYMBOL || "GEN",
- decimals: 18,
- },
- rpcUrls: [process.env.NEXT_PUBLIC_GENLAYER_RPC_URL || "https://studio.genlayer.com/api"],
- blockExplorerUrls: [],
-};
-
-// Ethereum provider type from window
-interface EthereumProvider {
- isMetaMask?: boolean;
- request: (args: { method: string; params?: any[] }) => Promise;
- on: (event: string, handler: (...args: any[]) => void) => void;
- removeListener: (event: string, handler: (...args: any[]) => void) => void;
-}
-
-declare global {
- interface Window {
- ethereum?: EthereumProvider;
- }
-}
+// Re-export chain ID from wagmi config
+export { GENLAYER_CHAIN_ID } from "@/lib/wagmi/config";
/**
* Get the GenLayer RPC URL from environment variables
@@ -49,253 +21,13 @@ export function getStudioUrl(): string {
export function getContractAddress(): string {
const address = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS;
if (!address) {
- // Return empty string during build, error will be shown in UI during runtime
return "";
}
return address;
}
/**
- * Check if MetaMask is installed
- */
-export function isMetaMaskInstalled(): boolean {
- if (typeof window === "undefined") return false;
- return !!window.ethereum?.isMetaMask;
-}
-
-/**
- * Get the Ethereum provider (MetaMask)
- */
-export function getEthereumProvider(): EthereumProvider | null {
- if (typeof window === "undefined") return null;
- return window.ethereum || null;
-}
-
-/**
- * Request accounts from MetaMask
- * @returns Array of addresses
- */
-export async function requestAccounts(): Promise {
- const provider = getEthereumProvider();
-
- if (!provider) {
- throw new Error("MetaMask is not installed");
- }
-
- try {
- const accounts = await provider.request({
- method: "eth_requestAccounts",
- });
- return accounts;
- } catch (error: any) {
- if (error.code === 4001) {
- throw new Error("User rejected the connection request");
- }
- throw new Error(`Failed to connect to MetaMask: ${error.message}`);
- }
-}
-
-/**
- * Get current MetaMask accounts without requesting permission
- * @returns Array of addresses
- */
-export async function getAccounts(): Promise {
- const provider = getEthereumProvider();
-
- if (!provider) {
- return [];
- }
-
- try {
- const accounts = await provider.request({
- method: "eth_accounts",
- });
- return accounts;
- } catch (error) {
- console.error("Error getting accounts:", error);
- return [];
- }
-}
-
-/**
- * Get the current chain ID from MetaMask
- */
-export async function getCurrentChainId(): Promise {
- const provider = getEthereumProvider();
-
- if (!provider) {
- return null;
- }
-
- try {
- const chainId = await provider.request({
- method: "eth_chainId",
- });
- return chainId;
- } catch (error) {
- console.error("Error getting chain ID:", error);
- return null;
- }
-}
-
-/**
- * Add GenLayer network to MetaMask
- */
-export async function addGenLayerNetwork(): Promise {
- const provider = getEthereumProvider();
-
- if (!provider) {
- throw new Error("MetaMask is not installed");
- }
-
- try {
- await provider.request({
- method: "wallet_addEthereumChain",
- params: [GENLAYER_NETWORK],
- });
- } catch (error: any) {
- if (error.code === 4001) {
- throw new Error("User rejected adding the network");
- }
- throw new Error(`Failed to add GenLayer network: ${error.message}`);
- }
-}
-
-/**
- * Switch to GenLayer network
- */
-export async function switchToGenLayerNetwork(): Promise {
- const provider = getEthereumProvider();
-
- if (!provider) {
- throw new Error("MetaMask is not installed");
- }
-
- try {
- await provider.request({
- method: "wallet_switchEthereumChain",
- params: [{ chainId: GENLAYER_CHAIN_ID_HEX }],
- });
- } catch (error: any) {
- // If the chain is not added, add it
- if (error.code === 4902) {
- await addGenLayerNetwork();
- } else if (error.code === 4001) {
- throw new Error("User rejected switching the network");
- } else {
- throw new Error(`Failed to switch network: ${error.message}`);
- }
- }
-}
-
-/**
- * Check if we're on the GenLayer network
- */
-export async function isOnGenLayerNetwork(): Promise {
- const chainId = await getCurrentChainId();
-
- if (!chainId) {
- return false;
- }
-
- // Convert both to decimal for comparison
- const currentChainIdDecimal = parseInt(chainId, 16);
- return currentChainIdDecimal === GENLAYER_CHAIN_ID;
-}
-
-/**
- * Connect to MetaMask and ensure we're on GenLayer network
- * @returns The connected address
- */
-export async function connectMetaMask(): Promise {
- if (!isMetaMaskInstalled()) {
- throw new Error("MetaMask is not installed");
- }
-
- // Request accounts
- const accounts = await requestAccounts();
-
- if (!accounts || accounts.length === 0) {
- throw new Error("No accounts found");
- }
-
- // Check and switch to GenLayer network
- const onCorrectNetwork = await isOnGenLayerNetwork();
-
- if (!onCorrectNetwork) {
- await switchToGenLayerNetwork();
- }
-
- return accounts[0];
-}
-
-/**
- * Request user to switch MetaMask account
- * Shows MetaMask account picker even if already connected
- * Uses wallet_requestPermissions to force account selection dialog
- * @returns The newly selected account address
- */
-export async function switchAccount(): Promise {
- const provider = getEthereumProvider();
-
- if (!provider) {
- throw new Error("MetaMask is not installed");
- }
-
- try {
- // Request permissions - this shows account picker
- await provider.request({
- method: "wallet_requestPermissions",
- params: [{ eth_accounts: {} }],
- });
-
- // Get the newly selected account
- const accounts = await provider.request({
- method: "eth_accounts",
- });
-
- if (!accounts || accounts.length === 0) {
- throw new Error("No account selected");
- }
-
- return accounts[0];
- } catch (error: any) {
- if (error.code === 4001) {
- throw new Error("User rejected account switch");
- } else if (error.code === -32002) {
- throw new Error("Account switch request already pending");
- }
- throw new Error(`Failed to switch account: ${error.message}`);
- }
-}
-
-/**
- * Create a viem wallet client from MetaMask provider
- */
-export function createMetaMaskWalletClient(): WalletClient | null {
- const provider = getEthereumProvider();
-
- if (!provider) {
- return null;
- }
-
- try {
- return createWalletClient({
- chain: studionet as any,
- transport: custom(provider),
- });
- } catch (error) {
- console.error("Error creating wallet client:", error);
- return null;
- }
-}
-
-/**
- * Create a GenLayer client with MetaMask account
- *
- * Note: The genlayer-js SDK doesn't directly support custom transports like viem.
- * When an address is provided, the SDK will use the window.ethereum provider
- * automatically for transaction signing via MetaMask.
+ * Create a GenLayer client with optional account
*/
export function createGenLayerClient(address?: string) {
const config: any = {
@@ -310,18 +42,8 @@ export function createGenLayerClient(address?: string) {
return createClient(config);
} catch (error) {
console.error("Error creating GenLayer client:", error);
- // Return client without account on error
return createClient({
chain: studionet,
});
}
}
-
-/**
- * Get a client instance with MetaMask account
- */
-export async function getClient() {
- const accounts = await getAccounts();
- const address = accounts[0];
- return createGenLayerClient(address);
-}
diff --git a/frontend/lib/genlayer/wallet.ts b/frontend/lib/genlayer/wallet.ts
index 306a3d7..1b8a553 100644
--- a/frontend/lib/genlayer/wallet.ts
+++ b/frontend/lib/genlayer/wallet.ts
@@ -2,8 +2,6 @@
/**
* Re-export wallet functionality from WalletProvider
- * This maintains backward compatibility with existing imports
- * All components that import from this file will now use shared context state
*/
export { useWallet, WalletProvider } from "./WalletProvider";
export type { WalletState } from "./WalletProvider";
diff --git a/frontend/lib/hooks/useFootballBets.ts b/frontend/lib/hooks/useFootballBets.ts
index 109b079..ad3cc23 100644
--- a/frontend/lib/hooks/useFootballBets.ts
+++ b/frontend/lib/hooks/useFootballBets.ts
@@ -5,7 +5,7 @@ import { useMemo, useState } from "react";
import FootballBets from "../contracts/FootballBets";
import { getContractAddress, getStudioUrl } from "../genlayer/client";
import { useWallet } from "../genlayer/wallet";
-import { success, error, configError } from "../utils/toast";
+import { success, error } from "../utils/toast";
import type { Bet, LeaderboardEntry } from "../contracts/types";
/**
@@ -13,9 +13,6 @@ import type { Bet, LeaderboardEntry } from "../contracts/types";
*
* Returns null if contract address is not configured.
* The contract instance is recreated whenever the wallet address changes.
- * Read-only operations (getBets, getLeaderboard, etc.) work without a connected wallet.
- * Write operations (createBet, resolveBet) require a connected wallet and will fail
- * if the address is null. Defensive validation is added in the mutation hooks.
*/
export function useFootballBetsContract(): FootballBets | null {
const { address } = useWallet();
@@ -23,22 +20,10 @@ export function useFootballBetsContract(): FootballBets | null {
const studioUrl = getStudioUrl();
const contract = useMemo(() => {
- // Validate contract address is configured
if (!contractAddress) {
- configError(
- "Setup Required",
- "Contract address not configured. Please set NEXT_PUBLIC_CONTRACT_ADDRESS in your .env file.",
- {
- label: "Setup Guide",
- onClick: () => window.open("/docs/setup", "_blank")
- }
- );
- // Return null to indicate contract is not available
return null;
}
- // Contract instance is recreated when address changes to ensure
- // the genlayer-js client is properly configured with the current account
return new FootballBets(contractAddress, address, studioUrl);
}, [contractAddress, address, studioUrl]);
diff --git a/frontend/lib/utils/toast.ts b/frontend/lib/utils/toast.ts
index 902ad4a..50a44b0 100644
--- a/frontend/lib/utils/toast.ts
+++ b/frontend/lib/utils/toast.ts
@@ -81,21 +81,17 @@ export const loading = (message: string, options?: ExternalToast) => {
// Promise toast for handling async operations
export const promise = (
- promise: Promise,
+ promiseArg: Promise,
messages: {
loading: string;
success: string | ((result: T) => string);
error: string | ((error: any) => string);
- },
- options?: ExternalToast
+ }
) => {
- return sonnerToast.promise(promise, {
+ return sonnerToast.promise(promiseArg, {
loading: messages.loading,
success: messages.success,
error: messages.error,
- }, {
- ...defaultOptions,
- ...options,
});
};
diff --git a/frontend/lib/wagmi/config.ts b/frontend/lib/wagmi/config.ts
new file mode 100644
index 0000000..a3e910f
--- /dev/null
+++ b/frontend/lib/wagmi/config.ts
@@ -0,0 +1,46 @@
+"use client";
+
+import { getDefaultConfig, type Chain as RainbowKitChain } from "@rainbow-me/rainbowkit";
+import { http } from "wagmi";
+
+// GenLayer Network Configuration (from environment variables with fallbacks)
+const GENLAYER_CHAIN_ID = parseInt(process.env.NEXT_PUBLIC_GENLAYER_CHAIN_ID || "61999");
+const GENLAYER_RPC_URL = process.env.NEXT_PUBLIC_GENLAYER_RPC_URL || "https://studio.genlayer.com/api";
+const GENLAYER_CHAIN_NAME = process.env.NEXT_PUBLIC_GENLAYER_CHAIN_NAME || "GenLayer Studio";
+const GENLAYER_SYMBOL = process.env.NEXT_PUBLIC_GENLAYER_SYMBOL || "GEN";
+
+// Define GenLayer as a custom chain for RainbowKit
+export const genlayerChain = {
+ id: GENLAYER_CHAIN_ID,
+ name: GENLAYER_CHAIN_NAME,
+ nativeCurrency: {
+ name: GENLAYER_SYMBOL,
+ symbol: GENLAYER_SYMBOL,
+ decimals: 18,
+ },
+ rpcUrls: {
+ default: {
+ http: [GENLAYER_RPC_URL],
+ },
+ },
+ iconUrl: "/genlayer-icon.svg",
+ iconBackground: "#9B6AF6",
+ testnet: true,
+} as const satisfies RainbowKitChain;
+
+// RainbowKit configuration with GenLayer chain
+// Note: Get your projectId from https://cloud.walletconnect.com (free)
+const WALLETCONNECT_PROJECT_ID = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "demo-project-id";
+
+export const wagmiConfig = getDefaultConfig({
+ appName: "GenLayer Football Betting",
+ projectId: WALLETCONNECT_PROJECT_ID,
+ chains: [genlayerChain],
+ transports: {
+ [GENLAYER_CHAIN_ID]: http(GENLAYER_RPC_URL),
+ },
+ ssr: true,
+});
+
+// Export chain ID for use in other parts of the app
+export { GENLAYER_CHAIN_ID, GENLAYER_RPC_URL };
diff --git a/frontend/next.config.ts b/frontend/next.config.ts
index 4b2803a..e0dd5eb 100644
--- a/frontend/next.config.ts
+++ b/frontend/next.config.ts
@@ -2,7 +2,13 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
- turbopack: {},
+ // Disable Turbopack for build due to compatibility issues with some dependencies
+ // Use webpack for production builds
+ webpack: (config) => {
+ config.resolve.fallback = { fs: false, net: false, tls: false };
+ config.externals.push("pino-pretty", "lokijs", "encoding");
+ return config;
+ },
};
export default nextConfig;
diff --git a/frontend/package.json b/frontend/package.json
index fb7993c..b32c143 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -4,8 +4,8 @@
"description": "GenLayer Football Betting - AI-powered prediction betting on GenLayer blockchain.",
"main": "index.js",
"scripts": {
- "dev": "next dev",
- "build": "next build",
+ "dev": "next dev --turbopack",
+ "build": "next build --webpack",
"start": "next start",
"lint": "next lint"
},
@@ -14,6 +14,7 @@
"license": "ISC",
"type": "module",
"dependencies": {
+ "@rainbow-me/rainbowkit": "^2.2.4",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
diff --git a/frontend/public/genlayer-icon.svg b/frontend/public/genlayer-icon.svg
new file mode 100644
index 0000000..bcbd700
--- /dev/null
+++ b/frontend/public/genlayer-icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/package-lock.json b/package-lock.json
index 669c18e..1e1fd66 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
+ "@rainbow-me/rainbowkit": "^2.2.4",
"@tanstack/react-query": "^5.90.5",
"@wagmi/connectors": "^6.1.3",
"@wagmi/core": "^2.22.1",
@@ -479,6 +480,12 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@emotion/hash": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+ "license": "MIT"
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
@@ -2457,6 +2464,56 @@
}
}
},
+ "node_modules/@rainbow-me/rainbowkit": {
+ "version": "2.2.10",
+ "resolved": "https://registry.npmjs.org/@rainbow-me/rainbowkit/-/rainbowkit-2.2.10.tgz",
+ "integrity": "sha512-8+E4die1A2ovN9t3lWxWnwqTGEdFqThXDQRj+E4eDKuUKyymYD+66Gzm6S9yfg8E95c6hmGlavGUfYPtl1EagA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vanilla-extract/css": "1.17.3",
+ "@vanilla-extract/dynamic": "2.1.4",
+ "@vanilla-extract/sprinkles": "1.6.4",
+ "clsx": "2.1.1",
+ "cuer": "0.0.3",
+ "react-remove-scroll": "2.6.2",
+ "ua-parser-js": "^1.0.37"
+ },
+ "engines": {
+ "node": ">=12.4"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": ">=5.0.0",
+ "react": ">=18",
+ "react-dom": ">=18",
+ "viem": "2.x",
+ "wagmi": "^2.9.0"
+ }
+ },
+ "node_modules/@rainbow-me/rainbowkit/node_modules/react-remove-scroll": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz",
+ "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@reown/appkit": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/@reown/appkit/-/appkit-1.7.8.tgz",
@@ -4795,6 +4852,50 @@
"@types/node": "*"
}
},
+ "node_modules/@vanilla-extract/css": {
+ "version": "1.17.3",
+ "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.17.3.tgz",
+ "integrity": "sha512-jHivr1UPoJTX5Uel4AZSOwrCf4mO42LcdmnhJtUxZaRWhW4FviFbIfs0moAWWld7GOT+2XnuVZjjA/K32uUnMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/hash": "^0.9.0",
+ "@vanilla-extract/private": "^1.0.8",
+ "css-what": "^6.1.0",
+ "cssesc": "^3.0.0",
+ "csstype": "^3.0.7",
+ "dedent": "^1.5.3",
+ "deep-object-diff": "^1.1.9",
+ "deepmerge": "^4.2.2",
+ "lru-cache": "^10.4.3",
+ "media-query-parser": "^2.0.2",
+ "modern-ahocorasick": "^1.0.0",
+ "picocolors": "^1.0.0"
+ }
+ },
+ "node_modules/@vanilla-extract/dynamic": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@vanilla-extract/dynamic/-/dynamic-2.1.4.tgz",
+ "integrity": "sha512-7+Ot7VlP3cIzhJnTsY/kBtNs21s0YD7WI1rKJJKYP56BkbDxi/wrQUWMGEczKPUDkJuFcvbye+E2ub1u/mHH9w==",
+ "license": "MIT",
+ "dependencies": {
+ "@vanilla-extract/private": "^1.0.8"
+ }
+ },
+ "node_modules/@vanilla-extract/private": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.9.tgz",
+ "integrity": "sha512-gT2jbfZuaaCLrAxwXbRgIhGhcXbRZCG3v4TTUnjw0EJ7ArdBRxkq4msNJkbuRkCgfIK5ATmprB5t9ljvLeFDEA==",
+ "license": "MIT"
+ },
+ "node_modules/@vanilla-extract/sprinkles": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/@vanilla-extract/sprinkles/-/sprinkles-1.6.4.tgz",
+ "integrity": "sha512-lW3MuIcdIeHKX81DzhTnw68YJdL1ial05exiuvTLJMdHXQLKcVB93AncLPajMM6mUhaVVx5ALZzNHMTrq/U9Hg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@vanilla-extract/css": "^1.0.0"
+ }
+ },
"node_modules/@wagmi/connectors": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-6.1.4.tgz",
@@ -6302,13 +6403,61 @@
"node": "*"
}
},
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.2.tgz",
"integrity": "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g==",
- "devOptional": true,
"license": "MIT"
},
+ "node_modules/cuer": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/cuer/-/cuer-0.0.3.tgz",
+ "integrity": "sha512-f/UNxRMRCYtfLEGECAViByA3JNflZImOk11G9hwSd+44jvzrc99J35u5l+fbdQ2+ZG441GvOpaeGYBmWquZsbQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/wevm"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "qr": "~0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18",
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -6417,6 +6566,20 @@
"node": ">=0.10"
}
},
+ "node_modules/dedent": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz",
+ "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -6424,6 +6587,21 @@
"license": "MIT",
"peer": true
},
+ "node_modules/deep-object-diff": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz",
+ "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==",
+ "license": "MIT"
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -8919,6 +9097,15 @@
"is-buffer": "~1.1.6"
}
},
+ "node_modules/media-query-parser": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz",
+ "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ }
+ },
"node_modules/micro-ftch": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz",
@@ -8987,6 +9174,12 @@
}
}
},
+ "node_modules/modern-ahocorasick": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.1.0.tgz",
+ "integrity": "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -9758,6 +9951,15 @@
"node": ">=6"
}
},
+ "node_modules/qr": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/qr/-/qr-0.5.3.tgz",
+ "integrity": "sha512-BSrGdNXa8z6PfEYWtvITV21mQ4asR4UCj38Fa3MUUoFAtYzFK/swEQXF+OeBuNbHPFfs3PzpZuK0BXizWXgFOQ==",
+ "license": "(MIT OR Apache-2.0)",
+ "engines": {
+ "node": ">= 20.19.0"
+ }
+ },
"node_modules/qrcode": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz",
@@ -10869,6 +11071,32 @@
"integrity": "sha512-6RD4xOxp26BTZLopNbqT2iErqNhQZZWb5m5F07/UwGhldGvOAKOl41pZ3fxsFp04bNL+PbgMjNfb6IvJAC/uYQ==",
"license": "MIT"
},
+ "node_modules/ua-parser-js": {
+ "version": "1.0.41",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
+ "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "ua-parser-js": "script/cli.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ufo": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",