Przeglądaj źródła

feat(entropy-debug) Add Entropy debug app (#2274)

* Fix entropy debug app config (#2271)

* fix

* name change
Aditya Arora 10 miesięcy temu
rodzic
commit
a9b6b69da5
32 zmienionych plików z 2697 dodań i 175 usunięć
  1. 1 0
      .prettierignore
  2. 1 0
      apps/entropy-debugger/.gitignore
  3. 7 0
      apps/entropy-debugger/.prettierignore
  4. 0 0
      apps/entropy-debugger/README.md
  5. 14 0
      apps/entropy-debugger/components.json
  6. 1 0
      apps/entropy-debugger/eslint.config.js
  7. 1 0
      apps/entropy-debugger/jest.config.js
  8. 5 0
      apps/entropy-debugger/next-env.d.ts
  9. 49 0
      apps/entropy-debugger/next.config.js
  10. 52 0
      apps/entropy-debugger/package.json
  11. 8 0
      apps/entropy-debugger/postcss.config.js
  12. 1 0
      apps/entropy-debugger/prettier.config.js
  13. 7 0
      apps/entropy-debugger/prettierignore
  14. BIN
      apps/entropy-debugger/src/app/favicon.ico
  15. BIN
      apps/entropy-debugger/src/app/fonts/GeistMonoVF.woff
  16. BIN
      apps/entropy-debugger/src/app/fonts/GeistVF.woff
  17. 72 0
      apps/entropy-debugger/src/app/globals.css
  18. 35 0
      apps/entropy-debugger/src/app/layout.tsx
  19. 241 0
      apps/entropy-debugger/src/app/page.tsx
  20. 55 0
      apps/entropy-debugger/src/components/ui/button.tsx
  21. 13 0
      apps/entropy-debugger/src/components/ui/input.tsx
  22. 146 0
      apps/entropy-debugger/src/components/ui/select.tsx
  23. 25 0
      apps/entropy-debugger/src/components/ui/switch.tsx
  24. 1000 0
      apps/entropy-debugger/src/lib/entropy-abi.ts
  25. 77 0
      apps/entropy-debugger/src/lib/revelation.ts
  26. 4 0
      apps/entropy-debugger/src/lib/utils.ts
  27. 395 0
      apps/entropy-debugger/src/store/entropy-deployments.ts
  28. 63 0
      apps/entropy-debugger/tailwind.config.ts
  29. 5 0
      apps/entropy-debugger/tsconfig.json
  30. 19 0
      apps/entropy-debugger/turbo.json
  31. 3 0
      apps/entropy-debugger/vercel.json
  32. 397 175
      pnpm-lock.yaml

+ 1 - 0
.prettierignore

@@ -20,5 +20,6 @@ patches/
 apps/api-reference
 apps/staking
 apps/insights
+apps/entropy-debug
 governance/pyth_staking_sdk
 packages/*

+ 1 - 0
apps/entropy-debugger/.gitignore

@@ -0,0 +1 @@
+.env*.local

+ 7 - 0
apps/entropy-debugger/.prettierignore

@@ -0,0 +1,7 @@
+.next/
+coverage/
+node_modules/
+*.tsbuildinfo
+.env*.local
+.env
+.DS_Store

+ 0 - 0
apps/entropy-debugger/README.md


+ 14 - 0
apps/entropy-debugger/components.json

@@ -0,0 +1,14 @@
+{
+  "$schema": "https://ui.shadcn.com/schema.json",
+  "style": "default",
+  "rsc": true,
+  "tsx": true,
+  "tailwind": {
+    "config": "tailwind.config.ts",
+    "css": "src/app/globals.css",
+    "baseColor": "neutral",
+    "cssVariables": true,
+    "prefix": ""
+  },
+  "iconLibrary": "lucide"
+}

+ 1 - 0
apps/entropy-debugger/eslint.config.js

@@ -0,0 +1 @@
+export { nextjs as default } from "@cprussin/eslint-config";

+ 1 - 0
apps/entropy-debugger/jest.config.js

@@ -0,0 +1 @@
+export { nextjs as default } from "@cprussin/jest-config";

+ 5 - 0
apps/entropy-debugger/next-env.d.ts

@@ -0,0 +1,5 @@
+/// <reference types="next" />
+/// <reference types="next/image-types/global" />
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

+ 49 - 0
apps/entropy-debugger/next.config.js

@@ -0,0 +1,49 @@
+const config = {
+  reactStrictMode: true,
+
+  pageExtensions: ["ts", "tsx", "mdx"],
+
+  logging: {
+    fetches: {
+      fullUrl: true,
+    },
+  },
+
+  webpack(config) {
+    config.resolve.extensionAlias = {
+      ".js": [".js", ".ts", ".tsx"],
+    };
+
+    return config;
+  },
+
+  headers: async () => [
+    {
+      source: "/:path*",
+      headers: [
+        {
+          key: "X-XSS-Protection",
+          value: "1; mode=block",
+        },
+        {
+          key: "Referrer-Policy",
+          value: "strict-origin-when-cross-origin",
+        },
+        {
+          key: "Strict-Transport-Security",
+          value: "max-age=2592000",
+        },
+        {
+          key: "X-Content-Type-Options",
+          value: "nosniff",
+        },
+        {
+          key: "Permissions-Policy",
+          value:
+            "vibrate=(), geolocation=(), midi=(), notifications=(), push=(), sync-xhr=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), speaker=(), vibrate=(), fullscreen=self",
+        },
+      ],
+    },
+  ],
+};
+export default config;

+ 52 - 0
apps/entropy-debugger/package.json

@@ -0,0 +1,52 @@
+{
+  "name": "@pythnetwork/entropy-debugger",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "engines": {
+    "node": "22"
+  },
+  "scripts": {
+    "build": "next build",
+    "fix:format": "prettier --write .",
+    "fix:lint": "eslint --fix .",
+    "start:dev": "next dev --port 3005",
+    "start:prod": "next start --port 3005",
+    "test:format": "prettier --check .",
+    "test:lint": "eslint .",
+    "test:types": "tsc"
+  },
+  "dependencies": {
+    "@radix-ui/react-select": "^2.1.2",
+    "@radix-ui/react-slot": "^1.1.0",
+    "@radix-ui/react-switch": "^1.1.1",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "catalog:",
+    "highlight.js": "^11.10.0",
+    "lucide-react": "^0.465.0",
+    "next": "catalog:",
+    "react": "catalog:",
+    "react-dom": "catalog:",
+    "tailwind-merge": "^2.5.5",
+    "tailwindcss-animate": "^1.0.7",
+    "viem": "^2.21.53",
+    "zod": "catalog:"
+  },
+  "devDependencies": {
+    "@cprussin/eslint-config": "catalog:",
+    "@cprussin/jest-config": "catalog:",
+    "@cprussin/prettier-config": "catalog:",
+    "@cprussin/tsconfig": "catalog:",
+    "@types/jest": "catalog:",
+    "@types/node": "catalog:",
+    "@types/react": "catalog:",
+    "@types/react-dom": "catalog:",
+    "eslint": "catalog:",
+    "jest": "catalog:",
+    "postcss": "catalog:",
+    "prettier": "catalog:",
+    "tailwindcss": "catalog:",
+    "typescript": "catalog:",
+    "vercel": "catalog:"
+  }
+}

+ 8 - 0
apps/entropy-debugger/postcss.config.js

@@ -0,0 +1,8 @@
+/** @type {import('postcss-load-config').Config} */
+const config = {
+  plugins: {
+    tailwindcss: {},
+  },
+};
+
+export default config;

+ 1 - 0
apps/entropy-debugger/prettier.config.js

@@ -0,0 +1 @@
+export { base as default } from "@cprussin/prettier-config";

+ 7 - 0
apps/entropy-debugger/prettierignore

@@ -0,0 +1,7 @@
+.next/
+coverage/
+node_modules/
+*.tsbuildinfo
+.env*.local
+.env
+.DS_Store

BIN
apps/entropy-debugger/src/app/favicon.ico


BIN
apps/entropy-debugger/src/app/fonts/GeistMonoVF.woff


BIN
apps/entropy-debugger/src/app/fonts/GeistVF.woff


+ 72 - 0
apps/entropy-debugger/src/app/globals.css

@@ -0,0 +1,72 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+  font-family: Arial, Helvetica, sans-serif;
+}
+
+@layer base {
+  :root {
+    --background: 0 0% 100%;
+    --foreground: 0 0% 3.9%;
+    --card: 0 0% 100%;
+    --card-foreground: 0 0% 3.9%;
+    --popover: 0 0% 100%;
+    --popover-foreground: 0 0% 3.9%;
+    --primary: 0 0% 9%;
+    --primary-foreground: 0 0% 98%;
+    --secondary: 0 0% 96.1%;
+    --secondary-foreground: 0 0% 9%;
+    --muted: 0 0% 96.1%;
+    --muted-foreground: 0 0% 45.1%;
+    --accent: 0 0% 96.1%;
+    --accent-foreground: 0 0% 9%;
+    --destructive: 0 84.2% 60.2%;
+    --destructive-foreground: 0 0% 98%;
+    --border: 0 0% 89.8%;
+    --input: 0 0% 89.8%;
+    --ring: 0 0% 3.9%;
+    --chart-1: 12 76% 61%;
+    --chart-2: 173 58% 39%;
+    --chart-3: 197 37% 24%;
+    --chart-4: 43 74% 66%;
+    --chart-5: 27 87% 67%;
+    --radius: 0.5rem;
+  }
+  .dark {
+    --background: 0 0% 3.9%;
+    --foreground: 0 0% 98%;
+    --card: 0 0% 3.9%;
+    --card-foreground: 0 0% 98%;
+    --popover: 0 0% 3.9%;
+    --popover-foreground: 0 0% 98%;
+    --primary: 0 0% 98%;
+    --primary-foreground: 0 0% 9%;
+    --secondary: 0 0% 14.9%;
+    --secondary-foreground: 0 0% 98%;
+    --muted: 0 0% 14.9%;
+    --muted-foreground: 0 0% 63.9%;
+    --accent: 0 0% 14.9%;
+    --accent-foreground: 0 0% 98%;
+    --destructive: 0 62.8% 30.6%;
+    --destructive-foreground: 0 0% 98%;
+    --border: 0 0% 14.9%;
+    --input: 0 0% 14.9%;
+    --ring: 0 0% 83.1%;
+    --chart-1: 220 70% 50%;
+    --chart-2: 160 60% 45%;
+    --chart-3: 30 80% 55%;
+    --chart-4: 280 65% 60%;
+    --chart-5: 340 75% 55%;
+  }
+}
+
+@layer base {
+  * {
+    @apply border-border;
+  }
+  body {
+    @apply bg-background text-foreground;
+  }
+}

+ 35 - 0
apps/entropy-debugger/src/app/layout.tsx

@@ -0,0 +1,35 @@
+import type { Metadata } from "next";
+import localFont from "next/font/local";
+import "./globals.css";
+
+const geistSans = localFont({
+  src: "./fonts/GeistVF.woff",
+  variable: "--font-geist-sans",
+  weight: "100 900",
+});
+const geistMono = localFont({
+  src: "./fonts/GeistMonoVF.woff",
+  variable: "--font-geist-mono",
+  weight: "100 900",
+});
+
+export const metadata: Metadata = {
+  title: "Pyth Entropy Debug App",
+  description: "Pyth Entropy Debug App",
+};
+
+export default function RootLayout({
+  children,
+}: Readonly<{
+  children: React.ReactNode;
+}>) {
+  return (
+    <html lang="en">
+      <body
+        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
+      >
+        {children}
+      </body>
+    </html>
+  );
+}

+ 241 - 0
apps/entropy-debugger/src/app/page.tsx

@@ -0,0 +1,241 @@
+"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 { Input } from "../components/ui/input";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  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 "highlight.js/styles/github-dark.css"; // You can choose different themes
+
+// Register the bash language
+hljs.registerLanguage("bash", bash);
+
+class BaseError extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = "BaseError";
+  }
+}
+
+class InvalidTxHashError extends BaseError {
+  constructor(message: string) {
+    super(message);
+    this.name = "InvalidTxHashError";
+  }
+}
+
+enum TxStateType {
+  NotLoaded,
+  Loading,
+  Success,
+  Error,
+}
+
+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,
+  }),
+};
+
+type TxStateContext =
+  | ReturnType<typeof TxState.NotLoaded>
+  | ReturnType<typeof TxState.Loading>
+  | ReturnType<typeof TxState.Success>
+  | ReturnType<typeof TxState.ErrorState>;
+
+export default function PythEntropyDebugApp() {
+  const [state, setState] = useState<TxStateContext>(TxState.NotLoaded());
+  const [isMainnet, setIsMainnet] = useState<boolean>(false);
+  const [txHash, setTxHash] = useState<string>("");
+  const [error, setError] = useState<BaseError | undefined>(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);
+    }
+    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));
+        });
+    }
+  }, [txHash, selectedChain]);
+
+  const updateIsMainnet = useCallback(
+    (newValue: boolean) => {
+      setSelectedChain("");
+      setIsMainnet(newValue);
+    },
+    [setSelectedChain, setIsMainnet],
+  );
+
+  const updateSelectedChain = useCallback(
+    (chain: string) => {
+      if (isValidDeployment(chain)) {
+        setSelectedChain(chain);
+      }
+    },
+    [setSelectedChain],
+  );
+
+  return (
+    <div className="flex flex-col items-center justify-start h-screen">
+      <h1 className="text-4xl font-bold mt-8">Pyth Entropy Debug App</h1>
+
+      <div className="flex items-center space-x-2 mt-4">
+        <label htmlFor="network-mode">Testnet</label>
+        <Switch
+          id="network-mode"
+          defaultChecked={false}
+          onCheckedChange={updateIsMainnet}
+        />
+        <label htmlFor="network-mode">Mainnet</label>
+      </div>
+      <div className="mt-4">
+        <Select onValueChange={updateSelectedChain} value={selectedChain}>
+          <SelectTrigger>
+            <SelectValue placeholder="Select Chain" />
+          </SelectTrigger>
+          <SelectContent>
+            {availableChains.map((chain) => (
+              <SelectItem key={chain} value={chain}>
+                {chain.charAt(0).toUpperCase() +
+                  chain.slice(1).replaceAll("-", " ")}
+              </SelectItem>
+            ))}
+          </SelectContent>
+        </Select>
+      </div>
+      <div className="mt-4">
+        <label htmlFor="tx-hash" className="mr-2">
+          Request Transaction Hash:
+        </label>
+        <Input
+          minLength={64}
+          id="tx-hash"
+          className={`border rounded p-2 w-full ${error ? "border-red-500" : ""}`}
+          placeholder="Enter transaction hash"
+          value={txHash}
+          onChange={(e) => {
+            validateTxHash(e.target.value);
+          }}
+        />
+        {error && <p className="text-red-500 text-sm mt-1">{error.message}</p>}
+      </div>
+      <div className="mt-4">
+        <button
+          className="bg-blue-500 hover:bg-blue-600 text-white p-2 rounded disabled:bg-slate-200 disabled:text-slate-500 disabled:cursor-not-allowed"
+          onClick={oncClickFetchInfo}
+          disabled={selectedChain === "" || txHash === ""}
+        >
+          Fetch Info
+        </button>
+      </div>
+      <Info state={state} />
+    </div>
+  );
+}
+
+const Info = ({ state }: { state: TxStateContext }) => {
+  const preRef = useRef<HTMLPreElement>(null);
+
+  useEffect(() => {
+    if (preRef.current && state.status === TxStateType.Success) {
+      hljs.highlightElement(preRef.current);
+    }
+  }, [state]);
+
+  switch (state.status) {
+    case TxStateType.NotLoaded: {
+      return <div>Not loaded</div>;
+    }
+    case TxStateType.Loading: {
+      return <div>Loading...</div>;
+    }
+    case TxStateType.Success: {
+      return (
+        <div className="mt-4 p-4 bg-gray-100 rounded w-full max-w-3xl">
+          <p className="mb-2">
+            Please run the following command in your terminal:
+          </p>
+          <div className="relative">
+            <pre
+              ref={preRef}
+              className="bg-black text-white p-4 rounded overflow-x-auto whitespace-pre-wrap break-words"
+            >
+              <code className="language-bash">{state.data}</code>
+            </pre>
+            <button
+              onClick={() => {
+                // eslint-disable-next-line n/no-unsupported-features/node-builtins
+                navigator.clipboard.writeText(state.data).catch(() => {
+                  /* no-op on error */
+                });
+              }}
+              className="absolute top-2 right-2 bg-gray-700 text-white px-3 py-1 rounded hover:bg-gray-600"
+            >
+              Copy
+            </button>
+          </div>
+        </div>
+      );
+    }
+    case TxStateType.Error: {
+      return (
+        <div className="mt-4 p-4 bg-red-100 border border-red-400 rounded">
+          <div className="text-red-600">{String(state.error)}</div>
+        </div>
+      );
+    }
+  }
+};
+
+function isValidTxHash(hash: string) {
+  const cleanHash = hash.toLowerCase().replace("0x", "");
+  return /^[\da-f]{64}$/.test(cleanHash);
+}

