diff --git a/apps/entropy-debugger/src/app/page.tsx b/apps/entropy-debugger/src/app/page.tsx index 4e3e06e00b..c3ebf26d13 100644 --- a/apps/entropy-debugger/src/app/page.tsx +++ b/apps/entropy-debugger/src/app/page.tsx @@ -1,8 +1,9 @@ "use client"; -import hljs from "highlight.js/lib/core"; -import bash from "highlight.js/lib/languages/bash"; -import { useState, useMemo, useCallback, useEffect, useRef } from "react"; +import { Copy, ChevronDown, ChevronRight } from "lucide-react"; +import React, { useState, useEffect } from "react"; +import { createPublicClient, http, publicActions } from "viem"; +import { bigint } from "zod"; import { Input } from "../components/ui/input"; import { @@ -12,229 +13,343 @@ import { SelectTrigger, SelectValue, } from "../components/ui/select"; -import { Switch } from "../components/ui/switch"; -import { requestCallback } from "../lib/revelation"; -import { - EntropyDeployments, - isValidDeployment, -} from "../store/entropy-deployments"; +import { EntropyAbi } from "../lib/entropy-abi"; +import { getAllRequests, requestCallback, getRequestBySequenceNumber, getRequestByTransactionHash } from "../lib/revelation"; +import { EntropyDeployments } from "../store/entropy-deployments"; -import "highlight.js/styles/github-dark.css"; // You can choose different themes -// Register the bash language -hljs.registerLanguage("bash", bash); +type Request = { + chain: keyof typeof EntropyDeployments; + network: "mainnet" | "testnet"; + provider: `0x${string}`; + sequenceNumber: bigint; + userRandomNumber: `0x${string}`; + transactionHash: `0x${string}`; + blockNumber: bigint; +}; -class BaseError extends Error { - constructor(message: string) { - super(message); - this.name = "BaseError"; - } -} +type RequestWithStatus = Request & { + isExpanded: boolean; + isFulfilled?: boolean; + isLoading?: boolean; +}; -class InvalidTxHashError extends BaseError { - constructor(message: string) { - super(message); - this.name = "InvalidTxHashError"; - } -} +export default function PythEntropyDebugApp() { + const [requests, setRequests] = useState([]); + const [filteredRequests, setFilteredRequests] = useState([]); + const [selectedChain, setSelectedChain] = useState(""); + const [searchTerm, setSearchTerm] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [isSearching, setIsSearching] = useState(false); -enum TxStateType { - NotLoaded, - Loading, - Success, - Error, -} + const searchBySequenceNumber = async (sequenceNumber: string) => { + if (!selectedChain || !sequenceNumber) return; -const TxState = { - NotLoaded: () => ({ status: TxStateType.NotLoaded as const }), - Loading: () => ({ status: TxStateType.Loading as const }), - Success: (data: string) => ({ status: TxStateType.Success as const, data }), - ErrorState: (error: unknown) => ({ - status: TxStateType.Error as const, - error, - }), -}; + setIsSearching(true); + try { + const request = await getRequestBySequenceNumber(selectedChain, sequenceNumber); -type TxStateContext = - | ReturnType - | ReturnType - | ReturnType - | ReturnType; + if (request) { + // Check if the request already exists in the list + const exists = requests.some( + req => req.chain === selectedChain && req.sequenceNumber === request.sequenceNumber + ); -export default function PythEntropyDebugApp() { - const [state, setState] = useState(TxState.NotLoaded()); - const [isMainnet, setIsMainnet] = useState(false); - const [txHash, setTxHash] = useState(""); - const [error, setError] = useState(undefined); - const [selectedChain, setSelectedChain] = useState< - "" | keyof typeof EntropyDeployments - >(""); - - const validateTxHash = (hash: string) => { - if (!isValidTxHash(hash) && hash !== "") { - setError( - new InvalidTxHashError( - "Transaction hash must be 64 hexadecimal characters", - ), - ); - } else { - setError(undefined); + if (!exists) { + const newRequest = { ...request, isExpanded: false }; + setRequests(prev => [...prev, newRequest]); + setFilteredRequests(prev => [...prev, newRequest]); + } + } + } catch (error) { + console.error("Error searching for sequence number:", error); + } finally { + setIsSearching(false); } - setTxHash(hash); }; - const availableChains = useMemo(() => { - return Object.entries(EntropyDeployments) - .filter( - ([, deployment]) => - deployment.network === (isMainnet ? "mainnet" : "testnet"), - ) - .toSorted(([a], [b]) => a.localeCompare(b)) - .map(([key]) => key); - }, [isMainnet]); - - const oncClickFetchInfo = useCallback(() => { - if (selectedChain !== "") { - setState(TxState.Loading()); - requestCallback(txHash, selectedChain) - .then((data) => { - setState(TxState.Success(data)); - }) - .catch((error: unknown) => { - setState(TxState.ErrorState(error)); - }); + const searchByTransactionHash = async (txHash: string) => { + if (!selectedChain || !txHash) return; + + setIsSearching(true); + try { + const request = await getRequestByTransactionHash(selectedChain, txHash); + + if (request) { + // Check if the request already exists in the list + const exists = requests.some( + req => req.transactionHash.toLowerCase() === txHash.toLowerCase() + ); + + if (!exists) { + const newRequest = { ...request, isExpanded: false }; + setRequests(prev => [...prev, newRequest]); + setFilteredRequests(prev => [...prev, newRequest]); + } + } + } catch (error) { + console.error("Error searching for transaction hash:", error); + } finally { + setIsSearching(false); } - }, [txHash, selectedChain]); - - const updateIsMainnet = useCallback( - (newValue: boolean) => { - setSelectedChain(""); - setIsMainnet(newValue); - }, - [setSelectedChain, setIsMainnet], - ); + }; - const updateSelectedChain = useCallback( - (chain: string) => { - if (isValidDeployment(chain)) { - setSelectedChain(chain); + useEffect(() => { + const fetchRequests = async () => { + if (!selectedChain) { + setRequests([]); + setFilteredRequests([]); + return; } - }, - [setSelectedChain], - ); - return ( -
-

Pyth Entropy Debug App

- -
- - - -
-
- -
-
- - { - validateTxHash(e.target.value); - }} - /> - {error &&

{error.message}

} -
-
- -
- -
- ); -} + setIsLoading(true); + try { + const allRequests = await getAllRequests(selectedChain); + // Filter out any requests with undefined values + const validRequests = allRequests.filter( + (req): req is Request => + req.provider !== undefined && + req.sequenceNumber !== undefined && + req.userRandomNumber !== undefined + ); + setRequests(validRequests.map(req => ({ ...req, isExpanded: false }))); + setFilteredRequests(validRequests.map(req => ({ ...req, isExpanded: false }))); + } catch (error) { + console.error("Error fetching requests:", error); + } finally { + setIsLoading(false); + } + }; -const Info = ({ state }: { state: TxStateContext }) => { - const preRef = useRef(null); + fetchRequests(); + }, [selectedChain]); useEffect(() => { - if (preRef.current && state.status === TxStateType.Success) { - hljs.highlightElement(preRef.current); - } - }, [state]); + if (searchTerm) { + const term = searchTerm.toLowerCase(); + + // Check if the search term is a valid sequence number + if (/^\d+$/.test(term)) { + searchBySequenceNumber(term); + } + // Check if the search term is a valid transaction hash + else if (/^0x[a-fA-F0-9]{64}$/.test(term)) { + searchByTransactionHash(term); + } - switch (state.status) { - case TxStateType.NotLoaded: { - return
Not loaded
; + // Filter existing requests + const filtered = requests.filter( + (req) => + req.sequenceNumber.toString().includes(term) || + req.transactionHash.toLowerCase().includes(term) + ); + setFilteredRequests(filtered); + } else { + setFilteredRequests(requests); } - case TxStateType.Loading: { - return
Loading...
; + }, [requests, searchTerm]); + + const getExplorerUrl = (chain: string, txHash: string) => { + const deployment = EntropyDeployments[chain as keyof typeof EntropyDeployments]; + if (!deployment) return "#"; + + const baseUrl = deployment.network === "mainnet" + ? deployment.explorer + : deployment.explorer; + + return `${baseUrl}/tx/${txHash}`; + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + }; + + const checkRequestFulfillment = async (request: RequestWithStatus) => { + const deployment = EntropyDeployments[request.chain]; + if (!deployment) return; + + const client = createPublicClient({ + transport: http(deployment.rpc), + }).extend(publicActions); + + try { + const requestData = await client.readContract({ + address: deployment.address as `0x${string}`, + abi: EntropyAbi, + functionName: "getRequest", + args: [request.provider, request.sequenceNumber], + }); + + // If the request exists and has been fulfilled, the sequenceNumber will be zero + const isFulfilled = requestData.sequenceNumber == BigInt(0); + + setRequests(prev => prev.map(req => + req.chain === request.chain && req.sequenceNumber === request.sequenceNumber + ? { ...req, isFulfilled, isLoading: false } + : req + )); + } catch (error) { + console.error("Error checking request fulfillment:", error); + setRequests(prev => prev.map(req => + req.chain === request.chain && req.sequenceNumber === request.sequenceNumber + ? { ...req, isLoading: false } + : req + )); } - case TxStateType.Success: { - return ( -
-

- Please run the following command in your terminal: -

-
-
-              {state.data}
-            
- -
-
- ); + }; + + const toggleRow = (request: RequestWithStatus) => { + setRequests(prev => prev.map(req => + req.chain === request.chain && req.sequenceNumber === request.sequenceNumber + ? { ...req, isExpanded: !req.isExpanded } + : req + )); + + if (!request.isExpanded) { + setRequests(prev => prev.map(req => + req.chain === request.chain && req.sequenceNumber === request.sequenceNumber + ? { ...req, isLoading: true } + : req + )); + checkRequestFulfillment(request); } - case TxStateType.Error: { - return ( -
-
{String(state.error)}
+ }; + + return ( +
+

Pyth Entropy Requests

+ +
+
+ + + {selectedChain && ( +
+ { setSearchTerm(e.target.value); }} + className="flex-1" + /> + {isSearching && ( +
+ Searching... +
+ )} +
+ )}
- ); - } - } -}; -function isValidTxHash(hash: string) { - const cleanHash = hash.toLowerCase().replace("0x", ""); - return /^[\da-f]{64}$/.test(cleanHash); + {selectedChain ? (isLoading ? ( +
Loading requests...
+ ) : ( +
+ + + + + + + + + + + {filteredRequests.map((request) => ( + + { toggleRow(request); }} + > + + + + + + {request.isExpanded && ( + + + + )} + + ))} + +
ChainSequence NumberProviderTransaction Hash
+
+ {request.isExpanded ? : } + + {request.chain} + +
+
{request.sequenceNumber.toString()}{request.provider} +
+ { e.stopPropagation(); }} + > + {request.transactionHash.slice(0, 8)}...{request.transactionHash.slice(-6)} + + +
+
+ {request.isLoading ? ( +
Loading...
+ ) : ( +
+
+ Status: {request.isFulfilled ? "Fulfilled" : "Not Fulfilled"} +
+ {!request.isFulfilled && ( +
+
Retry Command:
+
+ { /* TODO: put in retry command here */ } +
+
+ )} +
+ )} +
+
+ )) : ( +
+ Please select a chain to view requests +
+ )} +
+
+ ); } diff --git a/apps/entropy-debugger/src/lib/revelation.ts b/apps/entropy-debugger/src/lib/revelation.ts index 224e0d1532..b967a007df 100644 --- a/apps/entropy-debugger/src/lib/revelation.ts +++ b/apps/entropy-debugger/src/lib/revelation.ts @@ -1,4 +1,4 @@ -import { createPublicClient, http, parseEventLogs, publicActions } from "viem"; +import { createPublicClient, http, parseEventLogs, publicActions, keccak256 } from "viem"; import { z } from "zod"; import { EntropyAbi } from "./entropy-abi"; @@ -73,3 +73,143 @@ const revelationSchema = z.object({ data: z.string(), }), }); + +export async function getAllRequests(chain: keyof typeof EntropyDeployments) { + const deployment = EntropyDeployments[chain]; + if (!deployment) { + throw new Error(`Invalid chain: ${chain}`); + } + + try { + const client = createPublicClient({ + transport: http(deployment.rpc), + }).extend(publicActions); + + // Get the latest block number + const latestBlock = await client.getBlockNumber(); + + // Look back 100 blocks for requests + const fromBlock = latestBlock - BigInt(100); + + console.log(`Fetching logs for chain ${chain} from block ${fromBlock} to ${latestBlock}`); + console.log(`Contract address: ${deployment.address}`); + + // Get logs using the event from the ABI + const logs = await client.getLogs({ + address: deployment.address as `0x${string}`, + fromBlock, + toBlock: latestBlock, + event: EntropyAbi.find(event => event.name === "RequestedWithCallback" && event.type === "event")!, + }); + + console.log(`Found ${logs.length} logs for chain ${chain}`); + + return logs.map(log => ({ + chain, + network: deployment.network, + provider: log.args.provider!, + sequenceNumber: log.args.sequenceNumber!, + userRandomNumber: log.args.userRandomNumber!, + transactionHash: log.transactionHash, + blockNumber: log.blockNumber, + })); + } catch (error) { + console.error(`Error fetching logs for chain ${chain}:`, error); + return []; + } +} + +export async function getRequestBySequenceNumber( + chain: keyof typeof EntropyDeployments, + sequenceNumber: string +) { + const deployment = EntropyDeployments[chain]; + if (!deployment) { + throw new Error(`Invalid chain: ${chain}`); + } + + try { + const client = createPublicClient({ + transport: http(deployment.rpc), + }).extend(publicActions); + + // Get the latest block number + const latestBlock = await client.getBlockNumber(); + + // Look back 10000 blocks for requests + const fromBlock = latestBlock - BigInt(10_000); + + // Get logs for the specific sequence number + const logs = await client.getLogs({ + address: deployment.address as `0x${string}`, + fromBlock: fromBlock, + toBlock: latestBlock, + event: EntropyAbi.find(event => event.name === "RequestedWithCallback" && event.type === "event")!, + args: { + sequenceNumber: BigInt(sequenceNumber) + } + }); + + if (logs.length > 0) { + const log = logs[0] as { + args: { + provider: `0x${string}`; + sequenceNumber: bigint; + userRandomNumber: `0x${string}`; + }; + transactionHash: `0x${string}`; + blockNumber: bigint; + }; + + return { + chain, + network: deployment.network, + provider: log.args.provider, + sequenceNumber: log.args.sequenceNumber, + userRandomNumber: log.args.userRandomNumber, + transactionHash: log.transactionHash, + blockNumber: log.blockNumber, + }; + } + + return null; + } catch (error) { + console.error(`Error searching for sequence number ${sequenceNumber} on chain ${chain}:`, error); + return null; + } +} + +export async function getRequestByTransactionHash( + chain: keyof typeof EntropyDeployments, + txHash: string +) { + const deployment = EntropyDeployments[chain]; + if (!deployment) { + throw new Error(`Invalid chain: ${chain}`); + } + + try { + const { provider, sequenceNumber, userRandomNumber } = await fetchInfoFromTx(txHash, deployment); + + const client = createPublicClient({ + transport: http(deployment.rpc), + }).extend(publicActions); + + const receipt = await client.getTransactionReceipt({ + hash: txHash as `0x${string}`, + }); + + return { + chain, + network: deployment.network, + provider, + sequenceNumber, + userRandomNumber, + transactionHash: txHash as `0x${string}`, + blockNumber: receipt.blockNumber, + }; + } catch (error) { + console.error(`Error fetching request for transaction ${txHash} on chain ${chain}:`, error); + return null; + } +}