Initial commit with 🏗️ create-eth @ 2.0.4

This commit is contained in:
han
2026-01-23 20:20:58 +07:00
commit b330aba2b4
185 changed files with 36981 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
import { TimeLeft } from "./TimeLeft";
import { formatEther } from "viem";
import { ChevronRightIcon } from "@heroicons/react/24/outline";
import { useScaffoldReadContract } from "~~/hooks/scaffold-eth";
import { useChallengeState } from "~~/services/store/challengeStore";
export const AssertedRow = ({ assertionId, state }: { assertionId: number; state: number }) => {
const { openAssertionModal } = useChallengeState();
const { data: assertionData } = useScaffoldReadContract({
contractName: "OptimisticOracle",
functionName: "getAssertion",
args: [BigInt(assertionId)],
});
if (!assertionData) return null;
return (
<tr
key={assertionId}
onClick={() => {
openAssertionModal({ ...assertionData, assertionId, state });
}}
className={`group border-b border-base-300 cursor-pointer`}
>
{/* Description Column */}
<td>
<div className="group-hover:text-error">{assertionData.description}</div>
</td>
{/* Bond Column */}
<td>{formatEther(assertionData.bond)} ETH</td>
{/* Reward Column */}
<td>{formatEther(assertionData.reward)} ETH</td>
{/* Time Left Column */}
<td>
<TimeLeft startTime={assertionData.startTime} endTime={assertionData.endTime} />
</td>
{/* Chevron Column */}
<td>
<div className="w-6 h-6 rounded-full border-error border flex items-center justify-center hover:bg-base-200 group-hover:bg-error transition-colors mx-auto">
<ChevronRightIcon className="w-4 h-4 text-error group-hover:text-white stroke-2 transition-colors" />
</div>
</td>
</tr>
);
};

View File

@@ -0,0 +1,34 @@
"use client";
import { OOTableProps } from "../types";
import { AssertedRow } from "./AssertedRow";
import { EmptyRow } from "./EmptyRow";
export const AssertedTable = ({ assertions }: OOTableProps) => {
return (
<div className="bg-base-100 rounded-lg shadow-lg overflow-x-auto">
<table className="w-full table-auto [&_th]:px-6 [&_th]:py-4 [&_td]:px-6 [&_td]:py-4">
{/* Header */}
<thead>
<tr className="bg-base-300">
<th className="text-left font-semibold w-5/12">Description</th>
<th className="text-left font-semibold w-2/12">Bond</th>
<th className="text-left font-semibold w-2/12">Reward</th>
<th className="text-left font-semibold w-2/12">Time Left</th>
<th className="text-left font-semibold w-1/12">{/* Icon column */}</th>
</tr>
</thead>
<tbody>
{assertions.length > 0 ? (
assertions.map(assertion => (
<AssertedRow key={assertion.assertionId} assertionId={assertion.assertionId} state={assertion.state} />
))
) : (
<EmptyRow colspan={5} />
)}
</tbody>
</table>
</div>
);
};

View File