+ 55 - 0
apps/entropy-debugger/src/components/ui/button.tsx

@@ -0,0 +1,55 @@
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import type { ComponentProps } from "react";
+
+import { cn } from "../../lib/utils";
+
+const buttonVariants = cva(
+  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+  {
+    variants: {
+      variant: {
+        default: "bg-primary text-primary-foreground hover:bg-primary/90",
+        destructive:
+          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+        outline:
+          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+        secondary:
+          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+        ghost: "hover:bg-accent hover:text-accent-foreground",
+        link: "text-primary underline-offset-4 hover:underline",
+      },
+      size: {
+        default: "h-10 px-4 py-2",
+        sm: "h-9 rounded-md px-3",
+        lg: "h-11 rounded-md px-8",
+        icon: "h-10 w-10",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  },
+);
+
+export type ButtonProps = {
+  asChild?: boolean;
+} & ComponentProps<"button"> &
+  VariantProps<typeof buttonVariants>;
+
+export const Button = ({
+  className,
+  variant,
+  size,
+  asChild = false,
+  ...props
+}: ButtonProps) => {
+  const Comp = asChild ? Slot : "button";
+  return (
+    <Comp
+      className={cn(buttonVariants({ variant, size, className }))}
+      {...props}
+    />
+  );
+};

+ 13 - 0
apps/entropy-debugger/src/components/ui/input.tsx

@@ -0,0 +1,13 @@
+import type { ComponentProps } from "react";
+
+import { cn } from "../../lib/utils";
+
+export const Input = ({ className, ...props }: ComponentProps<"input">) => (
+  <input
+    className={cn(
+      "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+      className,
+    )}
+    {...props}
+  />
+);

+ 146 - 0
apps/entropy-debugger/src/components/ui/select.tsx

@@ -0,0 +1,146 @@
+"use client";
+
+import {
+  Trigger,
+  ScrollUpButton,
+  ScrollDownButton,
+  Icon,
+  Portal,
+  Content,
+  Viewport,
+  Label,
+  ItemIndicator,
+  ItemText,
+  Item,
+  Separator,
+} from "@radix-ui/react-select";
+import { Check, ChevronDown, ChevronUp } from "lucide-react";
+import type { ComponentProps } from "react";
+
+import { cn } from "../../lib/utils";
+
+export {
+  Root as Select,
+  Group as SelectGroup,
+  Value as SelectValue,
+} from "@radix-ui/react-select";
+
+export const SelectTrigger = ({
+  className,
+  children,
+  ...props
+}: ComponentProps<typeof Trigger>) => (
+  <Trigger
+    className={cn(
+      "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
+      className,
+    )}
+    {...props}
+  >
+    {children}
+    <Icon asChild>
+      <ChevronDown className="h-4 w-4 opacity-50" />
+    </Icon>
+  </Trigger>
+);
+
+export const SelectScrollUpButton = ({
+  className,
+  ...props
+}: ComponentProps<typeof ScrollUpButton>) => (
+  <ScrollUpButton
+    className={cn(
+      "flex cursor-default items-center justify-center py-1",
+      className,
+    )}
+    {...props}
+  >
+    <ChevronUp className="h-4 w-4" />
+  </ScrollUpButton>
+);
+
+export const SelectScrollDownButton = ({
+  className,
+  ...props
+}: ComponentProps<typeof ScrollDownButton>) => (
+  <ScrollDownButton
+    className={cn(
+      "flex cursor-default items-center justify-center py-1",
+      className,
+    )}
+    {...props}
+  >
+    <ChevronDown className="h-4 w-4" />
+  </ScrollDownButton>
+);
+
+export const SelectContent = ({
+  className,
+  children,
+  position = "popper",
+  ...props
+}: ComponentProps<typeof Content>) => (
+  <Portal>
+    <Content
+      className={cn(
+        "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+        position === "popper" &&
+          "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
+        className,
+      )}
+      position={position}
+      {...props}
+    >
+      <SelectScrollUpButton />
+      <Viewport
+        className={cn(
+          "p-1",
+          position === "popper" &&
+            "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
+        )}
+      >
+        {children}
+      </Viewport>
+      <SelectScrollDownButton />
+    </Content>
+  </Portal>
+);
+
+export const SelectLabel = ({
+  className,
+  ...props
+}: ComponentProps<typeof Label>) => (
+  <Label
+    className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
+    {...props}
+  />
+);
+
+export const SelectItem = ({
+  className,
+  children,
+  ...props
+}: ComponentProps<typeof Item>) => (
+  <Item
+    className={cn(
+      "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+      className,
+    )}
+    {...props}
+  >
+    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+      <ItemIndicator>
+        <Check className="h-4 w-4" />
+      </ItemIndicator>
+    </span>
+
+    <ItemText>{children}</ItemText>
+  </Item>
+);
+
+export const SelectSeparator = ({
+  className,
+  ...props
+}: ComponentProps<typeof Separator>) => (
+  <Separator className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
+);

+ 25 - 0
apps/entropy-debugger/src/components/ui/switch.tsx

@@ -0,0 +1,25 @@
+"use client";
+
+import { Root, Thumb } from "@radix-ui/react-switch";
+import type { ComponentProps } from "react";
+
+import { cn } from "../../lib/utils";
+
+export const Switch = ({
+  className,
+  ...props
+}: ComponentProps<typeof Root>) => (
+  <Root
+    className={cn(
+      "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
+      className,
+    )}
+    {...props}
+  >
+    <Thumb
+      className={cn(
+        "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
+      )}
+    />
+  </Root>
+);

+ 1000 - 0
apps/entropy-debugger/src/lib/entropy-abi.ts

@@ -0,0 +1,1000 @@
+export const EntropyAbi = [
+  {
+    anonymous: false,
+    inputs: [
+      {
+        indexed: false,
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        indexed: false,
+        internalType: "address",
+        name: "oldFeeManager",
+        type: "address",
+      },
+      {
+        indexed: false,
+        internalType: "address",
+        name: "newFeeManager",
+        type: "address",
+      },
+    ],
+    name: "ProviderFeeManagerUpdated",
+    type: "event",
+  },
+  {
+    anonymous: false,
+    inputs: [
+      {
+        indexed: false,
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        indexed: false,
+        internalType: "uint128",
+        name: "oldFee",
+        type: "uint128",
+      },
+      {
+        indexed: false,
+        internalType: "uint128",
+        name: "newFee",
+        type: "uint128",
+      },
+    ],
+    name: "ProviderFeeUpdated",
+    type: "event",
+  },
+  {
+    anonymous: false,
+    inputs: [
+      {
+        indexed: false,
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        indexed: false,
+        internalType: "uint32",
+        name: "oldMaxNumHashes",
+        type: "uint32",
+      },
+      {
+        indexed: false,
+        internalType: "uint32",
+        name: "newMaxNumHashes",
+        type: "uint32",
+      },
+    ],
+    name: "ProviderMaxNumHashesAdvanced",
+    type: "event",
+  },
+  {
+    anonymous: false,
+    inputs: [
+      {
+        indexed: false,
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        indexed: false,
+        internalType: "bytes",
+        name: "oldUri",
+        type: "bytes",
+      },
+      {
+        indexed: false,
+        internalType: "bytes",
+        name: "newUri",
+        type: "bytes",
+      },
+    ],
+    name: "ProviderUriUpdated",
+    type: "event",
+  },
+  {
+    anonymous: false,
+    inputs: [
+      {
+        components: [
+          {
+            internalType: "uint128",
+            name: "feeInWei",
+            type: "uint128",
+          },
+          {
+            internalType: "uint128",
+            name: "accruedFeesInWei",
+            type: "uint128",
+          },
+          {
+            internalType: "bytes32",
+            name: "originalCommitment",
+            type: "bytes32",
+          },
+          {
+            internalType: "uint64",
+            name: "originalCommitmentSequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "bytes",
+            name: "commitmentMetadata",
+            type: "bytes",
+          },
+          {
+            internalType: "bytes",
+            name: "uri",
+            type: "bytes",
+          },
+          {
+            internalType: "uint64",
+            name: "endSequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "uint64",
+            name: "sequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "bytes32",
+            name: "currentCommitment",
+            type: "bytes32",
+          },
+          {
+            internalType: "uint64",
+            name: "currentCommitmentSequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "address",
+            name: "feeManager",
+            type: "address",
+          },
+          {
+            internalType: "uint32",
+            name: "maxNumHashes",
+            type: "uint32",
+          },
+        ],
+        indexed: false,
+        internalType: "struct EntropyStructs.ProviderInfo",
+        name: "provider",
+        type: "tuple",
+      },
+    ],
+    name: "Registered",
+    type: "event",
+  },
+  {
+    anonymous: false,
+    inputs: [
+      {
+        components: [
+          {
+            internalType: "address",
+            name: "provider",
+            type: "address",
+          },
+          {
+            internalType: "uint64",
+            name: "sequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "uint32",
+            name: "numHashes",
+            type: "uint32",
+          },
+          {
+            internalType: "bytes32",
+            name: "commitment",
+            type: "bytes32",
+          },
+          {
+            internalType: "uint64",
+            name: "blockNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "address",
+            name: "requester",
+            type: "address",
+          },
+          {
+            internalType: "bool",
+            name: "useBlockhash",
+            type: "bool",
+          },
+          {
+            internalType: "bool",
+            name: "isRequestWithCallback",
+            type: "bool",
+          },
+        ],
+        indexed: false,
+        internalType: "struct EntropyStructs.Request",
+        name: "request",
+        type: "tuple",
+      },
+    ],
+    name: "Requested",
+    type: "event",
+  },
+  {
+    anonymous: false,
+    inputs: [
+      {
+        indexed: true,
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        indexed: true,
+        internalType: "address",
+        name: "requestor",
+        type: "address",
+      },
+      {
+        indexed: true,
+        internalType: "uint64",
+        name: "sequenceNumber",
+        type: "uint64",
+      },
+      {
+        indexed: false,
+        internalType: "bytes32",
+        name: "userRandomNumber",
+        type: "bytes32",
+      },
+      {
+        components: [
+          {
+            internalType: "address",
+            name: "provider",
+            type: "address",
+          },
+          {
+            internalType: "uint64",
+            name: "sequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "uint32",
+            name: "numHashes",
+            type: "uint32",
+          },
+          {
+            internalType: "bytes32",
+            name: "commitment",
+            type: "bytes32",
+          },
+          {
+            internalType: "uint64",
+            name: "blockNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "address",
+            name: "requester",
+            type: "address",
+          },
+          {
+            internalType: "bool",
+            name: "useBlockhash",
+            type: "bool",
+          },
+          {
+            internalType: "bool",
+            name: "isRequestWithCallback",
+            type: "bool",
+          },
+        ],
+        indexed: false,
+        internalType: "struct EntropyStructs.Request",
+        name: "request",
+        type: "tuple",
+      },
+    ],
+    name: "RequestedWithCallback",
+    type: "event",
+  },
+  {
+    anonymous: false,
+    inputs: [
+      {
+        components: [
+          {
+            internalType: "address",
+            name: "provider",
+            type: "address",
+          },
+          {
+            internalType: "uint64",
+            name: "sequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "uint32",
+            name: "numHashes",
+            type: "uint32",
+          },
+          {
+            internalType: "bytes32",
+            name: "commitment",
+            type: "bytes32",
+          },
+          {
+            internalType: "uint64",
+            name: "blockNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "address",
+            name: "requester",
+            type: "address",
+          },
+          {
+            internalType: "bool",
+            name: "useBlockhash",
+            type: "bool",
+          },
+          {
+            internalType: "bool",
+            name: "isRequestWithCallback",
+            type: "bool",
+          },
+        ],
+        indexed: false,
+        internalType: "struct EntropyStructs.Request",
+        name: "request",
+        type: "tuple",
+      },
+      {
+        indexed: false,
+        internalType: "bytes32",
+        name: "userRevelation",
+        type: "bytes32",
+      },
+      {
+        indexed: false,
+        internalType: "bytes32",
+        name: "providerRevelation",
+        type: "bytes32",
+      },
+      {
+        indexed: false,
+        internalType: "bytes32",
+        name: "blockHash",
+        type: "bytes32",
+      },
+      {
+        indexed: false,
+        internalType: "bytes32",
+        name: "randomNumber",
+        type: "bytes32",
+      },
+    ],
+    name: "Revealed",
+    type: "event",
+  },
+  {
+    anonymous: false,
+    inputs: [
+      {
+        components: [
+          {
+            internalType: "address",
+            name: "provider",
+            type: "address",
+          },
+          {
+            internalType: "uint64",
+            name: "sequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "uint32",
+            name: "numHashes",
+            type: "uint32",
+          },
+          {
+            internalType: "bytes32",
+            name: "commitment",
+            type: "bytes32",
+          },
+          {
+            internalType: "uint64",
+            name: "blockNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "address",
+            name: "requester",
+            type: "address",
+          },
+          {
+            internalType: "bool",
+            name: "useBlockhash",
+            type: "bool",
+          },
+          {
+            internalType: "bool",
+            name: "isRequestWithCallback",
+            type: "bool",
+          },
+        ],
+        indexed: false,
+        internalType: "struct EntropyStructs.Request",
+        name: "request",
+        type: "tuple",
+      },
+      {
+        indexed: false,
+        internalType: "bytes32",
+        name: "userRandomNumber",
+        type: "bytes32",
+      },
+      {
+        indexed: false,
+        internalType: "bytes32",
+        name: "providerRevelation",
+        type: "bytes32",
+      },
+      {
+        indexed: false,
+        internalType: "bytes32",
+        name: "randomNumber",
+        type: "bytes32",
+      },
+    ],
+    name: "RevealedWithCallback",
+    type: "event",
+  },
+  {
+    anonymous: false,
+    inputs: [
+      {
+        indexed: false,
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        indexed: false,
+        internalType: "address",
+        name: "recipient",
+        type: "address",
+      },
+      {
+        indexed: false,
+        internalType: "uint128",
+        name: "withdrawnAmount",
+        type: "uint128",
+      },
+    ],
+    name: "Withdrawal",
+    type: "event",
+  },
+  {
+    inputs: [
+      {
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        internalType: "uint64",
+        name: "advancedSequenceNumber",
+        type: "uint64",
+      },
+      {
+        internalType: "bytes32",
+        name: "providerRevelation",
+        type: "bytes32",
+      },
+    ],
+    name: "advanceProviderCommitment",
+    outputs: [],
+    stateMutability: "nonpayable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes32",
+        name: "userRandomness",
+        type: "bytes32",
+      },
+      {
+        internalType: "bytes32",
+        name: "providerRandomness",
+        type: "bytes32",
+      },
+      {
+        internalType: "bytes32",
+        name: "blockHash",
+        type: "bytes32",
+      },
+    ],
+    name: "combineRandomValues",
+    outputs: [
+      {
+        internalType: "bytes32",
+        name: "combinedRandomness",
+        type: "bytes32",
+      },
+    ],
+    stateMutability: "pure",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes32",
+        name: "userRandomness",
+        type: "bytes32",
+      },
+    ],
+    name: "constructUserCommitment",
+    outputs: [
+      {
+        internalType: "bytes32",
+        name: "userCommitment",
+        type: "bytes32",
+      },
+    ],
+    stateMutability: "pure",
+    type: "function",
+  },
+  {
+    inputs: [],
+    name: "getAccruedPythFees",
+    outputs: [
+      {
+        internalType: "uint128",
+        name: "accruedPythFeesInWei",
+        type: "uint128",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [],
+    name: "getDefaultProvider",
+    outputs: [
+      {
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+    ],
+    name: "getFee",
+    outputs: [
+      {
+        internalType: "uint128",
+        name: "feeAmount",
+        type: "uint128",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+    ],
+    name: "getProviderInfo",
+    outputs: [
+      {
+        components: [
+          {
+            internalType: "uint128",
+            name: "feeInWei",
+            type: "uint128",
+          },
+          {
+            internalType: "uint128",
+            name: "accruedFeesInWei",
+            type: "uint128",
+          },
+          {
+            internalType: "bytes32",
+            name: "originalCommitment",
+            type: "bytes32",
+          },
+          {
+            internalType: "uint64",
+            name: "originalCommitmentSequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "bytes",
+            name: "commitmentMetadata",
+            type: "bytes",
+          },
+          {
+            internalType: "bytes",
+            name: "uri",
+            type: "bytes",
+          },
+          {
+            internalType: "uint64",
+            name: "endSequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "uint64",
+            name: "sequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "bytes32",
+            name: "currentCommitment",
+            type: "bytes32",
+          },
+          {
+            internalType: "uint64",
+            name: "currentCommitmentSequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "address",
+            name: "feeManager",
+            type: "address",
+          },
+          {
+            internalType: "uint32",
+            name: "maxNumHashes",
+            type: "uint32",
+          },
+        ],
+        internalType: "struct EntropyStructs.ProviderInfo",
+        name: "info",
+        type: "tuple",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        internalType: "uint64",
+        name: "sequenceNumber",
+        type: "uint64",
+      },
+    ],
+    name: "getRequest",
+    outputs: [
+      {
+        components: [
+          {
+            internalType: "address",
+            name: "provider",
+            type: "address",
+          },
+          {
+            internalType: "uint64",
+            name: "sequenceNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "uint32",
+            name: "numHashes",
+            type: "uint32",
+          },
+          {
+            internalType: "bytes32",
+            name: "commitment",
+            type: "bytes32",
+          },
+          {
+            internalType: "uint64",
+            name: "blockNumber",
+            type: "uint64",
+          },
+          {
+            internalType: "address",
+            name: "requester",
+            type: "address",
+          },
+          {
+            internalType: "bool",
+            name: "useBlockhash",
+            type: "bool",
+          },
+          {
+            internalType: "bool",
+            name: "isRequestWithCallback",
+            type: "bool",
+          },
+        ],
+        internalType: "struct EntropyStructs.Request",
+        name: "req",
+        type: "tuple",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "uint128",
+        name: "feeInWei",
+        type: "uint128",
+      },
+      {
+        internalType: "bytes32",
+        name: "commitment",
+        type: "bytes32",
+      },
+      {
+        internalType: "bytes",
+        name: "commitmentMetadata",
+        type: "bytes",
+      },
+      {
+        internalType: "uint64",
+        name: "chainLength",
+        type: "uint64",
+      },
+      {
+        internalType: "bytes",
+        name: "uri",
+        type: "bytes",
+      },
+    ],
+    name: "register",
+    outputs: [],
+    stateMutability: "nonpayable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        internalType: "bytes32",
+        name: "userCommitment",
+        type: "bytes32",
+      },
+      {
+        internalType: "bool",
+        name: "useBlockHash",
+        type: "bool",
+      },
+    ],
+    name: "request",
+    outputs: [
+      {
+        internalType: "uint64",
+        name: "assignedSequenceNumber",
+        type: "uint64",
+      },
+    ],
+    stateMutability: "payable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        internalType: "bytes32",
+        name: "userRandomNumber",
+        type: "bytes32",
+      },
+    ],
+    name: "requestWithCallback",
+    outputs: [
+      {
+        internalType: "uint64",
+        name: "assignedSequenceNumber",
+        type: "uint64",
+      },
+    ],
+    stateMutability: "payable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        internalType: "uint64",
+        name: "sequenceNumber",
+        type: "uint64",
+      },
+      {
+        internalType: "bytes32",
+        name: "userRevelation",
+        type: "bytes32",
+      },
+      {
+        internalType: "bytes32",
+        name: "providerRevelation",
+        type: "bytes32",
+      },
+    ],
+    name: "reveal",
+    outputs: [
+      {
+        internalType: "bytes32",
+        name: "randomNumber",
+        type: "bytes32",
+      },
+    ],
+    stateMutability: "nonpayable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        internalType: "uint64",
+        name: "sequenceNumber",
+        type: "uint64",
+      },
+      {
+        internalType: "bytes32",
+        name: "userRandomNumber",
+        type: "bytes32",
+      },
+      {
+        internalType: "bytes32",
+        name: "providerRevelation",
+        type: "bytes32",
+      },
+    ],
+    name: "revealWithCallback",
+    outputs: [],
+    stateMutability: "nonpayable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "address",
+        name: "manager",
+        type: "address",
+      },
+    ],
+    name: "setFeeManager",
+    outputs: [],
+    stateMutability: "nonpayable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "uint32",
+        name: "maxNumHashes",
+        type: "uint32",
+      },
+    ],
+    name: "setMaxNumHashes",
+    outputs: [],
+    stateMutability: "nonpayable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "uint128",
+        name: "newFeeInWei",
+        type: "uint128",
+      },
+    ],
+    name: "setProviderFee",
+    outputs: [],
+    stateMutability: "nonpayable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        internalType: "uint128",
+        name: "newFeeInWei",
+        type: "uint128",
+      },
+    ],
+    name: "setProviderFeeAsFeeManager",
+    outputs: [],
+    stateMutability: "nonpayable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes",
+        name: "newUri",
+        type: "bytes",
+      },
+    ],
+    name: "setProviderUri",
+    outputs: [],
+    stateMutability: "nonpayable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "uint128",
+        name: "amount",
+        type: "uint128",
+      },
+    ],
+    name: "withdraw",
+    outputs: [],
+    stateMutability: "nonpayable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "address",
+        name: "provider",
+        type: "address",
+      },
+      {
+        internalType: "uint128",
+        name: "amount",
+        type: "uint128",
+      },
+    ],
+    name: "withdrawAsFeeManager",
+    outputs: [],
+    stateMutability: "nonpayable",
+    type: "function",
+  },
+] as const;

