Files
sre-06-oracles/packages/nextjs/components/oracle/NodeRow.tsx
2026-01-23 20:20:58 +07:00

201 lines
8.1 KiB
TypeScript

import { useMemo } from "react";
import { ConfigSlider } from "./ConfigSlider";
import { NodeRowProps } from "./types";
import { Address } from "@scaffold-ui/components";
import { erc20Abi, formatEther } from "viem";
import { useReadContract } from "wagmi";
import { HighlightedCell } from "~~/components/oracle/HighlightedCell";
import { useScaffoldReadContract } from "~~/hooks/scaffold-eth";
import { getHighlightColorForPrice } from "~~/utils/helpers";
export interface NodeRowEditRequest {
address: string;
buttonRect: { top: number; left: number; bottom: number; right: number };
}
interface NodeRowWithEditProps extends NodeRowProps {
onEditRequest?: (req: NodeRowEditRequest) => void;
isEditing?: boolean;
showInlineSettings?: boolean;
}
export const NodeRow = ({ address, bucketNumber, showInlineSettings }: NodeRowWithEditProps) => {
// Hooks and contract reads
const { data: oracleTokenAddress } = useScaffoldReadContract({
contractName: "StakingOracle",
functionName: "oracleToken",
});
const { data: oraBalance } = useReadContract({
address: oracleTokenAddress as `0x${string}`,
abi: erc20Abi,
functionName: "balanceOf",
args: [address],
query: { enabled: !!oracleTokenAddress, refetchInterval: 5000 },
});
const { data: minimumStake } = useScaffoldReadContract({
contractName: "StakingOracle",
functionName: "MINIMUM_STAKE",
args: undefined,
});
const { data: currentBucket } = useScaffoldReadContract({
contractName: "StakingOracle",
functionName: "getCurrentBucketNumber",
}) as { data: bigint | undefined };
const previousBucket = useMemo(
() => (currentBucket && currentBucket > 0n ? currentBucket - 1n : 0n),
[currentBucket],
);
const shouldFetchPrevMedian = currentBucket !== undefined && previousBucket > 0n;
const { data: prevBucketMedian } = useScaffoldReadContract({
contractName: "StakingOracle",
functionName: "getPastPrice",
args: [previousBucket] as any,
query: { enabled: shouldFetchPrevMedian },
}) as { data: bigint | undefined };
const { data: effectiveStake } = useScaffoldReadContract({
contractName: "StakingOracle",
functionName: "getEffectiveStake",
args: [address],
}) as { data: bigint | undefined };
// Get current bucket price
const { data: currentBucketPrice } = useScaffoldReadContract({
contractName: "StakingOracle",
functionName: "getSlashedStatus",
args: [address, currentBucket ?? 0n] as const,
watch: true,
}) as { data?: [bigint, boolean] };
const reportedPriceInCurrentBucket = currentBucketPrice?.[0];
// Past bucket data (always call hook; gate via enabled)
const isCurrentView = bucketNumber === null || bucketNumber === undefined;
const { data: addressDataAtBucket } = useScaffoldReadContract({
contractName: "StakingOracle",
functionName: "getSlashedStatus",
args: [address, (bucketNumber ?? 0n) as any],
query: { enabled: !isCurrentView },
}) as { data?: [bigint, boolean] };
const pastReportedPrice = !isCurrentView && addressDataAtBucket ? addressDataAtBucket[0] : undefined;
const pastSlashed = !isCurrentView && addressDataAtBucket ? addressDataAtBucket[1] : undefined;
const { data: selectedBucketMedian } = useScaffoldReadContract({
contractName: "StakingOracle",
functionName: "getPastPrice",
args: [bucketNumber ?? 0n] as any,
query: {
enabled: !isCurrentView && bucketNumber !== null && bucketNumber !== undefined && (bucketNumber as bigint) > 0n,
},
}) as { data: bigint | undefined };
// Formatting
const stakedAmountFormatted =
effectiveStake !== undefined
? Number(formatEther(effectiveStake)).toLocaleString(undefined, { maximumFractionDigits: 2 })
: "Loading...";
const lastReportedPriceFormatted =
reportedPriceInCurrentBucket !== undefined && reportedPriceInCurrentBucket !== 0n
? `$${Number(parseFloat(formatEther(reportedPriceInCurrentBucket)).toFixed(2))}`
: "Not reported";
const oraBalanceFormatted =
oraBalance !== undefined
? Number(formatEther(oraBalance as bigint)).toLocaleString(undefined, { maximumFractionDigits: 2 })
: "Loading...";
const isInsufficientStake =
effectiveStake !== undefined && minimumStake !== undefined && effectiveStake < (minimumStake as bigint);
// Calculate deviation for past buckets
const deviationText = useMemo(() => {
if (isCurrentView) return "—";
if (!pastReportedPrice || pastReportedPrice === 0n) return "—";
if (!selectedBucketMedian || selectedBucketMedian === 0n) return "—";
const median = Number(formatEther(selectedBucketMedian));
const price = Number(formatEther(pastReportedPrice));
if (!Number.isFinite(median) || median === 0) return "—";
const pct = ((price - median) / median) * 100;
const sign = pct > 0 ? "+" : "";
return `${sign}${pct.toFixed(2)}%`;
}, [isCurrentView, pastReportedPrice, selectedBucketMedian]);
// Deviation for current bucket vs previous bucket average
const currentDeviationText = useMemo(() => {
if (!isCurrentView) return "—";
if (!reportedPriceInCurrentBucket || reportedPriceInCurrentBucket === 0n) return "—";
if (!prevBucketMedian || prevBucketMedian === 0n) return "—";
const avg = Number(formatEther(prevBucketMedian));
const price = Number(formatEther(reportedPriceInCurrentBucket));
if (!Number.isFinite(avg) || avg === 0) return "—";
const pct = ((price - avg) / avg) * 100;
const sign = pct > 0 ? "+" : "";
return `${sign}${pct.toFixed(2)}%`;
}, [isCurrentView, reportedPriceInCurrentBucket, prevBucketMedian]);
return (
<>
<tr className={isInsufficientStake ? "opacity-40" : ""}>
<td>
<div className="flex flex-col">
<Address address={address} size="sm" format="short" onlyEnsOrAddress={true} />
<span className="text-xs opacity-70">{oraBalanceFormatted} ORA</span>
</div>
</td>
{showInlineSettings ? (
// Inline settings mode: only show the settings sliders column
<td className="whitespace-nowrap">
<div className="flex flex-col gap-2 min-w-[220px]">
<div className="flex items-center gap-2">
<ConfigSlider nodeAddress={address.toLowerCase()} endpoint="skip-probability" label="skip rate" />
<ConfigSlider nodeAddress={address.toLowerCase()} endpoint="price-variance" label="price deviation" />
</div>
</div>
</td>
) : isCurrentView ? (
<>
<HighlightedCell value={stakedAmountFormatted} highlightColor="bg-error">
{stakedAmountFormatted}
</HighlightedCell>
<HighlightedCell value={oraBalanceFormatted} highlightColor="bg-success">
{oraBalanceFormatted}
</HighlightedCell>
<HighlightedCell
value={lastReportedPriceFormatted}
highlightColor={getHighlightColorForPrice(reportedPriceInCurrentBucket, prevBucketMedian)}
className={""}
>
{lastReportedPriceFormatted}
</HighlightedCell>
<td>{currentDeviationText}</td>
</>
) : (
<>
<HighlightedCell
value={
pastReportedPrice !== undefined && pastReportedPrice !== 0n
? `$${Number(parseFloat(formatEther(pastReportedPrice)).toFixed(2))}`
: "Not reported"
}
highlightColor={
pastSlashed ? "bg-error" : getHighlightColorForPrice(pastReportedPrice, selectedBucketMedian)
}
className={pastSlashed ? "border-2 border-error" : ""}
>
{pastReportedPrice !== undefined && pastReportedPrice !== 0n
? `$${Number(parseFloat(formatEther(pastReportedPrice)).toFixed(2))}`
: "Not reported"}
{pastSlashed && <span className="ml-2 text-xs text-error">Slashed</span>}
</HighlightedCell>
<td>{deviationText}</td>
</>
)}
</tr>
{/* No inline editor row; editor is rendered by parent as floating panel */}
</>
);
};