@@ -0,0 +1,260 @@
"use client";
import { useState } from "react";
import { AssertionWithIdAndState } from "../types";
import { Address } from "@scaffold-ui/components";
import { formatEther } from "viem";
import { useScaffoldWriteContract } from "~~/hooks/scaffold-eth";
import { useChallengeState } from "~~/services/store/challengeStore";
import { ZERO_ADDRESS } from "~~/utils/scaffold-eth/common";
const getStateName = (state: number) => {
switch (state) {
case 0:
return "Invalid";
case 1:
return "Asserted";
case 2:
return "Proposed";
case 3:
return "Disputed";
case 4:
return "Settled";
case 5:
return "Expired";
default:
return "Invalid";
}
};
// Helper function to format timestamp to UTC
const formatTimestamp = (timestamp: bigint | string | number) => {
const timestampNumber = Number(timestamp);
const date = new Date(timestampNumber * 1000); // Convert from seconds to milliseconds
return date.toLocaleString();
};
const Description = ({ assertion }: { assertion: AssertionWithIdAndState }) => {
return (
<div className="bg-base-200 p-4 rounded-lg space-y-2 mb-4">
<div>
<span className="font-bold">AssertionId:</span> {assertion.assertionId}
</div>
<div>
<span className="font-bold">Description:</span> {assertion.description}
</div>
<div>
<span className="font-bold">Bond:</span> {formatEther(assertion.bond)} ETH
</div>
<div>
<span className="font-bold">Reward:</span> {formatEther(assertion.reward)} ETH
</div>
<div>
<span className="font-bold">Start Time:</span>
<span className="text-sm"> UTC: {formatTimestamp(assertion.startTime)}</span>
<span className="text-sm"> Timestamp: {assertion.startTime}</span>
</div>
<div>
<span className="font-bold">End Time:</span>
<span className="text-sm"> UTC: {formatTimestamp(assertion.endTime)}</span>
<span className="text-sm"> Timestamp: {assertion.endTime}</span>
</div>
{assertion.proposer !== ZERO_ADDRESS && (
<div>
<span className="font-bold">Proposed Outcome:</span> {assertion.proposedOutcome ? "True" : "False"}
</div>
)}
{assertion.proposer !== ZERO_ADDRESS && (
<div>
<span className="font-bold">Proposer:</span>{" "}
<Address address={assertion.proposer} format="short" onlyEnsOrAddress disableAddressLink size="sm" />
</div>
)}
{assertion.disputer !== ZERO_ADDRESS && (
<div>
<span className="font-bold">Disputer:</span>{" "}
<Address address={assertion.disputer} format="short" onlyEnsOrAddress disableAddressLink size="sm" />
</div>
)}
</div>
);
};
export const AssertionModal = () => {
const [isActionPending, setIsActionPending] = useState(false);
const { refetchAssertionStates, openAssertion, closeAssertionModal } = useChallengeState();
const isOpen = !!openAssertion;
const { writeContractAsync: writeOOContractAsync } = useScaffoldWriteContract({
contractName: "OptimisticOracle",
});
const { writeContractAsync: writeDeciderContractAsync } = useScaffoldWriteContract({
contractName: "Decider",
});
const handleAction = async (args: any) => {
if (!openAssertion) return;
try {
setIsActionPending(true);
if (args.functionName === "settleDispute") {
await writeDeciderContractAsync(args);
} else {
await writeOOContractAsync(args);
}
refetchAssertionStates();
closeAssertionModal();
} catch (error) {
console.log(error);
} finally {
setIsActionPending(false);
}
};
if (!openAssertion) return null;
return (
<>
<input type="checkbox" id="challenge-modal" className="modal-toggle" checked={isOpen} readOnly />
<label htmlFor="challenge-modal" className="modal cursor-pointer" onClick={closeAssertionModal}>
<label
className="modal-box relative max-w-2xl w-full bg-base-100"
htmlFor=""
onClick={e => e.stopPropagation()}
>
{/* dummy input to capture event onclick on modal box */}
<input className="h-0 w-0 absolute top-0 left-0" />
{/* Close button */}
<button onClick={closeAssertionModal} className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
</button>
{/* Modal Content */}
<div className="">
{/* Header with Current State */}
<div className="text-center mb-6">
<h2 className="text-lg">
Current State: <span className="font-bold">{getStateName(openAssertion.state)}</span>
</h2>
</div>
<Description assertion={openAssertion} />
{openAssertion.state === 1 && (
<>
{/* Proposed Outcome Section */}
<div className="rounded-lg p-4">
<div className="flex justify-center mb-4">
<span className="font-medium">Propose Outcome</span>
</div>
{isActionPending && <span className="loading loading-spinner loading-xs"></span>}
<div className="flex justify-center gap-4">
<button
className="btn btn-primary flex-1"
onClick={() =>
handleAction({
functionName: "proposeOutcome",
args: [BigInt(openAssertion.assertionId), true],
value: openAssertion.bond,
})
}
disabled={isActionPending}
>
True
</button>
<button
className="btn btn-primary flex-1"
onClick={() =>
handleAction({
functionName: "proposeOutcome",
args: [BigInt(openAssertion.assertionId), false],
value: openAssertion.bond,
})
}
disabled={isActionPending}
>
False
</button>
</div>
</div>
</>
)}
{openAssertion.state === 2 && (
<div className="rounded-lg p-4">
<div className="flex justify-center mb-4">
<span className="font-medium">Submit Dispute</span>
</div>
{isActionPending && <span className="loading loading-spinner loading-xs"></span>}
<div className="flex justify-center gap-4">
<button
className="btn btn-primary flex-1"
onClick={() =>
handleAction({
functionName: "disputeOutcome",
args: [BigInt(openAssertion.assertionId)],
value: openAssertion.bond,
})
}
disabled={isActionPending}
>
{!openAssertion.proposedOutcome ? "True" : "False"}
</button>
</div>
</div>
)}
{openAssertion.state === 3 && (
<div className="rounded-lg p-4">
<div className="flex flex-col items-center gap-2 mb-4">
<span className="text-2xl font-medium">Impersonate Decider</span>
<span className="font-medium">Resolve Answer to</span>
</div>
{isActionPending && <span className="loading loading-spinner loading-xs"></span>}
<div className="flex justify-center gap-4">
<button
className="btn btn-primary flex-1"
onClick={() =>
handleAction({
functionName: "settleDispute",
args: [BigInt(openAssertion.assertionId), true],
})
}
disabled={isActionPending}
>
True
</button>
<button
className="btn btn-primary flex-1"
onClick={() =>
handleAction({
functionName: "settleDispute",
args: [BigInt(openAssertion.assertionId), false],
})
}
disabled={isActionPending}
>
False
</button>
</div>
</div>
)}
</div>
</label>
</label>
</>
);
};