+ 77 - 0
apps/entropy-debugger/src/lib/revelation.ts

@@ -0,0 +1,77 @@
+import { createPublicClient, http, parseEventLogs, publicActions } from "viem";
+import { z } from "zod";
+
+import { EntropyAbi } from "./entropy-abi";
+import {
+  type EntropyDeployment,
+  EntropyDeployments,
+} from "../store/entropy-deployments";
+
+export async function requestCallback(
+  txHash: string,
+  chain: keyof typeof EntropyDeployments,
+): Promise<string> {
+  const deployment = EntropyDeployments[chain];
+  const { provider, sequenceNumber, userRandomNumber } = await fetchInfoFromTx(
+    txHash,
+    deployment,
+  );
+  const revelation = await getRevelation(chain, Number(sequenceNumber));
+
+  return `cast send ${deployment.address} 'revealWithCallback(address, uint64, bytes32, bytes32)' ${provider} ${sequenceNumber.toString()} ${userRandomNumber} ${revelation.value.data} -r ${deployment.rpc} --private-key <YOUR_PRIVATE_KEY>`;
+}
+
+export async function fetchInfoFromTx(
+  txHash: string,
+  deployment: EntropyDeployment,
+) {
+  const receipt = await createPublicClient({
+    transport: http(deployment.rpc),
+  })
+    .extend(publicActions)
+    .getTransactionReceipt({
+      hash: txHash as `0x${string}`,
+    });
+
+  const logs = parseEventLogs({
+    abi: EntropyAbi,
+    logs: receipt.logs,
+    eventName: "RequestedWithCallback",
+  });
+
+  const firstLog = logs[0];
+  if (firstLog) {
+    const { provider, sequenceNumber, userRandomNumber } = firstLog.args;
+    return { provider, sequenceNumber, userRandomNumber };
+  } else {
+    throw new Error(
+      `No logs found for ${txHash}. Are you sure you send the requestCallback Transaction?`,
+    );
+  }
+}
+
+export async function getRevelation(
+  chain: keyof typeof EntropyDeployments,
+  sequenceNumber: number,
+) {
+  const deployment = EntropyDeployments[chain];
+  const url = new URL(
+    `/v1/chains/${chain}/revelations/${sequenceNumber.toString()}`,
+    deployment.network === "mainnet"
+      ? "https://fortuna.dourolabs.app"
+      : "https://fortuna-staging.dourolabs.app",
+  );
+  const response = await fetch(url);
+
+  if (response.ok) {
+    return revelationSchema.parse(await response.json());
+  } else {
+    throw new Error(`The provider returned an error: ${await response.text()}`);
+  }
+}
+
+const revelationSchema = z.object({
+  value: z.object({
+    data: z.string(),
+  }),
+});

