-
+
{gladiators.map((gladiator) => (
-
+
setInformation(e.target.value)}
@@ -124,11 +144,11 @@ export function BribeSubmission({ marketId, roundId, gladiators, onBribeSubmitte
disabled={isSubmitDisabled()}
onClick={handleSubmit}
>
- {!isConnected
- ? 'Connect Wallet'
- : isPending
- ? 'Submitting...'
- : 'Submit Bribe (1 Token)'}
+ {!isConnected
+ ? "Connect Wallet"
+ : isPending
+ ? "Submitting..."
+ : "Submit Bribe (1 Token)"}
@@ -141,12 +161,23 @@ export function BribeSubmission({ marketId, roundId, gladiators, onBribeSubmitte
{typedBribesData[0].map((address: string, index: number) => (
- From: {address.slice(0, 6)}...{address.slice(-4)}
- {formatEther(typedBribesData[1][index])} tokens
+
+ From: {address.slice(0, 6)}...{address.slice(-4)}
+
+
+ {formatEther(typedBribesData[1][index])} tokens
+
{typedBribesData[2][index]}
- Supporting: {gladiators.find(g => g.index.toString() === typedBribesData[4][index].toString())?.name}
+ Supporting:{" "}
+ {
+ gladiators.find(
+ (g) =>
+ g.index.toString() ===
+ typedBribesData[4][index].toString()
+ )?.name
+ }
))}
@@ -155,4 +186,4 @@ export function BribeSubmission({ marketId, roundId, gladiators, onBribeSubmitte
)}
);
-}
\ No newline at end of file
+}
diff --git a/frontend/debate-ai/components/debate-details/DebateDiscussion.tsx b/frontend/debate-ai/components/debate-details/DebateDiscussion.tsx
new file mode 100644
index 0000000..a5c63d8
--- /dev/null
+++ b/frontend/debate-ai/components/debate-details/DebateDiscussion.tsx
@@ -0,0 +1,137 @@
+import React from "react";
+import { Card, CardContent } from "@/components/ui/card";
+import { ChevronDown, Menu } from "lucide-react";
+
+interface Message {
+ sender: string;
+ content: string;
+ timestamp: string;
+}
+
+type BondingCurveStruct = {
+ target: bigint; // Target amount to reach
+ current: bigint; // Current amount raised
+ basePrice: bigint; // Starting price
+ currentPrice: bigint; // Current price
+ isFulfilled: boolean; // Whether target is reached
+ endTime: bigint; // When bonding period ends
+};
+type ExpandedCardStruct = {
+ aiDiscussion: boolean;
+ bondingCurve: boolean;
+ debateInfo: boolean;
+ gladiators: boolean;
+ leaderboard: boolean;
+};
+
+type ToggleCardFunction = (cardName: keyof ExpandedCardStruct) => void;
+
+type DebateDiscussionProps = {
+ bondingCurve: BondingCurveStruct | null;
+ expandedCards: ExpandedCardStruct;
+ toggleCard: ToggleCardFunction;
+ chatMessages: Message[];
+};
+
+export const DebateDiscussion = ({
+ bondingCurve,
+ expandedCards,
+ toggleCard,
+ chatMessages,
+}: DebateDiscussionProps) => {
+ return (
+
+
+ toggleCard("aiDiscussion")}
+ >
+
+ • AI Agents Discussion
+
+
+ {bondingCurve?.isFulfilled ? (
+ <>
+
+
+ >
+ ) : (
+
+
+ Locked 🔒
+
+
+ )}
+
+
+
+ {expandedCards.aiDiscussion && (
+
+ {bondingCurve?.isFulfilled ? (
+
+ {chatMessages.map((message, index) => (
+
+
+ {message.sender.charAt(0)}
+
+
+
+ {message.sender}
+
+
+
+
+ {message.content}
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
+
+ AI Agents are waiting to start
+
+
+ Once the bonding curve target is reached, three expert AI
+ agents will begin analyzing and debating this topic in
+ real-time.
+
+
+
+ Progress:{" "}
+ {bondingCurve
+ ? (
+ (Number(bondingCurve.current) * 100) /
+ Number(bondingCurve.target)
+ ).toFixed(1)
+ : "0"}
+ % to unlock
+
+
+
+
+ )}
+
+ )}
+
+
+ );
+};
diff --git a/frontend/debate-ai/components/debate-details/DebateInfo.tsx b/frontend/debate-ai/components/debate-details/DebateInfo.tsx
new file mode 100644
index 0000000..c2f446d
--- /dev/null
+++ b/frontend/debate-ai/components/debate-details/DebateInfo.tsx
@@ -0,0 +1,139 @@
+import React from "react";
+import { Card, CardContent } from "@/components/ui/card";
+import { formatEther, formatAddress } from "@/lib/utils";
+import { Clock, Users, Trophy } from "lucide-react";
+
+type ExpandedCardStruct = {
+ aiDiscussion: boolean;
+ bondingCurve: boolean;
+ debateInfo: boolean;
+ gladiators: boolean;
+ leaderboard: boolean;
+};
+
+type DebateDetails = [
+ topic: string, // topic
+ startTime: bigint, // startTime
+ duration: bigint, // duration
+ debateEndTime: bigint, // debateEndTime
+ currentRound: bigint, // currentRound
+ totalRounds: bigint, // totalRounds
+ isActive: boolean, // isActive
+ creator: string, // creator
+ market: string, // market
+ judges: string[], // judges
+ hasOutcome: boolean, // hasOutcome
+ finalOutcome: bigint, // finalOutcome
+];
+
+type DebateInfoProps = {
+ expandedCards: ExpandedCardStruct;
+ debateDetails: DebateDetails | undefined;
+ totalVolume: bigint | undefined;
+ timeRemaining: number;
+};
+
+function DebateInfo({
+ expandedCards,
+ debateDetails,
+ totalVolume,
+ timeRemaining,
+}: DebateInfoProps) {
+ // Format total volume
+ const totalVolumeFormatted = formatEther(totalVolume || 0n);
+
+ const daysRemaining = Math.floor(timeRemaining / (24 * 60 * 60));
+ const hoursRemaining = Math.floor((timeRemaining % (24 * 60 * 60)) / 3600);
+
+ let isDummyActive = true;
+ if (daysRemaining === 0 && hoursRemaining === 0) {
+ isDummyActive = false;
+ }
+
+ const [
+ topic,
+ startTime,
+ duration,
+ debateEndTime,
+ currentRound,
+ totalRounds,
+ isActive, // TODO: change it later on from contract
+ creator,
+ market,
+ judges,
+ hasOutcome,
+ finalOutcome,
+ ] = debateDetails || [];
+
+ return (
+
+
+ {expandedCards.debateInfo && (
+
+
+ {/* Header Section */}
+
+
+
+ {topic || "Loading..."}
+
+
+
+
+ {formatAddress(creator || "")}
+
+
+
+
+ ${totalVolumeFormatted}
+
+
+
+
+
+
+ {currentRound?.toString() || "0"}/
+ {totalRounds?.toString() || "0"}
+
+
+
+
+
+
+
+
+
{isDummyActive ? "Active" : "Ended"}
+
+
+ {isDummyActive && (
+
+
+
+ {daysRemaining}d {hoursRemaining}h remaining
+
+
+ )}
+
+
+
+
+ )}
+
+
+ );
+}
+
+export default DebateInfo;
diff --git a/frontend/debate-ai/components/debate-details/DebateView2.tsx b/frontend/debate-ai/components/debate-details/DebateView2.tsx
new file mode 100644
index 0000000..5f705c9
--- /dev/null
+++ b/frontend/debate-ai/components/debate-details/DebateView2.tsx
@@ -0,0 +1,945 @@
+import {
+ DEBATE_FACTORY_ADDRESS,
+ DEBATE_FACTORY_ABI,
+ MARKET_FACTORY_ADDRESS,
+ MARKET_FACTORY_ABI,
+ GLADIATOR_NFT_ABI,
+ GLADIATOR_NFT_ADDRESS,
+} from "@/config/contracts";
+import {
+ useAccount,
+ useReadContract,
+ useWriteContract,
+ useWaitForTransactionReceipt,
+ usePublicClient,
+} from "wagmi";
+
+import { useState, useEffect, useMemo } from "react";
+
+import { waitForTransactionReceipt } from "viem/actions";
+
+import BondingCurveComp from "./BondingCurve";
+import DebateInfo from "./DebateInfo";
+import { DebateDiscussion } from "./DebateDiscussion";
+
+import GladiatorListComp from "./GladiatorList";
+import NominationCard from "./NominationCard";
+import DesktopSideDrawer from "./DesktopSideDrawer";
+import MobileSideDrawer from "./MobileSideDrawer";
+import { Dialog, DialogContent } from "@/components/ui/dialog";
+import { Toaster, toast } from "sonner";
+
+interface Message {
+ sender: string;
+ content: string;
+ timestamp: string;
+}
+
+// Type for how bonding curve is returned from contract
+type BondingCurve = [
+ bigint, // target
+ bigint, // current
+ bigint, // basePrice
+ bigint, // currentPrice
+ boolean, // isFulfilled
+ bigint, // endTime
+];
+
+type Gladiator = {
+ aiAddress: string; // Address of the AI agent
+ name: string; // Name of the gladiator
+ index: bigint; // Index in gladiators array
+ isActive: boolean; // Whether still in competition
+ publicKey: string; // Public key for encrypted bribes
+ tokenId: number; // Token ID of the NFT - making this required instead of optional
+};
+
+type JudgeVerdict = {
+ scores: bigint[]; // Scores for each gladiator
+ timestamp: bigint; // When verdict was given
+};
+
+type Round = {
+ startTime: bigint;
+ endTime: bigint;
+ isComplete: boolean;
+ verdict: JudgeVerdict;
+};
+
+type Order = {
+ price: bigint; // Price in basis points (100 = 1%)
+ amount: bigint; // Amount of shares
+ outcomeIndex: bigint; // Which outcome this order is for
+ owner: string; // Order creator
+};
+
+type Position = {
+ shares: { [gladiatorIndex: string]: bigint }; // gladiatorIndex => number of shares
+};
+
+type Bribe = {
+ briber: string;
+ amount: bigint;
+ information: string;
+ timestamp: bigint;
+ outcomeIndex: bigint;
+};
+
+// Return types for contract read functions
+type MarketDetails = [
+ string, // token
+ bigint, // debateId
+ boolean, // resolved
+ bigint, // winningGladiator
+ BondingCurve, // bondingCurve
+ bigint, // totalBondingAmount
+];
+
+type RoundInfo = [
+ bigint, // roundIndex
+ bigint, // startTime
+ bigint, // endTime
+ boolean, // isComplete
+];
+
+type LeaderboardInfo = [
+ bigint[], // totalScores
+ bigint[], // gladiatorIndexes
+];
+
+type DebateDetails = [
+ topic: string, // topic
+ startTime: bigint, // startTime
+ duration: bigint, // duration
+ debateEndTime: bigint, // debateEndTime
+ currentRound: bigint, // currentRound
+ totalRounds: bigint, // totalRounds
+ isActive: boolean, // isActive
+ creator: string, // creator
+ market: string, // market
+ judges: string[], // judges
+ hasOutcome: boolean, // hasOutcome
+ finalOutcome: bigint, // finalOutcome
+];
+
+interface DebateViewProps {
+ debateId: number;
+}
+
+export function DebateView2({ debateId }: DebateViewProps) {
+ const { isConnected, address } = useAccount();
+ const publicClient = usePublicClient();
+
+ // Component state
+ const [pendingTx, setPendingTx] = useState(false);
+ const [orderType, setOrderType] = useState<"buy" | "sell">("buy");
+ const [selectedGladiator, setSelectedGladiator] = useState
(
+ null
+ );
+ const [amount, setAmount] = useState("0");
+ const [potentialReturn, setPotentialReturn] = useState("0.00");
+ const [chatMessages, setChatMessages] = useState([]);
+
+ // Transaction state
+ const {
+ data: approveHash,
+ isPending: isApprovePending,
+ writeContract: approveToken,
+ } = useWriteContract();
+
+ const {
+ data: orderHash,
+ isPending: isOrderPending,
+ writeContract: placeLimitOrder,
+ } = useWriteContract();
+
+ const { isLoading: isApproveConfirming, isSuccess: isApproveConfirmed } =
+ useWaitForTransactionReceipt({
+ hash: approveHash,
+ });
+
+ const {
+ isLoading: isOrderConfirming,
+ isSuccess: isOrderConfirmed,
+ error: errorTxnOrder,
+ } = useWaitForTransactionReceipt({
+ hash: orderHash,
+ });
+
+ // Effect for handling order confirmation
+ useEffect(() => {
+ if (isOrderConfirmed) {
+ console.log("Order confirmed, refreshing data...");
+ toast.success("Order confirmed!");
+ // Reset form
+ setAmount("0");
+ setPotentialReturn("0.00");
+ setSelectedGladiator(null);
+
+ // Refetch all data
+ refetchAllData();
+ }
+ }, [isOrderConfirmed]);
+
+ // Add state for tracking expanded cards
+ const [expandedCards, setExpandedCards] = useState({
+ aiDiscussion: true,
+ bondingCurve: true,
+ debateInfo: true,
+ gladiators: true,
+ leaderboard: true,
+ });
+
+ const toggleCard = (cardName: keyof typeof expandedCards) => {
+ setExpandedCards((prev) => ({
+ ...prev,
+ [cardName]: !prev[cardName],
+ }));
+ };
+
+ /// Get Debate Details
+ const { data: debateDetails, refetch: refetchDebateDetails } =
+ useReadContract({
+ address: DEBATE_FACTORY_ADDRESS,
+ abi: DEBATE_FACTORY_ABI,
+ functionName: "getDebateDetails",
+ args: [BigInt(debateId)],
+ }) as { data: DebateDetails | undefined; refetch: () => void };
+
+ // Get market ID from debate ID
+ const { data: marketId, refetch: refetchMarketId } = useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "debateIdToMarketId",
+ args: [BigInt(debateId)],
+ });
+
+ // Log market ID changes
+ useEffect(() => {
+ console.log("[DebateView] marketId changed:", marketId?.toString());
+ }, [marketId]);
+
+ // Get market details
+ const { data: marketDetails, refetch: refetchMarketDetails } =
+ useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "getMarketDetails",
+ args: marketId ? [marketId] : undefined,
+ }) as { data: MarketDetails | undefined; refetch: () => void };
+
+ // Get gladiators
+ const { data: gladiators, refetch: refetchGladiators } = useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "getGladiators",
+ args: marketId ? [marketId] : undefined,
+ }) as { data: Gladiator[] | undefined; refetch: () => void };
+
+ // Get round info
+ const { data: roundInfo, refetch: refetchRoundInfo } = useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "getCurrentRound",
+ args: marketId ? [marketId] : undefined,
+ }) as { data: RoundInfo | undefined; refetch: () => void };
+
+ // Get leaderboard
+ const { data: leaderboard, refetch: refetchLeaderboard } = useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "getLeaderboard",
+ args: marketId ? [marketId] : undefined,
+ }) as { data: LeaderboardInfo | undefined; refetch: () => void };
+
+ // Get market prices
+ const { data: gladiatorPrices, refetch: refetchPrices } = useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "getMarketPrices",
+ args: marketId ? [marketId] : undefined,
+ }) as { data: bigint[] | undefined; refetch: () => void };
+
+ // Get market volumes
+ const { data: gladiatorVolumes, refetch: refetchVolumes } = useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "getMarketVolumes",
+ args: marketId ? [marketId] : undefined,
+ }) as { data: bigint[] | undefined; refetch: () => void };
+
+ // Get total volume
+ const { data: totalVolume, refetch: refetchTotalVolume } = useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "getTotalVolume",
+ args: marketId ? [marketId] : undefined,
+ }) as { data: bigint | undefined; refetch: () => void };
+
+ // Get bonding curve details
+ const { data: bondingCurveDetails, refetch: refetchBondingCurve } =
+ useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "getBondingCurveDetails",
+ args: marketId ? [marketId] : undefined,
+ }) as { data: BondingCurve | undefined; refetch: () => void };
+
+ // Get user positions if connected
+ const { data: userPositions } = useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "getUserPositions",
+ args: marketId && address ? [marketId, address] : undefined,
+ }) as { data: bigint[] | undefined };
+
+ // Add allowance check
+ const { data: currentAllowance } = useReadContract({
+ address: marketDetails?.[0] as `0x${string}`,
+ abi: [
+ {
+ name: "allowance",
+ type: "function",
+ stateMutability: "view",
+ inputs: [
+ { name: "owner", type: "address" },
+ { name: "spender", type: "address" },
+ ],
+ outputs: [{ type: "uint256" }],
+ },
+ ],
+ functionName: "allowance",
+ args:
+ address && marketDetails ? [address, MARKET_FACTORY_ADDRESS] : undefined,
+ });
+
+ // Add state for chat messages
+
+ // Effect to fetch and subscribe to chat messages
+ useEffect(() => {
+ console.log(
+ "[DebateView] Chat effect triggered with marketId:",
+ marketId?.toString()
+ );
+ if (!marketId) {
+ console.log("[DebateView] No valid marketId yet");
+ return;
+ }
+
+ // Convert marketId to bigint to ensure type safety
+ const marketIdBigInt = BigInt(marketId.toString());
+
+ let ws: WebSocket;
+ let isWsConnected = false;
+
+ const fetchMessages = async () => {
+ try {
+ console.log(
+ "[DebateView] Fetching messages for market:",
+ marketIdBigInt.toString()
+ );
+ const response = await fetch(`/api/chat/${marketIdBigInt}`, {
+ method: "GET",
+ headers: {
+ "Cache-Control": "no-cache, no-store, must-revalidate",
+ Pragma: "no-cache",
+ Expires: "0",
+ // Add a timestamp to force unique requests
+ "X-Request-Time": Date.now().toString(),
+ },
+ // Disable caching at fetch level
+ cache: "no-store",
+ next: { revalidate: 0 },
+ });
+ if (!response.ok) {
+ console.error(
+ "[DebateView] HTTP error from API:",
+ response.status,
+ response.statusText
+ );
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const messages = await response.json();
+ if ("error" in messages) {
+ console.error("[DebateView] API returned error:", messages.error);
+ return;
+ }
+ console.log("[DebateView] Received messages:", messages);
+ setChatMessages(messages);
+ } catch (error) {
+ console.error("[DebateView] Error fetching chat messages:", error);
+ }
+ };
+
+ // Set up WebSocket connection
+ const setupWebSocket = () => {
+ if (!process.env.NEXT_PUBLIC_WS_URL) {
+ console.error("[DebateView] NEXT_PUBLIC_WS_URL not set");
+ return;
+ }
+
+ console.log(
+ "[DebateView] Setting up WebSocket for market:",
+ marketIdBigInt.toString()
+ );
+ const wsUrl = `${process.env.NEXT_PUBLIC_WS_URL}/chat/${marketIdBigInt}`;
+ console.log("[DebateView] WebSocket URL:", wsUrl);
+ ws = new WebSocket(wsUrl);
+
+ ws.onopen = () => {
+ console.log("[DebateView] WebSocket connected");
+ isWsConnected = true;
+ // Fetch messages after WebSocket is connected
+ fetchMessages();
+ };
+
+ ws.onerror = (error) => {
+ console.error("[DebateView] WebSocket error:", error);
+ isWsConnected = false;
+ };
+
+ ws.onclose = () => {
+ console.log("[DebateView] WebSocket closed");
+ isWsConnected = false;
+ // Try to reconnect after a delay
+ setTimeout(() => {
+ if (!isWsConnected) {
+ console.log("[DebateView] Attempting to reconnect WebSocket...");
+ setupWebSocket();
+ }
+ }, 3000);
+ };
+
+ ws.onmessage = (event) => {
+ console.log("[DebateView] Received WebSocket message:", event.data);
+ try {
+ const message = JSON.parse(event.data);
+ setChatMessages((prev) => [...prev, message]);
+ } catch (error) {
+ console.error("[DebateView] Error parsing WebSocket message:", error);
+ }
+ };
+ };
+
+ // Start WebSocket connection
+ setupWebSocket();
+
+ return () => {
+ console.log("[DebateView] Cleaning up WebSocket connection");
+ isWsConnected = false;
+ if (ws) {
+ ws.close();
+ }
+ };
+ }, [marketId]);
+
+ // Function to refetch all data
+ const refetchAllData = async () => {
+ await Promise.all([
+ refetchMarketId(),
+ refetchMarketDetails(),
+ refetchGladiators(),
+ refetchRoundInfo(),
+ refetchLeaderboard(),
+ refetchPrices(),
+ refetchVolumes(),
+ refetchTotalVolume(),
+ refetchBondingCurve(),
+ refetchDebateDetails(),
+ ]);
+ };
+
+ // Extract debate details
+ const [debateEndTime] = debateDetails || [];
+
+ // Loading check for market data
+ const isMarketDataLoading =
+ !marketDetails ||
+ !gladiators ||
+ !gladiatorPrices ||
+ !bondingCurveDetails ||
+ !debateDetails;
+
+ // Calculate end date
+ const endDate = debateEndTime
+ ? new Date(Number(debateEndTime) * 1000)
+ : new Date();
+
+ // Format bonding curve data
+ const bondingCurve = bondingCurveDetails
+ ? {
+ target: bondingCurveDetails[0],
+ current: bondingCurveDetails[1],
+ basePrice: bondingCurveDetails[2],
+ currentPrice: bondingCurveDetails[3],
+ isFulfilled: bondingCurveDetails[4],
+ endTime: bondingCurveDetails[5],
+ }
+ : null;
+
+ // Extract round info
+ const [roundIndex, roundStartTime, roundEndTime, isRoundComplete] =
+ roundInfo || [0n, 0n, 0n, false];
+
+ // Calculate time remaining
+ const timeRemaining = bondingCurve
+ ? Math.max(0, Number(bondingCurve.endTime) - Math.floor(Date.now() / 1000))
+ : 0;
+
+ const handleApproveToken = async (amountInWei: bigint) => {
+ if (!marketDetails || !address) return false;
+
+ try {
+ // Check if we already have sufficient allowance
+ if (currentAllowance && currentAllowance >= amountInWei) {
+ console.log("Already approved sufficient amount");
+ return true;
+ }
+
+ // If not, proceed with approval
+ approveToken({
+ address: marketDetails[0] as `0x${string}`,
+ abi: [
+ {
+ name: "approve",
+ type: "function",
+ stateMutability: "nonpayable",
+ inputs: [
+ { name: "spender", type: "address" },
+ { name: "amount", type: "uint256" },
+ ],
+ outputs: [{ type: "bool" }],
+ },
+ ],
+ functionName: "approve",
+ args: [MARKET_FACTORY_ADDRESS, amountInWei],
+ });
+
+ // Wait for approval hash
+ while (!approveHash) {
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ }
+
+ // Wait for approval confirmation
+ if (approveHash && publicClient) {
+ await waitForTransactionReceipt(publicClient as any, {
+ hash: approveHash,
+ });
+ return true;
+ }
+ } catch (error) {
+ console.error("Error in approval:", error);
+ return false;
+ }
+ return false;
+ };
+ console.log("Bonding curve", bondingCurve);
+ const handlePlaceLimitOrder = async (
+ outcomeIndex: bigint,
+ isLong: boolean
+ ) => {
+ if (!marketId || !bondingCurve) {
+ console.error("Market data not loaded");
+ return;
+ }
+
+ try {
+ setPendingTx(true);
+ console.log("Creating order for", amount, "of", isLong ? "Yes" : "No");
+ const amountInWei = BigInt(Math.floor(parseFloat(amount) * 10 ** 18));
+
+ // First approve
+ const approved = await handleApproveToken(amountInWei);
+ if (!approved) {
+ console.error("Approval failed");
+ setPendingTx(false);
+ return;
+ }
+
+ // Then place order
+ const price = isLong
+ ? bondingCurve.basePrice
+ : 10000n - bondingCurve.basePrice;
+ if (!marketId) {
+ console.error("Market ID is required");
+ return;
+ }
+ // Ensure marketId is properly converted to bigint
+ const marketIdBigInt = BigInt(marketId as string);
+
+ // Safe logging with null checks
+ console.log("Placing order with params:", {
+ marketId: marketIdBigInt,
+ outcomeIndex: outcomeIndex,
+ price: price,
+ amountInWei: amountInWei,
+ });
+
+ await placeLimitOrder({
+ address: MARKET_FACTORY_ADDRESS as `0x${string}`,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "placeLimitOrder",
+ args: [marketIdBigInt, outcomeIndex, price, amountInWei],
+ });
+ } catch (error) {
+ console.error("Error placing order:", error);
+ } finally {
+ setPendingTx(false);
+ }
+ };
+
+ const handleAmountChange = (value: string) => {
+ const numValue = parseFloat(value) || 0;
+ setAmount(value);
+ // Calculate potential return based on current price
+ if (selectedGladiator && gladiatorPrices) {
+ const price =
+ Number(gladiatorPrices[Number(selectedGladiator.index)]) / 100;
+ const return_value =
+ orderType === "buy"
+ ? numValue * (100 / price - 1)
+ : numValue * (100 / (100 - price) - 1);
+ setPotentialReturn(return_value.toFixed(2));
+ }
+ };
+
+ const adjustAmount = (delta: number) => {
+ const currentAmount = parseFloat(amount) || 0;
+ const newAmount = Math.max(0, currentAmount + delta);
+ handleAmountChange(newAmount.toString());
+ };
+
+ // Add new state for nomination modal
+ const [isNominationModalOpen, setIsNominationModalOpen] = useState(false);
+
+ // Add state for user's gladiators
+ const [userGladiators, setUserGladiators] = useState([]);
+
+ // Get all gladiators
+ const { data: allGladiators, refetch: refetchAllGladiators } =
+ useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "getAllGladiators",
+ }) as { data: Gladiator[] | undefined; refetch: () => void };
+
+ // Add contract write for nomination
+ const {
+ data: nominateHash,
+ isPending: isNominatePending,
+ writeContract: nominateGladiator,
+ } = useWriteContract();
+
+ const {
+ isLoading: isNominateConfirming,
+ isSuccess: isNominateConfirmed,
+ error: nominateTxError,
+ } = useWaitForTransactionReceipt({
+ hash: nominateHash,
+ });
+
+ useEffect(() => {
+ let toastId: any;
+ if (isNominateConfirming) {
+ console.log("Nomination is loading!");
+ toastId = toast.loading("Nomination transaction is getting confirmed..", {
+ id: "nomination-loading",
+ });
+ }
+
+ return () => {
+ if (toastId) {
+ toast.dismiss(toastId);
+ }
+ };
+ }, [isNominateConfirming]);
+
+ useEffect(() => {
+ if (isNominateConfirmed) {
+ console.log("Nomination confirmed");
+ toast.success("Nomination for your gladiator is confirmed!", {
+ id: "nomination-success",
+ });
+ }
+ }, [isNominateConfirmed]);
+
+ useEffect(() => {
+ if (nominateTxError) {
+ console.log("Error in Nomination");
+ toast.error("Error in Nomination for your gladiator!", {
+ id: "nomination-error",
+ });
+ }
+ }, [nominateTxError]);
+
+ useEffect(() => {
+ if (errorTxnOrder) {
+ console.log("Error in placing order");
+ toast.error("Some error occurred while placing order");
+ }
+ }, [errorTxnOrder]);
+
+ // Add hook for checking ownership
+ const { data: ownershipData } = useReadContract({
+ address: GLADIATOR_NFT_ADDRESS,
+ abi: GLADIATOR_NFT_ABI,
+ functionName: "ownerOf",
+ args: selectedGladiator?.tokenId
+ ? [BigInt(selectedGladiator.tokenId)]
+ : undefined,
+ });
+
+ // Effect to filter user's gladiators
+ useEffect(() => {
+ if (allGladiators && address && publicClient) {
+ const fetchUserGladiators = async () => {
+ try {
+ const userOwnedGladiators = await Promise.all(
+ allGladiators.map(async (gladiator, index) => {
+ try {
+ if (!gladiator) return null;
+
+ // Use multicall to batch ownership checks
+ const ownerData = await publicClient.readContract({
+ address: GLADIATOR_NFT_ADDRESS,
+ abi: GLADIATOR_NFT_ABI,
+ functionName: "ownerOf",
+ args: [BigInt(index + 1)], // tokenIds start from 1
+ });
+
+ const owner = ownerData as string;
+
+ if (owner && owner.toLowerCase() === address.toLowerCase()) {
+ const tokenId = index + 1;
+ return {
+ ...gladiator,
+ tokenId,
+ name: gladiator.name || `Gladiator #${tokenId}`,
+ } as Gladiator;
+ }
+ } catch (error) {
+ console.error(
+ `Error checking ownership for gladiator ${index + 1}:`,
+ error
+ );
+ }
+ return null;
+ })
+ );
+
+ const validGladiators = userOwnedGladiators.filter(
+ (g): g is Gladiator =>
+ g !== null &&
+ typeof g === "object" &&
+ "tokenId" in g &&
+ typeof g.tokenId === "number"
+ );
+
+ setUserGladiators(validGladiators);
+ } catch (error) {
+ console.error("Error fetching user gladiators:", error);
+ setUserGladiators([]);
+ }
+ };
+
+ fetchUserGladiators();
+ } else {
+ setUserGladiators([]);
+ }
+ }, [allGladiators, address, publicClient]);
+
+ const handleNominate = async (tokenId: number | undefined) => {
+ if (!marketId || !tokenId) return;
+
+ try {
+ await nominateGladiator({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "nominateGladiator",
+ args: [BigInt(tokenId), marketId],
+ });
+ setIsNominationModalOpen(false);
+ } catch (error) {
+ console.error("Error nominating gladiator:", error);
+ }
+ };
+
+ // Add index mapping
+ const indexedGladiators = useMemo(() => {
+ return (
+ gladiators?.map((g, index) => ({ ...g, index: BigInt(index) })) || []
+ );
+ }, [gladiators]);
+
+ // TODO: replace it with isActive when recieved correct from contract
+ const daysRemaining = Math.floor(timeRemaining / (24 * 60 * 60));
+ const hoursRemaining = Math.floor((timeRemaining % (24 * 60 * 60)) / 3600);
+
+ let isDummyActive = true;
+ if (daysRemaining === 0 && hoursRemaining === 0) {
+ isDummyActive = false;
+ }
+
+ return (
+ <>
+
+
+
+ {/* Debate Info Section */}
+
+
+ {/* AI Discussion */}
+
+
+ {/* Rest of the components */}
+ {isMarketDataLoading ? (
+
Loading market details...
+ ) : (
+ <>
+ {/* Bribe Submission */}
+ {/* {bondingCurve?.isFulfilled && marketId && (
+
+ )} */}
+
+ {/* Bonding Curve Progress */}
+
+
+ {/* Gladiator List */}
+
+ >
+ )}
+
+
+
+ {/* Desktop Sidebar - Visible on large screens */}
+
+
+
+
+ {/* Mobile/Tablet Drawer - Visible on smaller screens */}
+
+
+ {/* Nomination Card */}
+
+
+ {/* Add the confirmation dialog */}
+
+
+ >
+ );
+}
diff --git a/frontend/debate-ai/components/debate-details/DesktopSideDrawer.tsx b/frontend/debate-ai/components/debate-details/DesktopSideDrawer.tsx
new file mode 100644
index 0000000..e48182a
--- /dev/null
+++ b/frontend/debate-ai/components/debate-details/DesktopSideDrawer.tsx
@@ -0,0 +1,301 @@
+import React from "react";
+import { Card, CardContent } from "@/components/ui/card";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { formatEther, formatAddress } from "@/lib/utils";
+
+const BASIS_POINTS = 10000n;
+type Gladiator = {
+ aiAddress: string; // Address of the AI agent
+ name: string; // Name of the gladiator
+ index: bigint; // Index in gladiators array
+ isActive: boolean; // Whether still in competition
+ publicKey: string; // Public key for encrypted bribes
+ tokenId: number; // Token ID of the NFT - making this required instead of optional
+};
+
+type DesktopSideDrawerProps = {
+ setOrderType: React.Dispatch>;
+ indexedGladiators: Gladiator[];
+ selectedGladiator: Gladiator | null;
+ setSelectedGladiator: React.Dispatch>;
+ handleAmountChange: (value: string) => void;
+ amount: string;
+ adjustAmount: (delta: number) => void;
+ gladiatorPrices: bigint[] | undefined;
+ gladiatorVolumes: bigint[] | undefined;
+ totalVolume: bigint | undefined;
+ orderType: string;
+ potentialReturn: string;
+ isConnected: boolean;
+ isApproveConfirming: boolean;
+ isApprovePending: boolean;
+ isOrderPending: boolean;
+ isOrderConfirming: boolean;
+ handlePlaceLimitOrder: (
+ outcomeIndex: bigint,
+ isLong: boolean
+ ) => Promise;
+ isActive: boolean;
+};
+
+const DesktopSideDrawer = ({
+ setOrderType,
+ indexedGladiators,
+ selectedGladiator,
+ setSelectedGladiator,
+ handleAmountChange,
+ amount,
+ adjustAmount,
+ gladiatorPrices,
+ gladiatorVolumes,
+ totalVolume,
+ orderType,
+ potentialReturn,
+ isConnected,
+ isApproveConfirming,
+ isApprovePending,
+ isOrderPending,
+ isOrderConfirming,
+ handlePlaceLimitOrder,
+ isActive,
+}: DesktopSideDrawerProps) => {
+ const totalVolumeFormatted = formatEther(totalVolume || 0n);
+
+ return (
+
+
+
+
+
+
+ {/* Order Type Tabs */}
+
setOrderType(v as "buy" | "sell")}
+ className="w-full"
+ >
+
+
+ Buy
+
+
+ Sell
+
+
+
+
+ {/* Gladiator Selection */}
+
+
+
+ {indexedGladiators.map((gladiator, index) => {
+ if (!gladiator) return null;
+
+ return (
+
+ );
+ })}
+
+
+
+ {/* Amount Input */}
+
+
+
+
+ $
+
+
handleAmountChange(e.target.value)}
+ className="pl-6 bg-[#D1BB9E]/50 border-[#D1BB9E]/30 focus:border-[#D1BB9E] text-white focus:ring-[#D1BB9E]/20"
+ min="0"
+ step="0.1"
+ />
+
+
+
+
+
+
+
+ {/* Stats */}
+
+
+ Avg price
+
+ {selectedGladiator &&
+ gladiatorPrices &&
+ gladiatorVolumes &&
+ totalVolumeFormatted
+ ? (() => {
+ const volume = formatEther(
+ gladiatorVolumes[Number(selectedGladiator.index)]
+ );
+ const impliedProbability =
+ totalVolumeFormatted !== "0"
+ ? Number(volume) / Number(totalVolumeFormatted)
+ : Number(
+ gladiatorPrices[
+ Number(selectedGladiator.index)
+ ]
+ ) / Number(BASIS_POINTS);
+ return orderType === "sell"
+ ? `${(1 - impliedProbability).toFixed(2)}¢`
+ : `${impliedProbability.toFixed(2)}¢`;
+ })()
+ : "-"}
+
+
+
+ Shares
+
+ {selectedGladiator &&
+ gladiatorPrices &&
+ gladiatorVolumes &&
+ totalVolumeFormatted &&
+ parseFloat(amount)
+ ? (() => {
+ const volume = formatEther(
+ gladiatorVolumes[Number(selectedGladiator.index)]
+ );
+ const impliedProbability =
+ totalVolumeFormatted !== "0"
+ ? Number(volume) / Number(totalVolumeFormatted)
+ : Number(
+ gladiatorPrices[
+ Number(selectedGladiator.index)
+ ]
+ ) / Number(BASIS_POINTS);
+ const avgPrice =
+ orderType === "sell"
+ ? 1 - impliedProbability
+ : impliedProbability;
+ return (parseFloat(amount) / avgPrice).toFixed(2);
+ })()
+ : "0.00"}
+
+
+
+ Potential return
+
+ ${potentialReturn} (
+ {(
+ (parseFloat(potentialReturn) /
+ parseFloat(amount || "1")) *
+ 100 || 0
+ ).toFixed(2)}
+ %)
+
+
+
+
+ {/* Place Order Button */}
+
+
+
+ By trading, you agree to the Terms of Use.
+
+
+
+
+
+
+
+ );
+};
+
+export default DesktopSideDrawer;
diff --git a/frontend/debate-ai/components/debate-details/GladiatorList.tsx b/frontend/debate-ai/components/debate-details/GladiatorList.tsx
new file mode 100644
index 0000000..9ace83e
--- /dev/null
+++ b/frontend/debate-ai/components/debate-details/GladiatorList.tsx
@@ -0,0 +1,192 @@
+import React from "react";
+import { Card, CardContent } from "@/components/ui/card";
+import { ChevronDown, Menu } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { formatEther, formatAddress } from "@/lib/utils";
+
+const BASIS_POINTS = 10000n;
+
+type ExpandedCardStruct = {
+ aiDiscussion: boolean;
+ bondingCurve: boolean;
+ debateInfo: boolean;
+ gladiators: boolean;
+ leaderboard: boolean;
+};
+
+type Gladiator = {
+ aiAddress: string; // Address of the AI agent
+ name: string; // Name of the gladiator
+ index: bigint; // Index in gladiators array
+ isActive: boolean; // Whether still in competition
+ publicKey: string; // Public key for encrypted bribes
+ tokenId: number; // Token ID of the NFT - making this required instead of optional
+};
+
+type GladiatorListProps = {
+ expandedCards: ExpandedCardStruct;
+ setIsNominationModalOpen: React.Dispatch>;
+ isConnected: boolean;
+ indexedGladiators: Gladiator[];
+ gladiatorPrices: bigint[] | undefined;
+ gladiatorVolumes: bigint[] | undefined;
+ totalVolume: bigint | undefined;
+ handleAmountChange: (value: string) => void;
+ setAmount: React.Dispatch>;
+ setOrderType: React.Dispatch>;
+ setSelectedGladiator: React.Dispatch>;
+};
+
+const GladiatorListComp = ({
+ expandedCards,
+ setIsNominationModalOpen,
+ isConnected,
+ indexedGladiators,
+ gladiatorPrices,
+ gladiatorVolumes,
+ totalVolume,
+ handleAmountChange,
+ setAmount,
+ setOrderType,
+ setSelectedGladiator,
+}: GladiatorListProps) => {
+ return (
+
+ {" "}
+
+ {expandedCards.gladiators && (
+
+
+ {/* Header with dividers */}
+
+
+
+ Gladiator
+
+
+
Volume
+
Probability
+
+
+
+ {/* Rows */}
+
+ {indexedGladiators.map((gladiator, index) => {
+ if (!gladiator) return null;
+ const currentPrice =
+ Number(gladiatorPrices?.[Number(gladiator.index)] || 0n) /
+ Number(BASIS_POINTS);
+ const volume = gladiatorVolumes
+ ? formatEther(gladiatorVolumes[Number(gladiator.index)])
+ : "0";
+ const totalVolumeFormatted = formatEther(totalVolume || 0n);
+ const volumePercentage =
+ totalVolumeFormatted !== "0"
+ ? (
+ (Number(volume) / Number(totalVolumeFormatted)) *
+ 100
+ ).toFixed(1)
+ : currentPrice.toFixed(1);
+
+ const impliedProbability =
+ totalVolumeFormatted !== "0"
+ ? (
+ (Number(volume) / Number(totalVolumeFormatted)) *
+ 100
+ ).toFixed(1)
+ : currentPrice.toFixed(1);
+
+ const yesPrice = (Number(impliedProbability) / 100).toFixed(
+ 2
+ );
+ const noPrice = (
+ 1 -
+ Number(impliedProbability) / 100
+ ).toFixed(2);
+
+ return (
+
+
+
+
+
+
+ ${volume}
+
+
+ {volumePercentage}%
+
+
+
+
+
+ {impliedProbability}%
+
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+
+ )}
+
+
+ );
+};
+
+export default GladiatorListComp;
diff --git a/frontend/debate-ai/components/debate-details/MobileSideDrawer.tsx b/frontend/debate-ai/components/debate-details/MobileSideDrawer.tsx
new file mode 100644
index 0000000..cef0444
--- /dev/null
+++ b/frontend/debate-ai/components/debate-details/MobileSideDrawer.tsx
@@ -0,0 +1,321 @@
+import React from "react";
+
+import { Card, CardContent } from "@/components/ui/card";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { formatEther, formatAddress } from "@/lib/utils";
+import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
+import { ChevronDown, Menu } from "lucide-react";
+
+const BASIS_POINTS = 10000n;
+type Gladiator = {
+ aiAddress: string; // Address of the AI agent
+ name: string; // Name of the gladiator
+ index: bigint; // Index in gladiators array
+ isActive: boolean; // Whether still in competition
+ publicKey: string; // Public key for encrypted bribes
+ tokenId: number; // Token ID of the NFT - making this required instead of optional
+};
+
+type MobileSideDrawerProps = {
+ setOrderType: React.Dispatch>;
+ indexedGladiators: Gladiator[];
+ selectedGladiator: Gladiator | null;
+ setSelectedGladiator: React.Dispatch>;
+ handleAmountChange: (value: string) => void;
+ amount: string;
+ adjustAmount: (delta: number) => void;
+ gladiatorPrices: bigint[] | undefined;
+ gladiatorVolumes: bigint[] | undefined;
+ totalVolume: bigint | undefined;
+ orderType: string;
+ potentialReturn: string;
+ isConnected: boolean;
+ isApproveConfirming: boolean;
+ isApprovePending: boolean;
+ isOrderPending: boolean;
+ isOrderConfirming: boolean;
+ handlePlaceLimitOrder: (
+ outcomeIndex: bigint,
+ isLong: boolean
+ ) => Promise;
+ isActive: boolean;
+};
+
+const MobileSideDrawer = ({
+ setOrderType,
+ indexedGladiators,
+ selectedGladiator,
+ setSelectedGladiator,
+ handleAmountChange,
+ amount,
+ adjustAmount,
+ gladiatorPrices,
+ gladiatorVolumes,
+ totalVolume,
+ orderType,
+ potentialReturn,
+ isConnected,
+ isApproveConfirming,
+ isApprovePending,
+ isOrderPending,
+ isOrderConfirming,
+ handlePlaceLimitOrder,
+ isActive,
+}: MobileSideDrawerProps) => {
+ const totalVolumeFormatted = formatEther(totalVolume || 0n);
+ return (
+
+ {/*
*/}
+
+
+
+
+
+
+
+
+
+
+ {/* Order Type Tabs */}
+
setOrderType(v as "buy" | "sell")}
+ className="w-full"
+ >
+
+
+ Buy
+
+
+ Sell
+
+
+
+
+ {/* Gladiator Selection */}
+
+
+
+ {indexedGladiators.map((gladiator, index) => {
+ if (!gladiator) return null;
+ return (
+
+ );
+ })}
+
+
+
+ {/* Amount Input */}
+
+
+
+
+ $
+
+
handleAmountChange(e.target.value)}
+ className="pl-6 bg-[#D1BB9E]/50 border-[#D1BB9E]/30 focus:border-[#D1BB9E] text-white focus:ring-[#D1BB9E]/20"
+ min="0"
+ step="0.1"
+ />
+
+
+
+
+
+
+
+ {/* Stats */}
+
+
+ Avg price
+
+ {selectedGladiator &&
+ gladiatorPrices &&
+ gladiatorVolumes &&
+ totalVolumeFormatted
+ ? (() => {
+ const volume = formatEther(
+ gladiatorVolumes[
+ Number(selectedGladiator.index)
+ ]
+ );
+ const impliedProbability =
+ totalVolumeFormatted !== "0"
+ ? Number(volume) /
+ Number(totalVolumeFormatted)
+ : Number(
+ gladiatorPrices[
+ Number(selectedGladiator.index)
+ ]
+ ) / Number(BASIS_POINTS);
+ return orderType === "sell"
+ ? `${impliedProbability.toFixed(2)}¢`
+ : `${(1 - impliedProbability).toFixed(2)}¢`;
+ })()
+ : "-"}
+
+
+
+ Shares
+
+ {selectedGladiator &&
+ gladiatorPrices &&
+ gladiatorVolumes &&
+ totalVolumeFormatted &&
+ parseFloat(amount)
+ ? (() => {
+ const volume = formatEther(
+ gladiatorVolumes[
+ Number(selectedGladiator.index)
+ ]
+ );
+ const impliedProbability =
+ totalVolumeFormatted !== "0"
+ ? Number(volume) /
+ Number(totalVolumeFormatted)
+ : Number(
+ gladiatorPrices[
+ Number(selectedGladiator.index)
+ ]
+ ) / Number(BASIS_POINTS);
+ const avgPrice =
+ orderType === "sell"
+ ? impliedProbability
+ : 1 - impliedProbability;
+ return (parseFloat(amount) / avgPrice).toFixed(
+ 2
+ );
+ })()
+ : "0.00"}
+
+
+
+ Potential return
+
+ ${potentialReturn} (
+ {(
+ (parseFloat(potentialReturn) /
+ parseFloat(amount || "1")) *
+ 100 || 0
+ ).toFixed(2)}
+ %)
+
+
+
+
+ {/* Place Order Button */}
+
+
+ By trading, you agree to the Terms of Use.
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default MobileSideDrawer;
diff --git a/frontend/debate-ai/components/debate-details/NominateGladiatorCard.tsx b/frontend/debate-ai/components/debate-details/NominateGladiatorCard.tsx
new file mode 100644
index 0000000..c011dc1
--- /dev/null
+++ b/frontend/debate-ai/components/debate-details/NominateGladiatorCard.tsx
@@ -0,0 +1,156 @@
+"use client";
+
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import {
+ useAccount,
+ useReadContract,
+ useReadContracts,
+ useWriteContract,
+ useWaitForTransactionReceipt,
+} from "wagmi";
+import {
+ MARKET_FACTORY_ADDRESS,
+ MARKET_FACTORY_ABI,
+ GLADIATOR_NFT_ADDRESS,
+ GLADIATOR_NFT_ABI,
+} from "@/config/contracts";
+import { formatAddress } from "@/lib/utils";
+import { Sword } from "lucide-react";
+
+type Gladiator = {
+ aiAddress: string;
+ name: string;
+ index: bigint;
+ isActive: boolean;
+ publicKey: string;
+};
+
+interface NominateGladiatorCardProps {
+ marketId: bigint | undefined;
+ isBondingCurveFulfilled: boolean;
+}
+
+export function NominateGladiatorCard({
+ marketId,
+ isBondingCurveFulfilled,
+}: NominateGladiatorCardProps) {
+ const { isConnected, address } = useAccount();
+
+ // Get user's NFTs
+ const { data: totalSupply } = useReadContract({
+ address: GLADIATOR_NFT_ADDRESS,
+ abi: GLADIATOR_NFT_ABI,
+ functionName: "totalSupply",
+ });
+
+ const tokenIndices = totalSupply
+ ? Array.from({ length: Number(totalSupply) }, (_, i) => i)
+ : [];
+
+ // Get owner of each token
+ const { data: tokenOwners } = useReadContracts({
+ contracts: tokenIndices.map((index) => ({
+ address: GLADIATOR_NFT_ADDRESS,
+ abi: GLADIATOR_NFT_ABI as any,
+ functionName: "ownerOf",
+ args: [BigInt(index + 1)],
+ })),
+ });
+
+ // Get gladiator details for owned NFTs
+ const { data: myGladiators } = useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "getAllGladiators",
+ }) as { data: Gladiator[] };
+
+ // Nominate gladiator transaction
+ const {
+ data: nominateHash,
+ isPending: isNominatePending,
+ writeContract: nominateGladiator,
+ } = useWriteContract();
+
+ const { isLoading: isNominateConfirming } = useWaitForTransactionReceipt({
+ hash: nominateHash,
+ });
+
+ // Filter gladiators owned by the current user
+ const ownedGladiators =
+ myGladiators?.filter((_, index) => {
+ const ownerResult = tokenOwners?.[index];
+ return ownerResult?.result === address;
+ }) || [];
+
+ const handleNominate = async (tokenId: bigint) => {
+ if (!marketId) return;
+
+ try {
+ await nominateGladiator({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "nominateGladiator",
+ args: [marketId, tokenId],
+ });
+ } catch (error) {
+ console.error("Error nominating gladiator:", error);
+ }
+ };
+
+ // Don't show the card if conditions aren't met
+ if (isBondingCurveFulfilled || !isConnected || ownedGladiators.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+ Nominate Your Gladiator
+
+
+
+
+ As an NFT holder, you can nominate your gladiator to participate in
+ this debate.
+
+
+
+ {ownedGladiators.map((gladiator, index) => (
+
+
+
{gladiator.name}
+
+ {formatAddress(gladiator.aiAddress)}
+
+
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/debate-ai/components/debate-details/NominationCard.tsx b/frontend/debate-ai/components/debate-details/NominationCard.tsx
new file mode 100644
index 0000000..303cca0
--- /dev/null
+++ b/frontend/debate-ai/components/debate-details/NominationCard.tsx
@@ -0,0 +1,123 @@
+import React from "react";
+import { NominateGladiatorCard } from "./NominateGladiatorCard";
+import { Dialog, DialogContent } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+
+type BondingCurveStruct = {
+ target: bigint; // Target amount to reach
+ current: bigint; // Current amount raised
+ basePrice: bigint; // Starting price
+ currentPrice: bigint; // Current price
+ isFulfilled: boolean; // Whether target is reached
+ endTime: bigint; // When bonding period ends
+};
+
+type Gladiator = {
+ aiAddress: string; // Address of the AI agent
+ name: string; // Name of the gladiator
+ index: bigint; // Index in gladiators array
+ isActive: boolean; // Whether still in competition
+ publicKey: string; // Public key for encrypted bribes
+ tokenId: number; // Token ID of the NFT - making this required instead of optional
+};
+
+type NominationCardProps = {
+ bondingCurve: BondingCurveStruct | null;
+ marketId: unknown;
+ isApproveConfirming: boolean;
+ isApprovePending: boolean;
+ isOrderPending: boolean;
+ isOrderConfirming: boolean;
+ isNominationModalOpen: boolean;
+ setIsNominationModalOpen: React.Dispatch
>;
+ userGladiators: Gladiator[];
+ handleNominate: (tokenId: number | undefined) => void;
+ isActive: boolean;
+};
+
+const NominationCard = ({
+ bondingCurve,
+ marketId,
+ isApprovePending,
+ isOrderPending,
+ isApproveConfirming,
+ isOrderConfirming,
+ isNominationModalOpen,
+ setIsNominationModalOpen,
+ userGladiators,
+ handleNominate,
+ isActive,
+}: NominationCardProps) => {
+ return (
+
+
+
+ {/* Nomination Modal */}
+
+
+ );
+};
+
+export default NominationCard;
diff --git a/frontend/debate-ai/components/gladiators-grid.tsx b/frontend/debate-ai/components/gladiators-grid.tsx
deleted file mode 100644
index 7d6e0bf..0000000
--- a/frontend/debate-ai/components/gladiators-grid.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Abi } from "viem";
-import { useReadContract } from "wagmi";
-import { MARKET_FACTORY_ABI, MARKET_FACTORY_ADDRESS } from "@/config/contracts";
-import { GladiatorCard } from "./GladiatorCard"
-import { useRouter } from 'next/navigation';
-
-export function GladiatorsGrid() {
- interface Gladiator {
- aiAddress: string;
- name: string;
- isActive: boolean;
- publicKey: string;
- }
-
- const router = useRouter();
-
- const { data: gladiators } = useReadContract({
- address: MARKET_FACTORY_ADDRESS,
- abi: MARKET_FACTORY_ABI,
- functionName: 'getAllGladiators',
- }) as { data: Gladiator[] };
-
- if (!gladiators) {
- return Loading gladiators...
;
- }
-
- console.log("Gladiators", gladiators);
-
- return (
-
-
Gladiators
-
- {gladiators.map((gladiator: Gladiator) => (
-
- ))}
-
-
- )
-}
diff --git a/frontend/debate-ai/components/gladiators/GladiatorCard.tsx b/frontend/debate-ai/components/gladiators/GladiatorCard.tsx
new file mode 100644
index 0000000..133dab0
--- /dev/null
+++ b/frontend/debate-ai/components/gladiators/GladiatorCard.tsx
@@ -0,0 +1,138 @@
+import Image from "next/image";
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+} from "@/components/ui/card";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Badge } from "@/components/ui/badge";
+import { ChevronRight, Shield, Sword } from "lucide-react";
+import { Button } from "../ui/button";
+
+interface GladiatorProps {
+ id: string;
+ name: string;
+ image: string;
+ wins: number;
+ losses: number;
+ speciality: string;
+ level: number;
+}
+
+interface Gladiator {
+ aiAddress: string;
+ name: string;
+ isActive: boolean;
+ publicKey: string;
+}
+
+export function GladiatorCard({
+ aiAddress,
+ name,
+ isActive,
+ publicKey,
+}: Gladiator) {
+ return (
+
+ );
+}
diff --git a/frontend/debate-ai/components/gladiators/gladiators-grid.tsx b/frontend/debate-ai/components/gladiators/gladiators-grid.tsx
new file mode 100644
index 0000000..e0462d1
--- /dev/null
+++ b/frontend/debate-ai/components/gladiators/gladiators-grid.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import { useReadContract } from "wagmi";
+import { MARKET_FACTORY_ABI, MARKET_FACTORY_ADDRESS } from "@/config/contracts";
+import { GladiatorCard } from "./GladiatorCard";
+
+import { Sword } from "lucide-react";
+import Image from "next/image";
+
+export function GladiatorsGrid() {
+ interface Gladiator {
+ aiAddress: string;
+ name: string;
+ isActive: boolean;
+ publicKey: string;
+ }
+
+ const { data: gladiators } = useReadContract({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI,
+ functionName: "getAllGladiators",
+ }) as { data: Gladiator[] };
+
+ return (
+
+
+ {/* Header Section */}
+
+
+
+
+
+
+
+
+ ARENA OF GLADIATORS
+
+
+ Witness the finest warriors in our digital colosseum. Each
+ gladiator brings unique skills and strategies to the arena.
+
+
+
+
+
+
+
+
+
+ {/* Grid Section */}
+
+
+ {gladiators?.map((gladiator: Gladiator) => (
+
+ ))}
+
+
+ {/* Empty State */}
+ {gladiators?.length === 0 && (
+
+
+
+ No Gladiators Found
+
+
+ The arena awaits its first warriors.
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/debate-ai/components/DebateList.tsx b/frontend/debate-ai/components/home-page/DebateList.tsx
similarity index 64%
rename from frontend/debate-ai/components/DebateList.tsx
rename to frontend/debate-ai/components/home-page/DebateList.tsx
index 610a554..014e529 100644
--- a/frontend/debate-ai/components/DebateList.tsx
+++ b/frontend/debate-ai/components/home-page/DebateList.tsx
@@ -6,10 +6,10 @@ import {
MARKET_FACTORY_ADDRESS,
MARKET_FACTORY_ABI,
} from "@/config/contracts";
-import { Card } from "./ui/card";
+import { Card } from "../ui/card";
import { formatAddress } from "@/lib/utils";
-import { Button } from "./ui/button";
-import { ChatWindow } from "./MiniChatWindow";
+import { Button } from "../ui/button";
+import { ChatWindow } from "../common/MiniChatWindow";
import { type Abi } from "viem";
import {
Award,
@@ -20,6 +20,16 @@ import {
Users,
} from "lucide-react";
+// Type for how bonding curve is returned from contract
+type BondingCurve = [
+ bigint, // target
+ bigint, // current
+ bigint, // basePrice
+ bigint, // currentPrice
+ boolean, // isFulfilled
+ bigint, // endTime
+];
+
type DebateDetails = [
string, // topic
bigint, // startTime
@@ -32,7 +42,7 @@ type DebateDetails = [
string, // market
string[], // judges
boolean, // hasOutcome
- bigint // finalOutcome
+ bigint, // finalOutcome
];
interface DebateWithId {
@@ -83,7 +93,7 @@ export function DebateList() {
window.location.href = `/debate/${debateId}`;
};
- // Combine all data and sort
+ // Combined all data and sort
const debates: DebateWithId[] = [];
debateIds?.forEach((id, index) => {
const details = debateDetails?.[index]?.result as DebateDetails | undefined;
@@ -102,10 +112,31 @@ export function DebateList() {
setVisibleItems((prev) => prev + 6);
};
+ const { data: bondingCurveDetailsList } = useReadContracts({
+ contracts: (marketIdsData || []).map((marketIdData) => ({
+ address: MARKET_FACTORY_ADDRESS,
+ abi: MARKET_FACTORY_ABI as unknown as Abi,
+ functionName: "getBondingCurveDetails",
+ args: marketIdData?.result ? [marketIdData.result] : undefined,
+ })),
+ });
+
+ const bondingCurveMap = new Map();
+
+ (marketIdsData || []).forEach((marketIdData, index) => {
+ const marketId = marketIdData.result?.toString();
+ const details = bondingCurveDetailsList?.[index]?.result as
+ | BondingCurve
+ | undefined;
+ if (marketId && details) {
+ bondingCurveMap.set(marketId, details);
+ }
+ });
+
return (
- {visibleDebates.map(({ id: debateId, details, marketId }) => {
+ {visibleDebates.map(({ id: debateId, details, marketId }, index) => {
if (!details) return null;
const [
@@ -119,15 +150,36 @@ export function DebateList() {
finalOutcome,
] = details;
+ // Fetch bonding curve details
+ const bondingCurveDetails = bondingCurveMap.get(marketId.toString());
+
+ const isFulfilled = bondingCurveDetails
+ ? bondingCurveDetails[4]
+ : false;
+
+ const endTime = bondingCurveDetails ? bondingCurveDetails[5] : 0n;
+
+ const timeRemaining = endTime
+ ? Math.max(0, Number(endTime) - Math.floor(Date.now() / 1000))
+ : 0;
+
+ const daysRemaining = Math.floor(timeRemaining / (24 * 60 * 60));
+ const hoursRemaining = Math.floor(
+ (timeRemaining % (24 * 60 * 60)) / 3600
+ );
+
+ let isDummyActive = true;
+ if (daysRemaining === 0 && hoursRemaining === 0) {
+ isDummyActive = false;
+ }
+
return (
@@ -145,15 +197,15 @@ export function DebateList() {
- {isActive && (
+ {isDummyActive && (
)}
- {isActive ? "Active" : "Completed"}
+ {isDummyActive ? "Active" : "Completed"}
@@ -172,7 +224,37 @@ export function DebateList() {
-
+ {!isFulfilled ? (
+
+
+
+
+ AI Agents are waiting to start
+
+
+ Once the bonding curve target is reached, three expert
+ AI agents will begin analyzing and debating this topic
+ in real-time.
+
+
+
+ Progress:{" "}
+ {bondingCurveDetails
+ ? (
+ (Number(bondingCurveDetails[1]) * 100) /
+ Number(bondingCurveDetails[0])
+ ).toFixed(1)
+ : "0"}
+ % to unlock
+
+
+
+
+ ) : (
+
+ )}
diff --git a/frontend/debate-ai/components/HomeCenter.tsx b/frontend/debate-ai/components/home-page/HomeCenter.tsx
similarity index 81%
rename from frontend/debate-ai/components/HomeCenter.tsx
rename to frontend/debate-ai/components/home-page/HomeCenter.tsx
index 973a843..50bb5b7 100644
--- a/frontend/debate-ai/components/HomeCenter.tsx
+++ b/frontend/debate-ai/components/home-page/HomeCenter.tsx
@@ -1,17 +1,17 @@
import React from "react";
import Image from "next/image";
-import PixelCard from "../components/PixelCard";
+import PixelCard from "../common/PixelCard";
const HomeCenter = () => {
return (
<>
-
-
+
+
@@ -57,12 +57,12 @@ const HomeCenter = () => {
-
+
diff --git a/frontend/debate-ai/components/ui/drawer.tsx b/frontend/debate-ai/components/ui/drawer.tsx
new file mode 100644
index 0000000..5dad120
--- /dev/null
+++ b/frontend/debate-ai/components/ui/drawer.tsx
@@ -0,0 +1,116 @@
+import * as React from "react";
+import { Drawer as DrawerPrimitive } from "vaul";
+
+import { cn } from "@/lib/utils";
+
+const Drawer = ({
+ shouldScaleBackground = true,
+ ...props
+}: React.ComponentProps
) => (
+
+);
+Drawer.displayName = "Drawer";
+
+const DrawerTrigger = DrawerPrimitive.Trigger;
+
+const DrawerPortal = DrawerPrimitive.Portal;
+
+const DrawerClose = DrawerPrimitive.Close;
+
+const DrawerOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
+
+const DrawerContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+));
+DrawerContent.displayName = "DrawerContent";
+
+const DrawerHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DrawerHeader.displayName = "DrawerHeader";
+
+const DrawerFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DrawerFooter.displayName = "DrawerFooter";
+
+const DrawerTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
+
+const DrawerDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
+
+export {
+ Drawer,
+ DrawerPortal,
+ DrawerOverlay,
+ DrawerTrigger,
+ DrawerClose,
+ DrawerContent,
+ DrawerHeader,
+ DrawerFooter,
+ DrawerTitle,
+ DrawerDescription,
+};
diff --git a/frontend/debate-ai/components/wagmi.tsx b/frontend/debate-ai/components/wagmi.tsx
new file mode 100644
index 0000000..3276a12
--- /dev/null
+++ b/frontend/debate-ai/components/wagmi.tsx
@@ -0,0 +1,30 @@
+import { getDefaultConfig } from "@rainbow-me/rainbowkit";
+import {
+ arbitrum,
+ base,
+ mainnet,
+ optimism,
+ polygon,
+ sepolia,
+ holesky,
+} from "wagmi/chains";
+
+if (!process.env.NEXT_PUBLIC_RAINBOWKIT_PROJECT_ID) {
+ throw new Error("RAINBOWKIT_PROJECT_ID is not set");
+}
+export const rainbowKitProjectId =
+ process.env.NEXT_PUBLIC_RAINBOWKIT_PROJECT_ID;
+export const config = getDefaultConfig({
+ appName: "RainbowKit demo",
+ projectId: rainbowKitProjectId,
+ chains: [
+ mainnet,
+ polygon,
+ optimism,
+ arbitrum,
+ base,
+ holesky,
+ ...(process.env.NEXT_PUBLIC_ENABLE_TESTNETS === "true" ? [sepolia] : []),
+ ],
+ ssr: true,
+});
diff --git a/frontend/debate-ai/package.json b/frontend/debate-ai/package.json
index 55e9aa4..456fcc3 100644
--- a/frontend/debate-ai/package.json
+++ b/frontend/debate-ai/package.json
@@ -31,14 +31,18 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"ethers": "5.7.2",
+ "framer-motion": "^12.4.1",
"lucide-react": "^0.469.0",
"next": "14.2.16",
"openai-edge": "^1.2.2",
+ "pino-pretty": "^13.0.0",
"react": "^18",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18",
+ "sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
+ "vaul": "^1.1.2",
"viem": "^2.21.59",
"wagmi": "^2.14.6"
},
diff --git a/frontend/debate-ai/public/glad_left.webp b/frontend/debate-ai/public/glad_left.webp
new file mode 100644
index 0000000..a551585
Binary files /dev/null and b/frontend/debate-ai/public/glad_left.webp differ
diff --git a/frontend/debate-ai/public/glad_right.webp b/frontend/debate-ai/public/glad_right.webp
new file mode 100644
index 0000000..4f28a9f
Binary files /dev/null and b/frontend/debate-ai/public/glad_right.webp differ
diff --git a/frontend/debate-ai/public/gladiator_1.jpg b/frontend/debate-ai/public/gladiator_1.jpg
new file mode 100644
index 0000000..5a05e3a
Binary files /dev/null and b/frontend/debate-ai/public/gladiator_1.jpg differ
diff --git a/frontend/debate-ai/public/julius_with_a_gun.webp b/frontend/debate-ai/public/julius_with_a_gun.webp
new file mode 100644
index 0000000..20b4500
Binary files /dev/null and b/frontend/debate-ai/public/julius_with_a_gun.webp differ
diff --git a/frontend/debate-ai/public/rock_container.webp b/frontend/debate-ai/public/rock_container.webp
new file mode 100644
index 0000000..5917f34
Binary files /dev/null and b/frontend/debate-ai/public/rock_container.webp differ
diff --git a/frontend/debate-ai/public/side_1.webp b/frontend/debate-ai/public/side_1.webp
new file mode 100644
index 0000000..f3e3096
Binary files /dev/null and b/frontend/debate-ai/public/side_1.webp differ
diff --git a/frontend/debate-ai/public/side_2.webp b/frontend/debate-ai/public/side_2.webp
new file mode 100644
index 0000000..6e0497f
Binary files /dev/null and b/frontend/debate-ai/public/side_2.webp differ
diff --git a/frontend/debate-ai/public/swords.webp b/frontend/debate-ai/public/swords.webp
new file mode 100644
index 0000000..f3137ac
Binary files /dev/null and b/frontend/debate-ai/public/swords.webp differ
diff --git a/frontend/debate-ai/tailwind.config.ts b/frontend/debate-ai/tailwind.config.ts
index ebc9f38..1c63261 100644
--- a/frontend/debate-ai/tailwind.config.ts
+++ b/frontend/debate-ai/tailwind.config.ts
@@ -1,62 +1,62 @@
import type { Config } from "tailwindcss";
const config: Config = {
- darkMode: ["class"],
- content: [
+ darkMode: ["class"],
+ content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
- extend: {
- colors: {
- background: 'hsl(var(--background))',
- foreground: 'hsl(var(--foreground))',
- card: {
- DEFAULT: 'hsl(var(--card))',
- foreground: 'hsl(var(--card-foreground))'
- },
- popover: {
- DEFAULT: 'hsl(var(--popover))',
- foreground: 'hsl(var(--popover-foreground))'
- },
- primary: {
- DEFAULT: 'hsl(var(--primary))',
- foreground: 'hsl(var(--primary-foreground))'
- },
- secondary: {
- DEFAULT: 'hsl(var(--secondary))',
- foreground: 'hsl(var(--secondary-foreground))'
- },
- muted: {
- DEFAULT: 'hsl(var(--muted))',
- foreground: 'hsl(var(--muted-foreground))'
- },
- accent: {
- DEFAULT: 'hsl(var(--accent))',
- foreground: 'hsl(var(--accent-foreground))'
- },
- destructive: {
- DEFAULT: 'hsl(var(--destructive))',
- foreground: 'hsl(var(--destructive-foreground))'
- },
- border: 'hsl(var(--border))',
- input: 'hsl(var(--input))',
- ring: 'hsl(var(--ring))',
- chart: {
- '1': 'hsl(var(--chart-1))',
- '2': 'hsl(var(--chart-2))',
- '3': 'hsl(var(--chart-3))',
- '4': 'hsl(var(--chart-4))',
- '5': 'hsl(var(--chart-5))'
- }
- },
- borderRadius: {
- lg: 'var(--radius)',
- md: 'calc(var(--radius) - 2px)',
- sm: 'calc(var(--radius) - 4px)'
- }
- }
+ extend: {
+ colors: {
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ chart: {
+ "1": "hsl(var(--chart-1))",
+ "2": "hsl(var(--chart-2))",
+ "3": "hsl(var(--chart-3))",
+ "4": "hsl(var(--chart-4))",
+ "5": "hsl(var(--chart-5))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ },
},
plugins: [require("tailwindcss-animate")],
};