View File

@@ -0,0 +1,48 @@
import { Address } from "@scaffold-ui/components";
import { ChevronRightIcon } from "@heroicons/react/24/outline";
import { useScaffoldReadContract } from "~~/hooks/scaffold-eth";
import { useChallengeState } from "~~/services/store/challengeStore";
export const DisputedRow = ({ assertionId, state }: { assertionId: number; state: number }) => {
const { openAssertionModal } = useChallengeState();
const { data: assertionData } = useScaffoldReadContract({
contractName: "OptimisticOracle",
functionName: "getAssertion",
args: [BigInt(assertionId)],
});
if (!assertionData) return null;
return (
<tr
key={assertionId}
onClick={() => {
openAssertionModal({ ...assertionData, assertionId, state });
}}
className={`group border-b border-base-300 cursor-pointer`}
>
{/* Description Column */}
<td>
<div className="group-hover:text-error">{assertionData.description}</div>
</td>
{/* Proposer Column */}
<td>
<Address address={assertionData.proposer} format="short" onlyEnsOrAddress disableAddressLink size="sm" />
</td>
{/* Disputer Column */}
<td>
<Address address={assertionData.disputer} format="short" onlyEnsOrAddress disableAddressLink size="sm" />
</td>
{/* Chevron Column */}
<td className="">
<div className="w-6 h-6 rounded-full border-error border flex items-center justify-center hover:bg-base-200 group-hover:bg-error transition-colors mx-auto">
<ChevronRightIcon className="w-4 h-4 text-error group-hover:text-white stroke-2 transition-colors" />
</div>
</td>
</tr>
);
};

View File