+ 4 - 0
apps/entropy-debugger/src/lib/utils.ts

@@ -0,0 +1,4 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));

+ 395 - 0
apps/entropy-debugger/src/store/entropy-deployments.ts

@@ -0,0 +1,395 @@
+export type EntropyDeployment = {
+  address: string;
+  network: "mainnet" | "testnet";
+  explorer: string;
+  delay: string;
+  gasLimit: string;
+  rpc?: string;
+  nativeCurrency: string;
+};
+
+export const EntropyDeployments = {
+  blast: {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    network: "mainnet",
+    explorer: "https://blastscan.io/address/$ADDRESS",
+    delay: "1 block",
+    gasLimit: "500K",
+    rpc: "https://rpc.blast.io",
+    nativeCurrency: "ETH",
+  },
+  "lightlink-phoenix": {
+    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
+    network: "mainnet",
+    explorer: "https://phoenix.lightlink.io/address/$ADDRESS",
+    delay: "1 block",
+    gasLimit: "500K",
+    rpc: "https://replicator.phoenix.lightlink.io/rpc/v1",
+    nativeCurrency: "ETH",
+  },
+  chiliz: {
+    address: "0x0708325268dF9F66270F1401206434524814508b",
+    network: "mainnet",
+    explorer: "https://scan.chiliz.com/address/$ADDRESS",
+    delay: "12 blocks",
+    gasLimit: "500K",
+    rpc: "https://rpc.ankr.com/chiliz",
+    nativeCurrency: "CHZ",
+  },
+  arbitrum: {
+    address: "0x7698E925FfC29655576D0b361D75Af579e20AdAc",
+    network: "mainnet",
+    explorer: "https://arbiscan.io/address/$ADDRESS",
+    delay: "6 blocks",
+    gasLimit: "2.5M",
+    rpc: "https://arb1.arbitrum.io/rpc",
+    nativeCurrency: "ETH",
+  },
+  optimism: {
+    address: "0xdF21D137Aadc95588205586636710ca2890538d5",
+    network: "mainnet",
+    explorer: "https://optimistic.etherscan.io/address/$ADDRESS",
+    delay: "2 blocks",
+    gasLimit: "500K",
+    rpc: "https://rpc.ankr.com/optimism",
+    nativeCurrency: "ETH",
+  },
+  mode: {
+    address: "0x8D254a21b3C86D32F7179855531CE99164721933",
+    network: "mainnet",
+    explorer: "https://explorer.mode.network/address/$ADDRESS",
+    delay: "2 blocks",
+    gasLimit: "500K",
+    rpc: "https://mainnet.mode.network/",
+    nativeCurrency: "ETH",
+  },
+  zetachain: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    network: "mainnet",
+    explorer: "https://zetachain.blockscout.com/address/$ADDRESS",
+    delay: "0 block",
+    gasLimit: "500K",
+    rpc: "https://zetachain-evm.blockpi.network/v1/rpc/public",
+    nativeCurrency: "ZETA",
+  },
+  base: {
+    address: "0x6E7D74FA7d5c90FEF9F0512987605a6d546181Bb",
+    network: "mainnet",
+    explorer: "https://basescan.org/address/$ADDRESS",
+    delay: "1 block",
+    gasLimit: "500K",
+    rpc: "https://developer-access-mainnet.base.org/",
+    nativeCurrency: "ETH",
+  },
+  "lightlink-pegasus": {
+    rpc: "https://replicator.pegasus.lightlink.io/rpc/v1",
+    network: "testnet",
+    delay: "",
+    address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a",
+    explorer: "https://pegasus.lightlink.io/address/$ADDRESS",
+    gasLimit: "500K",
+    nativeCurrency: "ETH",
+  },
+  "chiliz-spicy": {
+    rpc: "https://spicy-rpc.chiliz.com",
+    network: "testnet",
+    delay: "",
+    address: "0xD458261E832415CFd3BAE5E416FdF3230ce6F134",
+    explorer: "https://spicy-explorer.chiliz.com/address/$ADDRESS",
+    gasLimit: "500K",
+    nativeCurrency: "CHZ",
+  },
+  "conflux-espace-testnet": {
+    rpc: "https://evmtestnet.confluxrpc.com",
+    network: "testnet",
+    delay: "",
+    address: "0xdF21D137Aadc95588205586636710ca2890538d5",
+    explorer: "https://evmtestnet.confluxscan.io/address/$ADDRESS",
+    gasLimit: "500K",
+    nativeCurrency: "CFX",
+  },
+  "mode-sepolia": {
+    rpc: "https://sepolia.mode.network/",
+    network: "testnet",
+    delay: "",
+    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
+    explorer: "https://sepolia.explorer.mode.network/address/$ADDRESS",
+    gasLimit: "500K",
+    nativeCurrency: "ETH",
+  },
+  "sei-evm-testnet": {
+    rpc: "https://evm-rpc-testnet.sei-apis.com",
+    network: "testnet",
+    delay: "",
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    explorer: "https://seitrace.com/address/$ADDRESS?chain=atlantic-2",
+    gasLimit: "500K",
+    nativeCurrency: "SEI",
+  },
+  "arbitrum-sepolia": {
+    rpc: "https://sepolia-rollup.arbitrum.io/rpc",
+    network: "testnet",
+    delay: "",
+    address: "0x549Ebba8036Ab746611B4fFA1423eb0A4Df61440",
+    explorer: "https://sepolia.arbiscan.io/address/$ADDRESS",
+    gasLimit: "2.5M",
+    nativeCurrency: "ETH",
+  },
+  "blast-testnet": {
+    rpc: "https://sepolia.blast.io",
+    network: "testnet",
+    delay: "",
+    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
+    explorer: "https://testnet.blastscan.io/address/$ADDRESS",
+    gasLimit: "500K",
+    nativeCurrency: "ETH",
+  },
+  "optimism-sepolia": {
+    rpc: "https://api.zan.top/opt-sepolia",
+    network: "testnet",
+    delay: "",
+    address: "0x4821932D0CDd71225A6d914706A621e0389D7061",
+    explorer: "https://optimism-sepolia.blockscout.com/address/$ADDRESS",
+    gasLimit: "500K",
+    nativeCurrency: "ETH",
+  },
+  "base-sepolia": {
+    rpc: "https://sepolia.base.org",
+    network: "testnet",
+    delay: "",
+    address: "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c",
+    explorer: "https://base-sepolia.blockscout.com/address/$ADDRESS",
+    gasLimit: "500K",
+    nativeCurrency: "ETH",
+  },
+  "berachain-testnet-v2": {
+    rpc: "https://evm-rpc-bera.rhino-apis.com/",
+    network: "testnet",
+    delay: "",
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    explorer: "https://bartio.beratrail.io/address/$ADDRESS",
+    gasLimit: "500K",
+    nativeCurrency: "BERA",
+  },
+  "coredao-testnet": {
+    rpc: "https://rpc.test.btcs.network",
+    network: "testnet",
+    delay: "",
+    address: "0xf0a1b566B55e0A0CB5BeF52Eb2a57142617Bee67",
+    explorer: "https://scan.test.btcs.network/address/$ADDRESS",
+    gasLimit: "500K",
+    nativeCurrency: "tCORE",
+  },
+  "zetachain-testnet": {
+    rpc: "https://zetachain-athens-evm.blockpi.network/v1/rpc/public",
+    network: "testnet",
+    delay: "",
+    address: "0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF",
+    explorer: "https://explorer.zetachain.com/address/$ADDRESS",
+    gasLimit: "500K",
+    nativeCurrency: "ZETA",
+  },
+  "taiko-hekla": {
+    rpc: "https://rpc.hekla.taiko.xyz/",
+    network: "testnet",
+    delay: "",
+    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
+    explorer: "https://hekla.taikoscan.network/address/$ADDRESS",
+    gasLimit: "500K",
+    nativeCurrency: "ETH",
+  },
+  "orange-testnet": {
+    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
+    explorer: "https://subnets-test.avax.network/orangetest/address/$ADDRESS",
+    delay: "",
+    gasLimit: "500K",
+    network: "testnet",
+    rpc: "https://subnets.avax.network/orangetest/testnet/rpc",
+    nativeCurrency: "JUICE",
+  },
+  "sei-evm-mainnet": {
+    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
+    explorer: "https://seitrace.com/address/$ADDRESS?chain=pacific-1",
+    delay: "1 block",
+    gasLimit: "500K",
+    network: "mainnet",
+    rpc: "https://evm-rpc.sei-apis.com",
+    nativeCurrency: "SEI",
+  },
+  merlin: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    explorer: "https://scan.merlinchain.io/address/$ADDRESS",
+    delay: "1 block",
+    gasLimit: "500K",
+    network: "mainnet",
+    rpc: "https://rpc.merlinchain.io",
+    nativeCurrency: "BTC",
+  },
+  "merlin-testnet": {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    explorer: "https://testnet-scan.merlinchain.io/address/$ADDRESS",
+    delay: "",
+    gasLimit: "500K",
+    network: "testnet",
+    rpc: "https://testnet-rpc.merlinchain.io/",
+    nativeCurrency: "BTC",
+  },
+  taiko: {
+    address: "0x26DD80569a8B23768A1d80869Ed7339e07595E85",
+    explorer: "https://taikoscan.io/address/$ADDRESS",
+    delay: "1 block",
+    gasLimit: "500K",
+    network: "mainnet",
+    rpc: "https://rpc.mainnet.taiko.xyz",
+    nativeCurrency: "ETH",
+  },
+  "etherlink-testnet": {
+    address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
+    explorer: "https://testnet.explorer.etherlink.com/address/$ADDRESS",
+    delay: "",
+    gasLimit: "15M",
+    network: "testnet",
+    rpc: "https://node.ghostnet.etherlink.com",
+    nativeCurrency: "XTZ",
+  },
+  etherlink: {
+    address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
+    explorer: "https://explorer.etherlink.com/address/$ADDRESS",
+    delay: "1 block",
+    gasLimit: "15M",
+    network: "mainnet",
+    rpc: "https://node.mainnet.etherlink.com/",
+    nativeCurrency: "XTZ",
+  },
+  kaia: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    explorer: "https://kaiascan.io/address/$ADDRESS",
+    delay: "1 block",
+    gasLimit: "500K",
+    network: "mainnet",
+    rpc: "https://rpc.ankr.com/klaytn",
+    nativeCurrency: "KLAY",
+  },
+  "kaia-testnet": {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    explorer: "https://kairos.kaiascan.io/address/$ADDRESS",
+    delay: "",
+    gasLimit: "500K",
+    network: "testnet",
+    rpc: "https://rpc.ankr.com/klaytn_testnet",
+    nativeCurrency: "KLAY",
+  },
+  "tabi-testnet": {
+    address: "0xEbe57e8045F2F230872523bbff7374986E45C486",
+    explorer: "https://testnetv2.tabiscan.com/address/$ADDRESS",
+    delay: "",
+    gasLimit: "500K",
+    network: "testnet",
+    rpc: "https://rpc.testnetv2.tabichain.com",
+    nativeCurrency: "TABI",
+  },
+  "b3-testnet": {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    explorer: "https://sepolia.explorer.b3.fun/address/$ADDRESS",
+    delay: "",
+    gasLimit: "500K",
+    network: "testnet",
+    rpc: "https://sepolia.b3.fun/http/",
+    nativeCurrency: "ETH",
+  },
+  "b3-mainnet": {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    explorer: "https://explorer.b3.fun/address/$ADDRESS",
+    delay: "1 block",
+    gasLimit: "500K",
+    network: "mainnet",
+    rpc: "https://mainnet-rpc.b3.fun/http",
+    nativeCurrency: "ETH",
+  },
+  "apechain-testnet": {
+    address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
+    explorer: "https://curtis.explorer.caldera.xyz/address/$ADDRESS",
+    delay: "",
+    gasLimit: "500K",
+    network: "testnet",
+    rpc: "https://curtis.rpc.caldera.xyz/http",
+    nativeCurrency: "APE",
+  },
+  "soneium-minato-testnet": {
+    address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
+    explorer: "https://explorer-testnet.soneium.org/address/$ADDRESS",
+    delay: "",
+    gasLimit: "500K",
+    network: "testnet",
+    rpc: "https://rpc.minato.soneium.org/",
+    nativeCurrency: "ETH",
+  },
+  sanko: {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    explorer: "https://explorer.sanko.xyz/address/$ADDRESS",
+    delay: "1 block",
+    gasLimit: "500K",
+    network: "mainnet",
+    rpc: "https://mainnet.sanko.xyz",
+    nativeCurrency: "DMT",
+  },
+  "sanko-testnet": {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    explorer: "https://sanko-arb-sepolia.explorer.caldera.xyz/address/$ADDRESS",
+    delay: "",
+    gasLimit: "500K",
+    network: "testnet",
+    rpc: "https://sanko-arb-sepolia.rpc.caldera.xyz/http",
+    nativeCurrency: "DMT",
+  },
+  "apechain-mainnet": {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    explorer: "https://apechain.calderaexplorer.xyz/address/$ADDRESS",
+    delay: "1 block",
+    gasLimit: "500K",
+    network: "mainnet",
+    rpc: "https://apechain.calderachain.xyz/http",
+    nativeCurrency: "APE",
+  },
+  "abstract-testnet": {
+    address: "0x858687fD592112f7046E394A3Bf10D0C11fF9e63",
+    explorer: "https://explorer.testnet.abs.xyz/address/$ADDRESS",
+    delay: "",
+    gasLimit: "500K",
+    network: "testnet",
+    rpc: "https://api.testnet.abs.xyz",
+    nativeCurrency: "ETH",
+  },
+  "sonic-fantom-testnet": {
+    address: "0xebe57e8045f2f230872523bbff7374986e45c486",
+    explorer: "https://blaze.soniclabs.com/address/$ADDRESS",
+    delay: "",
+    gasLimit: "500K",
+    network: "testnet",
+    rpc: "https://rpc.blaze.soniclabs.com",
+    nativeCurrency: "S",
+  },
+  "unichain-sepolia": {
+    address: "0x8D254a21b3C86D32F7179855531CE99164721933",
+    explorer: "https://unichain-sepolia.blockscout.com/address/$ADDRESS",
+    delay: "",
+    gasLimit: "500K",
+    network: "testnet",
+    rpc: "https://sepolia.unichain.org",
+    nativeCurrency: "ETH",
+  },
+  sonic: {
+    address: "0x36825bf3fbdf5a29e2d5148bfe7dcf7b5639e320",
+    explorer: "https://sonicscan.org/address/$ADDRESS",
+    delay: "1 block",
+    gasLimit: "500K",
+    network: "mainnet",
+    rpc: "https://rpc.soniclabs.com",
+    nativeCurrency: "S",
+  },
+} as const satisfies Record<string, EntropyDeployment>;
+
+export const isValidDeployment = (
+  name: string,
+): name is keyof typeof EntropyDeployments =>
+  Object.prototype.hasOwnProperty.call(EntropyDeployments, name);

