diff --git a/frontend/package.json b/frontend/package.json index 318e47f..1593ac8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.12", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.3", + "@starknet-react/chains": "^3.1.3", "@starknet-react/core": "^3.7.4", "@starknet-react/typescript-config": "^0.0.1", diff --git a/frontend/public/argent-x-logo.webp b/frontend/public/argent-x-logo.webp new file mode 100644 index 0000000..f30de59 Binary files /dev/null and b/frontend/public/argent-x-logo.webp differ diff --git a/frontend/public/bet-image1.jpg b/frontend/public/bet-image1.jpg new file mode 100644 index 0000000..14ade21 Binary files /dev/null and b/frontend/public/bet-image1.jpg differ diff --git a/frontend/public/bet-image2.jpg b/frontend/public/bet-image2.jpg new file mode 100644 index 0000000..5ed07ae Binary files /dev/null and b/frontend/public/bet-image2.jpg differ diff --git a/frontend/public/bet-image3.jpg b/frontend/public/bet-image3.jpg new file mode 100644 index 0000000..f8e07e3 Binary files /dev/null and b/frontend/public/bet-image3.jpg differ diff --git a/frontend/public/braavos-logo.webp b/frontend/public/braavos-logo.webp new file mode 100644 index 0000000..cba0ec0 Binary files /dev/null and b/frontend/public/braavos-logo.webp differ diff --git a/frontend/src/app/betting/page.tsx b/frontend/src/app/betting/page.tsx new file mode 100644 index 0000000..fb6f047 --- /dev/null +++ b/frontend/src/app/betting/page.tsx @@ -0,0 +1,11 @@ +import BettingInterface from "../../components/betting-interface"; + +export default function Home() { + return ( +
+
+ +
+
+ ); +} diff --git a/frontend/src/components/betting-interface.tsx b/frontend/src/components/betting-interface.tsx new file mode 100644 index 0000000..d302628 --- /dev/null +++ b/frontend/src/components/betting-interface.tsx @@ -0,0 +1,259 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useAccount } from "@starknet-react/core"; +import { WalletConnectButton } from "@/components/wallet-connect-button"; +import { Loader2, CheckCircle, XCircle } from "lucide-react"; +import type { Agent } from "@/types/betting"; +import { useBetting } from "@/hooks/use-betting"; +import { getAgents } from "@/lib/starknet"; +import Image from "next/image"; + +export default function BettingInterface() { + const { address, isConnected } = useAccount(); + const [agents, setAgents] = useState([]); + const [selectedAgent, setSelectedAgent] = useState(null); + const [betAmount, setBetAmount] = useState("0.01"); + const { placeBet, transactions, isLoading } = useBetting(); + + useEffect(() => { + const loadAgents = async () => { + try { + const agentData = await getAgents(); + setAgents(agentData); + } catch (error) { + console.error("Failed to load agents:", error); + } + }; + + if (isConnected) { + loadAgents(); + } + }, [isConnected]); + + const handleBetSubmit = async () => { + if (!selectedAgent) { + alert("Please select an agent to place your bet."); + return; + } + + if (!betAmount || Number.parseFloat(betAmount) <= 0) { + alert("Please enter a valid bet amount."); + return; + } + + try { + await placeBet(selectedAgent.id, betAmount); + setBetAmount("0.01"); + } catch (error) { + console.error("Failed to place bet:", error); + } + }; + + return ( +
+
+

+ BOTBATTLES ARENA +

+ +
+ + {!isConnected ? ( +
+

+ Connect Your Wallet +

+

+ Connect your StarkNet wallet to start placing bets on AI agents. +

+ +
+ ) : ( +
+
+
+

+ Place Your Bets +

+
+ {agents.map((agent) => ( +
setSelectedAgent(agent)} + > +
+
+ {agent.name} +
+

+ {agent.name} +

+
+
66 + ? "bg-green-400" + : agent.performance > 33 + ? "bg-yellow-400" + : "bg-red-400" + } rounded-full`} + style={{ width: `${agent.performance}%` }} + /> +
+
+
+ ))} +
+ + {selectedAgent && ( +
+

+ Betting on: {selectedAgent.name} +

+ +
+ +
+ setBetAmount(e.target.value)} + min="0.001" + step="0.001" + className={` + bg-gray-700 border-2 border-gray-500 + font-pixel text-white px-3 py-2 rounded + focus:border-green-400 focus:ring-1 focus:ring-green-400 + focus:outline-none + hover:border-gray-400 + shadow-[0_2px_4px_rgba(0,0,0,0.25)] + transition-all + w-full + `} + /> + +
+
+
+ )} +
+
+ +
+
+

+ Transaction History +

+
+ {transactions.length > 0 ? ( + transactions.map((tx) => ( +
+
+ {tx.status === "confirmed" ? ( + + ) : tx.status === "failed" ? ( + + ) : ( + + )} + +
+

+ {tx.message} +

+
+

+ {tx.status === "pending" + ? "Processing..." + : tx.status === "confirmed" + ? "Confirmed" + : "Failed"} +

+

+ {new Date(tx.timestamp).toLocaleTimeString()} +

+
+ {tx.status === "confirmed" && ( +

+ Tx: {tx.hash.substring(0, 10)}... + {tx.hash.substring(tx.hash.length - 6)} +

+ )} +
+
+
+ )) + ) : ( +

+ No transactions yet. Place a bet to get started! +

+ )} +
+
+ +
+

+ Wallet Info +

+
+

+ Address:{" "} + {address} +

+

+ Network: StarkNet +

+
+
+
+
+ )} +
+ ); +} diff --git a/frontend/src/components/wallet-connect-button.tsx b/frontend/src/components/wallet-connect-button.tsx new file mode 100644 index 0000000..4d8a3e8 --- /dev/null +++ b/frontend/src/components/wallet-connect-button.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { + useAccount, + useConnect, + useDisconnect, + type Connector, +} from "@starknet-react/core"; +import { Wallet } from "lucide-react"; +// import { Button } from "@/components/ui/button"; +import { WalletDropdown } from "./wallet-dropdown"; +import { truncateAddress } from "@/utils/wallet"; +import WalletConnectModal from "./wallet-connect-modal"; + +export function WalletConnectButton() { + const [isModalOpen, setIsModalOpen] = useState(false); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); + const [connectionError, setConnectionError] = useState(null); + const dropdownTimeoutRef = useRef(null); + + const { address, isConnected } = useAccount(); + const { connect, connectors } = useConnect(); + const { disconnect } = useDisconnect(); + + const handleConnect = () => { + if (isConnected) { + setIsDropdownOpen(!isDropdownOpen); + } else { + setIsModalOpen(true); + setConnectionError(null); + } + }; + + const handleDisconnect = () => { + disconnect(); + setIsDropdownOpen(false); + localStorage.removeItem("lastUsedConnector"); + }; + + const handleConnectorSelect = async (connector: Connector) => { + if (!connector) return; + + setIsConnecting(true); + setConnectionError(null); + + try { + await connect({ connector }); + + if (connector.id) { + localStorage.setItem("lastUsedConnector", connector.id); + } + + // Only close modal after successful connection + setIsModalOpen(false); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Failed to connect wallet"; + console.error("Wallet connection error:", errorMessage); + setConnectionError(errorMessage); + } finally { + setIsConnecting(false); + } + }; + + // Handle dropdown hover behavior + const handleMouseEnter = () => { + if (isConnected) { + if (dropdownTimeoutRef.current) { + clearTimeout(dropdownTimeoutRef.current); + dropdownTimeoutRef.current = null; + } + setIsDropdownOpen(true); + } + }; + + const handleMouseLeave = () => { + if (dropdownTimeoutRef.current) { + clearTimeout(dropdownTimeoutRef.current); + } + + dropdownTimeoutRef.current = setTimeout(() => { + setIsDropdownOpen(false); + }, 300); + }; + + // Listen for wallet_disconnected events + useEffect(() => { + const handleWalletDisconnected = () => { + setIsDropdownOpen(false); + }; + + window.addEventListener("wallet_disconnected", handleWalletDisconnected); + + return () => { + window.removeEventListener( + "wallet_disconnected", + handleWalletDisconnected + ); + + if (dropdownTimeoutRef.current) { + clearTimeout(dropdownTimeoutRef.current); + } + }; + }, []); + + return ( +
+ {/* Connect Button */} + + + {/* Wallet Connect Modal */} + { + if (!isConnecting) { + setIsModalOpen(false); + setConnectionError(null); + } + }} + onSelect={async (walletId) => { + const connector = connectors.find((c) => c.id === walletId); + if (connector) { + await handleConnectorSelect(connector); + } + }} + /> + + {/* Wallet Dropdown */} + {isConnected && address && ( + setIsDropdownOpen(false)} + address={address} + onDisconnect={handleDisconnect} + /> + )} +
+ ); +} diff --git a/frontend/src/components/wallet-connect-modal.tsx b/frontend/src/components/wallet-connect-modal.tsx index 97d87bc..e215fc3 100644 --- a/frontend/src/components/wallet-connect-modal.tsx +++ b/frontend/src/components/wallet-connect-modal.tsx @@ -15,37 +15,21 @@ import { interface WalletConnectModalProps { isOpen: boolean; onClose: () => void; - onSelect: (wallet: string) => void; + onSelect: (wallet: string) => void; // Added this prop } export default function WalletConnectModal({ isOpen, onClose, + onSelect, // Added this prop }: WalletConnectModalProps) { const [selectedWallet, setSelectedWallet] = useState(null); - const { connectors, connectAsync } = useWalletContext(); + const { connectors } = useWalletContext(); const handleSelect = (walletId: string) => { setSelectedWallet(walletId); }; - // ② On confirm, look up the connector object and call connectWallet - const handleConfirm = async () => { - if (!selectedWallet) return; - - const connector = connectors.find((c) => c.id === selectedWallet); - if (!connector) { - console.error("Connector not found:", selectedWallet); - return; - } - - try { - await connectAsync({ connector }); // ■ await the wallet prompt - onClose(); - } catch (err) { - console.error("Wallet connection failed:", err); // ■ handle rejections - } - }; // helper to get icon source function getIconSource( @@ -116,10 +100,14 @@ export default function WalletConnectModal({ ))}
- {/* ③ Confirmation button */} + {/* Connect button now triggers onSelect */} + + +
+ + + View on Explorer + + +
+ + ); +} diff --git a/frontend/src/hooks/use-betting.tsx b/frontend/src/hooks/use-betting.tsx new file mode 100644 index 0000000..a81537e --- /dev/null +++ b/frontend/src/hooks/use-betting.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { useAccount } from "@starknet-react/core"; +import { Transaction } from "../types/betting"; +import { placeBet as placeBetOnChain } from "../lib/starknet"; + +export function useBetting() { + const { address } = useAccount(); + const [transactions, setTransactions] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const placeBet = useCallback( + async (agentId: number, amount: string) => { + if (!address) { + throw new Error("Wallet not connected"); + } + + setIsLoading(true); + + try { + // Create a pending transaction record + const pendingTx: Transaction = { + hash: "pending-" + Date.now(), + status: "pending", + message: `Placing bet of ${amount} STRK on agent #${agentId}...`, + timestamp: Date.now(), + }; + + setTransactions((prev) => [pendingTx, ...prev]); + + // Call the StarkNet contract + const result = await placeBetOnChain(agentId, amount); + + // Update the transaction status + setTransactions((prev) => + prev.map((tx) => + tx.hash === pendingTx.hash + ? { + hash: result.transactionHash, + status: "confirmed", + message: `Successfully placed bet of ${amount} STRK on agent #${agentId}`, + timestamp: Date.now(), + } + : tx + ) + ); + + return result; + } catch (error) { + console.error("Transaction failed:", error); + + // Update the transaction status to failed + setTransactions((prev) => + prev.map((tx) => + tx.hash === "pending-" + Date.now().toString().slice(0, -3) + ? { + hash: "failed-" + Date.now(), + status: "failed", + message: `Failed to place bet: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + timestamp: Date.now(), + } + : tx + ) + ); + + throw error; + } finally { + setIsLoading(false); + } + }, + [address] + ); + + return { + transactions, + isLoading, + placeBet, + }; +} diff --git a/frontend/src/hooks/use-starknet-connect.tsx b/frontend/src/hooks/use-starknet-connect.tsx new file mode 100644 index 0000000..81c7bd3 --- /dev/null +++ b/frontend/src/hooks/use-starknet-connect.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useCallback, useMemo } from "react"; +import { + argent, + braavos, + useInjectedConnectors, + voyager, + publicProvider, +} from "@starknet-react/core"; +import { mainnet, sepolia } from "@starknet-react/chains"; + +/** + * Hook to get Starknet connectors + */ +export function useStarknetConnectors() { + return useInjectedConnectors({ + recommended: [argent(), braavos()], + includeRecommended: "onlyIfNoConnectors", + order: "random", + }); +} + +/** + * Hook to get the complete Starknet configuration + * Includes error handling for disconnection events + */ +export function useStarknetConfig() { + const { connectors } = useStarknetConnectors(); + + // Handle wallet disconnection errors + const handleDisconnectError = useCallback((error: Error) => { + console.warn("Starknet disconnect error:", error); + // Dispatch a custom event that our components can listen for + window.dispatchEvent(new CustomEvent("wallet_disconnected")); + // Return true to indicate the error was handled + return true; + }, []); + + return useMemo( + () => ({ + chains: [mainnet, sepolia], + provider: publicProvider(), + connectors, + explorer: voyager, + autoConnect: true, + onDisconnectError: handleDisconnectError, + }), + [connectors, handleDisconnectError] + ); +} diff --git a/frontend/src/hooks/use-wallet-connection.tsx b/frontend/src/hooks/use-wallet-connection.tsx new file mode 100644 index 0000000..b609cb3 --- /dev/null +++ b/frontend/src/hooks/use-wallet-connection.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { + useAccount, + useConnect, + useDisconnect, + type Connector, +} from "@starknet-react/core"; + +/** + * Custom hook for managing wallet connection state and operations + */ +export function useWalletConnection() { + const [isConnecting, setIsConnecting] = useState(false); + const [connectionError, setConnectionError] = useState(null); + const { address, isConnected, isConnecting: isReconnecting } = useAccount(); + const { connect, connectors } = useConnect(); + const { disconnect } = useDisconnect(); + + // Connect to wallet with error handling + const connectWallet = useCallback( + async (connector: Connector) => { + if (!connector) return Promise.reject(new Error("No connector provided")); + + setIsConnecting(true); + setConnectionError(null); + + try { + await connect({ connector }); + + // Save the last used connector for auto-connect + if (connector.id) { + localStorage.setItem("lastUsedConnector", connector.id); + } + return Promise.resolve(); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Failed to connect wallet"; + console.error("Wallet connection error:", errorMessage); + setConnectionError(errorMessage); + return Promise.reject(error); + } finally { + setIsConnecting(false); + } + }, + [connect] + ); + + // Disconnect wallet + const disconnectWallet = useCallback(() => { + disconnect(); + localStorage.removeItem("lastUsedConnector"); + }, [disconnect]); + + // Auto-connect on component mount + const autoConnect = useCallback(() => { + if ( + !isConnected && + !isReconnecting && + connectors && + connectors.length > 0 + ) { + const lastConnector = localStorage.getItem("lastUsedConnector"); + + if (lastConnector) { + const connector = connectors.find((c) => c.id === lastConnector); + if (connector) { + connectWallet(connector).catch(() => { + // Silent fail for auto-connect + }); + } + } + } + }, [connectWallet, connectors, isConnected, isReconnecting]); + + // Listen for wallet_disconnected events + useEffect(() => { + const handleWalletDisconnected = () => { + // Clear any connection state + localStorage.removeItem("lastUsedConnector"); + }; + + window.addEventListener("wallet_disconnected", handleWalletDisconnected); + + return () => { + window.removeEventListener( + "wallet_disconnected", + handleWalletDisconnected + ); + }; + }, []); + + // Run auto-connect on component mount + useEffect(() => { + autoConnect(); + }, [autoConnect]); + + return { + address, + isConnected, + isConnecting, + connectionError, + connectors, + connectWallet, + disconnectWallet, + autoConnect, + }; +} diff --git a/frontend/src/lib/starknet.ts b/frontend/src/lib/starknet.ts new file mode 100644 index 0000000..d898e04 --- /dev/null +++ b/frontend/src/lib/starknet.ts @@ -0,0 +1,82 @@ +import { Agent } from "../types/betting"; + +// Mock agents data +const mockAgents: Agent[] = [ + { + id: 1, + name: "Mark Cuban", + image: "/bet-image1.jpg", + performance: 75, + }, + { + id: 2, + name: "Batman", + image: "/bet-image3.jpg", + performance: 45, + }, + { + id: 3, + name: "Literally a fish", + image: "/bet-image2.jpg", + performance: 90, + }, +]; + +// Get list of available agents +export async function getAgents(): Promise { + // In a real implementation, this would fetch agents from a backend or smart contract + return new Promise((resolve) => { + setTimeout(() => { + resolve(mockAgents); + }, 800); + }); +} + +// Place a bet on an agent +export async function placeBet( + agentId: number, + amount: string +): Promise<{ transactionHash: string }> { + // In a real implementation, this would call the StarkNet contract + + // This is where you would implement the actual StarkNet contract call + // Example implementation (commented out as it requires actual contract): + /* + const contractAddress = "0x123..."; // Your contract address + const contract = new Contract(abi, contractAddress, provider); + + // Convert amount to uint256 + const amountUint256 = uint256.bnToUint256(ethers.utils.parseEther(amount)); + + // Prepare calldata + const calldata = CallData.compile({ + agentId: agentId, + amount: amountUint256 + }); + + // Execute transaction + const tx = await contract.invoke("placeBet", calldata); + + return { + transactionHash: tx.transaction_hash + }; + */ + + // For now, we'll simulate a transaction with a delay + return new Promise((resolve, reject) => { + // Simulate transaction delay + setTimeout(() => { + // 90% chance of success + if (Math.random() > 0.1) { + resolve({ + transactionHash: + "0x" + + Math.random().toString(16).substring(2) + + Math.random().toString(16).substring(2), + }); + } else { + reject(new Error("Transaction rejected by the network")); + } + }, 2000); + }); +} diff --git a/frontend/src/types/betting.ts b/frontend/src/types/betting.ts new file mode 100644 index 0000000..ceef481 --- /dev/null +++ b/frontend/src/types/betting.ts @@ -0,0 +1,13 @@ +export type Agent = { + id: number; + name: string; + image: string; + performance: number; // 0-100 scale for the colored bar +}; + +export type Transaction = { + hash: string; + status: "pending" | "confirmed" | "failed"; + message: string; + timestamp: number; +}; diff --git a/frontend/src/utils/wallet.ts b/frontend/src/utils/wallet.ts new file mode 100644 index 0000000..bbbdf7d --- /dev/null +++ b/frontend/src/utils/wallet.ts @@ -0,0 +1,13 @@ +/** + * Truncates an address for display purposes + * @param address The full address to truncate + * @returns The truncated address (e.g. 0x1234...5678) + */ +export function truncateAddress(address: string): string { + if (!address) return ""; + if (address.length <= 10) return address; + + return `${address.substring(0, 6)}...${address.substring( + address.length - 4 + )}`; +}