@@ -0,0 +1,33 @@
"use client";
import { OOTableProps } from "../types";
import { DisputedRow } from "./DisputedRow";
import { EmptyRow } from "./EmptyRow";
export const DisputedTable = ({ assertions }: OOTableProps) => {
return (
<div className="bg-base-100 rounded-lg shadow-lg overflow-x-auto">
<table className="w-full table-auto [&_th]:px-6 [&_th]:py-4 [&_td]:px-6 [&_td]:py-4">
{/* Header */}
<thead>
<tr className="bg-base-300">
<th className="text-left font-semibold w-5/12">Description</th>
<th className="text-left font-semibold w-3/12">Proposer</th>
<th className="text-left font-semibold w-3/12">Disputer</th>
<th className="text-left font-semibold w-1/12">{/* Icon column */}</th>
</tr>
</thead>
<tbody>
{assertions.length > 0 ? (
assertions.map(assertion => (
<DisputedRow key={assertion.assertionId} assertionId={assertion.assertionId} state={assertion.state} />
))
) : (
<EmptyRow colspan={4} />
)}
</tbody>
</table>
</div>
);
};

View File

@@ -0,0 +1,15 @@
export const EmptyRow = ({
message = "No assertions match this state.",
colspan = 4,
}: {
message?: string;
colspan?: number;
}) => {
return (
<tr>
<td colSpan={colspan} className="text-center">
{message}
</td>
</tr>
);
};

View File

@@ -0,0 +1,62 @@
import { useState } from "react";
import { Address } from "@scaffold-ui/components";
import { formatEther } from "viem";
import { useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth";
export const ExpiredRow = ({ assertionId }: { assertionId: number }) => {
const [isClaiming, setIsClaiming] = useState(false);
const { data: assertionData } = useScaffoldReadContract({
contractName: "OptimisticOracle",
functionName: "getAssertion",
args: [BigInt(assertionId)],
});
const { writeContractAsync } = useScaffoldWriteContract({
contractName: "OptimisticOracle",
});
const handleClaim = async () => {
setIsClaiming(true);
try {
await writeContractAsync({
functionName: "claimRefund",
args: [BigInt(assertionId)],
});
} catch (error) {
console.error(error);
} finally {
setIsClaiming(false);
}
};
if (!assertionData) return null;
return (
<tr key={assertionId} className={`border-b border-base-300`}>
{/* Description Column */}
<td>{assertionData.description}</td>
{/* Asserter Column */}
<td>
<Address address={assertionData.asserter} format="short" onlyEnsOrAddress disableAddressLink size="sm" />
</td>
{/* Reward Column */}
<td>{formatEther(assertionData.reward)} ETH</td>
{/* Claimed Column */}
<td>
{assertionData?.claimed ? (
<button className="btn btn-primary btn-xs" disabled>
Claimed
</button>
) : (
<button className="btn btn-primary btn-xs" onClick={handleClaim} disabled={isClaiming}>
Claim
</button>
)}
</td>
</tr>
);
};

View File

@@ -0,0 +1,31 @@
"use client";
import { OOTableProps } from "../types";
import { EmptyRow } from "./EmptyRow";
import { ExpiredRow } from "./ExpiredRow";
export const ExpiredTable = ({ assertions }: OOTableProps) => {
return (
<div className="bg-base-100 rounded-lg shadow-lg overflow-x-auto">
<table className="w-full table-auto [&_th]:px-6 [&_th]:py-4 [&_td]:px-6 [&_td]:py-4">
{/* Header */}
<thead>
<tr className="bg-base-300">
<th className="text-left font-semibold w-5/12">Description</th>
<th className="text-left font-semibold w-3/12">Asserter</th>
<th className="text-left font-semibold w-2/12">Reward</th>
<th className="text-left font-semibold w-2/12">Claim Refund</th>
</tr>
</thead>
<tbody>
{assertions.length > 0 ? (
assertions.map(assertion => <ExpiredRow key={assertion.assertionId} assertionId={assertion.assertionId} />)
) : (
<EmptyRow colspan={4} />
)}
</tbody>
</table>
</div>
);
};

View File

@@ -0,0 +1,21 @@
export const LoadingRow = () => {
return (
<tr className="border-b border-base-300">
<td>
<div className="h-5 bg-base-300 rounded animate-pulse"></div>
</td>
<td>
<div className="h-5 w-24 bg-base-300 rounded animate-pulse"></div>
</td>
<td>
<div className="h-5 w-24 bg-base-300 rounded animate-pulse"></div>
</td>
<td>
<div className="w-6 h-6 rounded-full bg-base-300 animate-pulse mx-auto"></div>
</td>
</tr>
);
};

View File

@@ -0,0 +1,52 @@
"use client";
import { OORowProps } from "../types";
import { TimeLeft } from "./TimeLeft";
import { formatEther } from "viem";
import { ChevronRightIcon } from "@heroicons/react/24/outline";
import { useScaffoldReadContract } from "~~/hooks/scaffold-eth";
import { useChallengeState } from "~~/services/store/challengeStore";
export const ProposedRow = ({ assertionId, state }: OORowProps) => {
const { openAssertionModal } = useChallengeState();
const { data: assertionData } = useScaffoldReadContract({
contractName: "OptimisticOracle",
functionName: "getAssertion",
args: [BigInt(assertionId)],
});
if (!assertionData) return null;
return (
<tr
key={assertionId}
className={`group border-b border-base-300 cursor-pointer`}
onClick={() => {
openAssertionModal({ ...assertionData, assertionId, state });
}}
>
{/* Query Column */}
<td>
<div className="group-hover:text-error">{assertionData?.description}</div>
</td>
{/* Bond Column */}
<td>{formatEther(assertionData?.bond)} ETH</td>
{/* Proposal Column */}
<td>{assertionData?.proposedOutcome ? "True" : "False"}</td>
{/* Challenge Period Column */}
<td>
<TimeLeft startTime={assertionData?.startTime} endTime={assertionData?.endTime} />
</td>
{/* Chevron Column */}
<td>
<div className="w-6 h-6 rounded-full border-error border flex items-center justify-center hover:bg-base-200 group-hover:bg-error transition-colors mx-auto">
<ChevronRightIcon className="w-4 h-4 text-error group-hover:text-white stroke-2 transition-colors" />
</div>
</td>
</tr>
);
};

View File

@@ -0,0 +1,32 @@
import { OOTableProps } from "../types";
import { EmptyRow } from "./EmptyRow";
import { ProposedRow } from "./ProposedRow";
export const ProposedTable = ({ assertions }: OOTableProps) => {
return (
<div className="bg-base-100 rounded-lg shadow-lg overflow-x-auto">
<table className="w-full table-auto [&_th]:px-6 [&_th]:py-4 [&_td]:px-6 [&_td]:py-4">
{/* Header */}
<thead>
<tr className="bg-base-300">
<th className="text-left font-semibold w-5/12">Description</th>
<th className="text-left font-semibold w-2/12">Bond</th>
<th className="text-left font-semibold w-2/12">Proposal</th>
<th className="text-left font-semibold w-2/12">Time Left</th>
<th className="text-left font-semibold w-1/12">{/* Icon column */}</th>
</tr>
</thead>
<tbody>
{assertions.length > 0 ? (
assertions.map(assertion => (
<ProposedRow key={assertion.assertionId} assertionId={assertion.assertionId} state={assertion.state} />
))
) : (
<EmptyRow colspan={5} />
)}
</tbody>
</table>
</div>
);
};

View File

@@ -0,0 +1,75 @@
"use client";
import { useState } from "react";
import { SettledRowProps } from "../types";
import { LoadingRow } from "./LoadingRow";
import { Address } from "@scaffold-ui/components";
import { formatEther } from "viem";
import { useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth";
import { ZERO_ADDRESS } from "~~/utils/scaffold-eth/common";
export const SettledRow = ({ assertionId }: SettledRowProps) => {
const [isClaiming, setIsClaiming] = useState(false);
const { data: assertionData, isLoading } = useScaffoldReadContract({
contractName: "OptimisticOracle",
functionName: "getAssertion",
args: [BigInt(assertionId)],
});
const { writeContractAsync } = useScaffoldWriteContract({
contractName: "OptimisticOracle",
});
if (isLoading) return <LoadingRow />;
if (!assertionData) return null;
const handleClaim = async () => {
try {
setIsClaiming(true);
const functionName = assertionData?.winner === ZERO_ADDRESS ? "claimUndisputedReward" : "claimDisputedReward";
await writeContractAsync({
functionName,
args: [BigInt(assertionId)],
});
} catch (error) {
console.error(error);
} finally {
setIsClaiming(false);
}
};
const winner = assertionData?.winner === ZERO_ADDRESS ? assertionData?.proposer : assertionData?.winner;
const outcome =
assertionData?.winner === ZERO_ADDRESS ? assertionData?.proposedOutcome : assertionData?.resolvedOutcome;
return (
<tr key={assertionId} className={`border-b border-base-300`}>
{/* Query Column */}
<td>{assertionData?.description}</td>
{/* Answer Column */}
<td>{outcome ? "True" : "False"}</td>
{/* Winner Column */}
<td>
<Address address={winner} format="short" onlyEnsOrAddress disableAddressLink size="sm" />
</td>
{/* Reward Column */}
<td>{formatEther(assertionData?.reward)} ETH</td>
{/* Claimed Column */}
<td>
{assertionData?.claimed ? (
<button className="btn btn-primary btn-xs" disabled>
Claimed
</button>
) : (
<button className="btn btn-primary btn-xs" onClick={handleClaim} disabled={isClaiming}>
Claim
</button>
)}
</td>
</tr>
);
};

View File

@@ -0,0 +1,32 @@
"use client";
import { OOTableProps } from "../types";
import { EmptyRow } from "./EmptyRow";
import { SettledRow } from "./SettledRow";
export const SettledTable = ({ assertions }: OOTableProps) => {
return (
<div className="bg-base-100 rounded-lg shadow-lg overflow-x-auto">
<table className="w-full table-auto [&_th]:px-6 [&_th]:py-4 [&_td]:px-6 [&_td]:py-4">
{/* Header */}
<thead>
<tr className="bg-base-300">
<th className="text-left font-semibold w-4/12">Description</th>
<th className="text-left font-semibold w-1/12">Result</th>
<th className="text-left font-semibold w-3/12">Winner</th>
<th className="text-left font-semibold w-2/12">Reward</th>
<th className="text-left font-semibold w-2/12">Claim</th>
</tr>
</thead>
<tbody>
{assertions.length > 0 ? (
assertions.map(assertion => <SettledRow key={assertion.assertionId} assertionId={assertion.assertionId} />)
) : (
<EmptyRow colspan={5} />
)}
</tbody>
</table>
</div>
);
};

View File

@@ -0,0 +1,245 @@
"use client";
import { useState } from "react";
import { IntegerInput } from "@scaffold-ui/debug-contracts";
import { parseEther } from "viem";
import { usePublicClient } from "wagmi";
import TooltipInfo from "~~/components/TooltipInfo";
import { useScaffoldWriteContract } from "~~/hooks/scaffold-eth";
import { useChallengeState } from "~~/services/store/challengeStore";
import { getRandomQuestion } from "~~/utils/helpers";
import { notification } from "~~/utils/scaffold-eth";
const MINIMUM_ASSERTION_WINDOW = 3;
const getStartTimestamp = (timestamp: bigint, startInMinutes: string) => {
if (startInMinutes.length === 0) return 0n;
if (Number(startInMinutes) === 0) return 0n;
return timestamp + BigInt(startInMinutes) * 60n;
};
const getEndTimestamp = (timestamp: bigint, startTimestamp: bigint, durationInMinutes: string) => {
if (durationInMinutes.length === 0) return 0n;
if (Number(durationInMinutes) === MINIMUM_ASSERTION_WINDOW) return 0n;
if (startTimestamp === 0n) return timestamp + BigInt(durationInMinutes) * 60n;
return startTimestamp + BigInt(durationInMinutes) * 60n;
};
interface SubmitAssertionModalProps {
isOpen: boolean;
onClose: () => void;
}
const SubmitAssertionModal = ({ isOpen, onClose }: SubmitAssertionModalProps) => {
const { timestamp } = useChallengeState();
const [isLoading, setIsLoading] = useState(false);
const publicClient = usePublicClient();
const [description, setDescription] = useState("");
const [reward, setReward] = useState<string>("");
const [startInMinutes, setStartInMinutes] = useState<string>("");
const [durationInMinutes, setDurationInMinutes] = useState<string>("");
const { writeContractAsync } = useScaffoldWriteContract({ contractName: "OptimisticOracle" });
const handleRandomQuestion = () => {
setDescription(getRandomQuestion());
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (durationInMinutes.length > 0 && Number(durationInMinutes) < MINIMUM_ASSERTION_WINDOW) {
notification.error(
`Duration must be at least ${MINIMUM_ASSERTION_WINDOW} minutes or leave blank to use default value`,
);
return;
}
if (Number(reward) === 0) {
notification.error(`Reward must be greater than 0 ETH`);
return;
}
if (!publicClient) {
notification.error("Public client not found");
return;
}
try {
setIsLoading(true);
let recentTimestamp = timestamp;
if (!recentTimestamp) {
const block = await publicClient.getBlock();
recentTimestamp = block.timestamp;
}
const startTimestamp = getStartTimestamp(recentTimestamp, startInMinutes);
const endTimestamp = getEndTimestamp(recentTimestamp, startTimestamp, durationInMinutes);
await writeContractAsync({
functionName: "assertEvent",
args: [description.trim(), startTimestamp, endTimestamp],
value: parseEther(reward),
});
// Reset form after successful submission
setDescription("");
setReward("");
setStartInMinutes("");
setDurationInMinutes("");
onClose();
} catch (error) {
console.log("Error with submission", error);
} finally {
setIsLoading(false);
}
};
const handleClose = () => {
onClose();
// Reset form when closing
setDescription("");
setReward("");
setStartInMinutes("");
setDurationInMinutes("");
};
if (!isOpen) return null;
const readyToSubmit = description.trim().length > 0 && reward.trim().length > 0;
return (
<>
<input type="checkbox" id="assertion-modal" className="modal-toggle" checked={isOpen} readOnly />
<label htmlFor="assertion-modal" className="modal cursor-pointer" onClick={handleClose}>
<label className="modal-box relative max-w-md w-full bg-base-100" htmlFor="" onClick={e => e.stopPropagation()}>
{/* dummy input to capture event onclick on modal box */}
<input className="h-0 w-0 absolute top-0 left-0" />
{/* Close button */}
<button onClick={handleClose} className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
</button>
<div className="relative">
<TooltipInfo
top={-2}
right={5}
className="tooltip-left"
infoText="Create a new assertion with your reward stake. Leave time inputs blank to use default values."
/>
</div>
{/* Modal Content */}
<div>
{/* Header */}
<div className="text-center mb-6">
<h2 className="text-xl font-bold">Submit New Assertion</h2>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
{/* Description Input */}
<div>
<label className="label">
<span className="text-accent font-medium">
Description <span className="text-red-500">*</span>
</span>
</label>
<div className="flex gap-2 items-start">
<div className="flex-1">
<div className="flex border-2 border-base-300 bg-base-200 rounded-full text-accent">
<textarea
name="description"
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="Enter assertion description..."
className="input input-ghost focus-within:border-transparent leading-8 focus:outline-hidden focus:bg-transparent h-auto min-h-[2.2rem] px-4 border w-full font-medium placeholder:text-accent/70 text-base-content/70 focus:text-base-content/70 whitespace-pre-wrap overflow-x-hidden"
rows={2}
/>
</div>
</div>
<button
type="button"
onClick={handleRandomQuestion}
className="btn btn-secondary btn-sm"
title="Select random question"
>
🎲
</button>
</div>
</div>
<div>
<label className="label">
<span className="text-accent font-medium">
Reward (ETH) <span className="text-red-500">*</span>
</span>
</label>
<IntegerInput
name="reward"
placeholder={`0.01`}
value={reward}
onChange={newValue => setReward(newValue)}
disableMultiplyBy1e18
/>
</div>
{/* Start Time and End Time Inputs */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="label">
<span className="text-accent font-medium">Start in (minutes)</span>
</label>
<IntegerInput
name="startTime"
placeholder="blank = now"
value={startInMinutes}
onChange={newValue => setStartInMinutes(newValue)}
disableMultiplyBy1e18
/>
</div>
<div>
<label className="label">
<span className="text-accent font-medium">Duration (minutes)</span>
</label>
<IntegerInput
name="endTime"
placeholder={`minimum ${MINIMUM_ASSERTION_WINDOW} minutes`}
value={durationInMinutes}
onChange={newValue => setDurationInMinutes(newValue)}
disableMultiplyBy1e18
/>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-3 mt-6">
<button type="submit" className="btn btn-primary flex-1" disabled={isLoading || !readyToSubmit}>
{isLoading && <span className="loading loading-spinner loading-xs"></span>}
Submit
</button>
</div>
</form>
</div>
</label>
</label>
</>
);
};
export const SubmitAssertionButton = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const openModal = () => setIsModalOpen(true);
const closeModal = () => setIsModalOpen(false);
return (
<>
{/* Button */}
<div className="my-8 flex justify-center">
<button className="btn btn-primary btn-lg" onClick={openModal}>
Submit New Assertion
</button>
</div>
{/* Modal - only mounted when open */}
{isModalOpen && <SubmitAssertionModal isOpen={isModalOpen} onClose={closeModal} />}
</>
);
};

View File

@@ -0,0 +1,62 @@
"use client";
import { useEffect, useState } from "react";
import { useChallengeState } from "~~/services/store/challengeStore";
function formatDuration(seconds: number, isPending: boolean) {
const totalSeconds = Math.max(seconds, 0);
const m = Math.floor(totalSeconds / 60);
const s = totalSeconds % 60;
return `${m} m ${s} s${isPending ? " left to start" : ""}`;
}
export const TimeLeft = ({ startTime, endTime }: { startTime: bigint; endTime: bigint }) => {
const { timestamp, refetchAssertionStates } = useChallengeState();
const [currentTime, setCurrentTime] = useState<number>(() =>
timestamp ? Number(timestamp) : Math.floor(Date.now() / 1000),
);
useEffect(() => {
const interval = setInterval(() => {
setCurrentTime(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
const start = Number(startTime);
const end = Number(endTime);
const now = currentTime;
const duration = end - now;
const ended = duration <= 0;
// Guard against division by zero and clamp to [0, 100]
const totalWindow = Math.max(end - start, 1);
const rawPercent = ((now - start) / totalWindow) * 100;
const progressPercent = Math.max(0, Math.min(100, rawPercent));
useEffect(() => {
if (ended && timestamp) {
refetchAssertionStates();
}
}, [ended, refetchAssertionStates, timestamp]);
let displayText: string;
if (ended) {
displayText = "Ended";
} else if (now < start) {
displayText = formatDuration(start - now, true);
} else {
displayText = formatDuration(Math.max(duration, 0), false);
}
return (
<div className="w-full space-y-1">
<div className={ended || duration < 60 ? "text-error" : ""}>{displayText}</div>
<div
className={`w-full h-1 bg-base-300 rounded-full overflow-hidden transition-opacity ${now > start ? "opacity-100" : "opacity-0"}`}
>
<div className="h-full bg-error transition-all" style={{ width: `${progressPercent}%` }} />
</div>
</div>
);
};