+ 63 - 0
apps/entropy-debugger/tailwind.config.ts

@@ -0,0 +1,63 @@
+import type { Config } from "tailwindcss";
+import animate from "tailwindcss-animate";
+
+export default {
+  darkMode: ["class"],
+  content: [
+    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
+    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
+    "./src/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)",
+      },
+    },
+  },
+  plugins: [animate],
+} satisfies Config;

+ 5 - 0
apps/entropy-debugger/tsconfig.json

@@ -0,0 +1,5 @@
+{
+  "extends": "@cprussin/tsconfig/nextjs.json",
+  "include": ["svg.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+  "exclude": ["node_modules"]
+}

+ 19 - 0
apps/entropy-debugger/turbo.json

@@ -0,0 +1,19 @@
+{
+  "$schema": "https://turbo.build/schema.json",
+  "extends": ["//"],
+  "tasks": {
+    "build": {
+      "dependsOn": ["^build"],
+      "outputs": [".next/**", "!.next/cache/**"]
+    },
+    "start:dev": {
+      "persistent": true,
+      "cache": false
+    },
+    "start:prod": {
+      "dependsOn": ["build"],
+      "persistent": true,
+      "cache": false
+    }
+  }
+}

+ 3 - 0
apps/entropy-debugger/vercel.json

@@ -0,0 +1,3 @@
+{
+  "ignoreCommand": "pnpm dlx turbo-ignore --fallback=HEAD^"
+}

Plik diff jest za duży
+ 397 - 175
pnpm-lock.yaml


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików