Initial commit with 🏗️ create-eth @ 2.0.4
This commit is contained in:
50
packages/nextjs/components/oracle/optimistic/AssertedRow.tsx
Normal file
50
packages/nextjs/components/oracle/optimistic/AssertedRow.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
260
packages/nextjs/components/oracle/optimistic/AssertionModal.tsx
Normal file
260
packages/nextjs/components/oracle/optimistic/AssertionModal.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
48
packages/nextjs/components/oracle/optimistic/DisputedRow.tsx
Normal file
48
packages/nextjs/components/oracle/optimistic/DisputedRow.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
15
packages/nextjs/components/oracle/optimistic/EmptyRow.tsx
Normal file
15
packages/nextjs/components/oracle/optimistic/EmptyRow.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
62
packages/nextjs/components/oracle/optimistic/ExpiredRow.tsx
Normal file
62
packages/nextjs/components/oracle/optimistic/ExpiredRow.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
21
packages/nextjs/components/oracle/optimistic/LoadingRow.tsx
Normal file
21
packages/nextjs/components/oracle/optimistic/LoadingRow.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
52
packages/nextjs/components/oracle/optimistic/ProposedRow.tsx
Normal file
52
packages/nextjs/components/oracle/optimistic/ProposedRow.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
75
packages/nextjs/components/oracle/optimistic/SettledRow.tsx
Normal file
75
packages/nextjs/components/oracle/optimistic/SettledRow.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
62
packages/nextjs/components/oracle/optimistic/TimeLeft.tsx
Normal file
62
packages/nextjs/components/oracle/optimistic/TimeLeft.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user