diff --git a/.prettierignore b/.prettierignore index c047e85b7..216245c8e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,5 +20,6 @@ patches/ apps/api-reference apps/staking apps/insights +apps/entropy-debug governance/pyth_staking_sdk packages/* diff --git a/apps/entropy-debugger/.gitignore b/apps/entropy-debugger/.gitignore new file mode 100644 index 000000000..9d2ee2a73 --- /dev/null +++ b/apps/entropy-debugger/.gitignore @@ -0,0 +1 @@ +.env*.local diff --git a/apps/entropy-debugger/.prettierignore b/apps/entropy-debugger/.prettierignore new file mode 100644 index 000000000..5f66a649b --- /dev/null +++ b/apps/entropy-debugger/.prettierignore @@ -0,0 +1,7 @@ +.next/ +coverage/ +node_modules/ +*.tsbuildinfo +.env*.local +.env +.DS_Store diff --git a/apps/entropy-debugger/README.md b/apps/entropy-debugger/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/apps/entropy-debugger/components.json b/apps/entropy-debugger/components.json new file mode 100644 index 000000000..dd679c08d --- /dev/null +++ b/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" +} diff --git a/apps/entropy-debugger/eslint.config.js b/apps/entropy-debugger/eslint.config.js new file mode 100644 index 000000000..7035c57cb --- /dev/null +++ b/apps/entropy-debugger/eslint.config.js @@ -0,0 +1 @@ +export { nextjs as default } from "@cprussin/eslint-config"; diff --git a/apps/entropy-debugger/jest.config.js b/apps/entropy-debugger/jest.config.js new file mode 100644 index 000000000..b7edcf4c8 --- /dev/null +++ b/apps/entropy-debugger/jest.config.js @@ -0,0 +1 @@ +export { nextjs as default } from "@cprussin/jest-config"; diff --git a/apps/entropy-debugger/next-env.d.ts b/apps/entropy-debugger/next-env.d.ts new file mode 100644 index 000000000..1b3be0840 --- /dev/null +++ b/apps/entropy-debugger/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/entropy-debugger/next.config.js b/apps/entropy-debugger/next.config.js new file mode 100644 index 000000000..dc9820062 --- /dev/null +++ b/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; diff --git a/apps/entropy-debugger/package.json b/apps/entropy-debugger/package.json new file mode 100644 index 000000000..6d09c2dd5 --- /dev/null +++ b/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:" + } +} diff --git a/apps/entropy-debugger/postcss.config.js b/apps/entropy-debugger/postcss.config.js new file mode 100644 index 000000000..1a69fd2a4 --- /dev/null +++ b/apps/entropy-debugger/postcss.config.js @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/apps/entropy-debugger/prettier.config.js b/apps/entropy-debugger/prettier.config.js new file mode 100644 index 000000000..1e43aeedd --- /dev/null +++ b/apps/entropy-debugger/prettier.config.js @@ -0,0 +1 @@ +export { base as default } from "@cprussin/prettier-config"; diff --git a/apps/entropy-debugger/prettierignore b/apps/entropy-debugger/prettierignore new file mode 100644 index 000000000..5f66a649b --- /dev/null +++ b/apps/entropy-debugger/prettierignore @@ -0,0 +1,7 @@ +.next/ +coverage/ +node_modules/ +*.tsbuildinfo +.env*.local +.env +.DS_Store diff --git a/apps/entropy-debugger/src/app/favicon.ico b/apps/entropy-debugger/src/app/favicon.ico new file mode 100644 index 000000000..718d6fea4 Binary files /dev/null and b/apps/entropy-debugger/src/app/favicon.ico differ diff --git a/apps/entropy-debugger/src/app/fonts/GeistMonoVF.woff b/apps/entropy-debugger/src/app/fonts/GeistMonoVF.woff new file mode 100644 index 000000000..f2ae185cb Binary files /dev/null and b/apps/entropy-debugger/src/app/fonts/GeistMonoVF.woff differ diff --git a/apps/entropy-debugger/src/app/fonts/GeistVF.woff b/apps/entropy-debugger/src/app/fonts/GeistVF.woff new file mode 100644 index 000000000..1b62daacf Binary files /dev/null and b/apps/entropy-debugger/src/app/fonts/GeistVF.woff differ diff --git a/apps/entropy-debugger/src/app/globals.css b/apps/entropy-debugger/src/app/globals.css new file mode 100644 index 000000000..a23ac26b0 --- /dev/null +++ b/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; + } +} diff --git a/apps/entropy-debugger/src/app/layout.tsx b/apps/entropy-debugger/src/app/layout.tsx new file mode 100644 index 000000000..382a9c96a --- /dev/null +++ b/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 ( + + + {children} + + + ); +} diff --git a/apps/entropy-debugger/src/app/page.tsx b/apps/entropy-debugger/src/app/page.tsx new file mode 100644 index 000000000..376904478 --- /dev/null +++ b/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 + | ReturnType + | ReturnType + | ReturnType; + +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); + } + 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 ( +
+

Pyth Entropy Debug App

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

{error.message}

} +
+
+ +
+ +
+ ); +} + +const Info = ({ state }: { state: TxStateContext }) => { + const preRef = useRef(null); + + useEffect(() => { + if (preRef.current && state.status === TxStateType.Success) { + hljs.highlightElement(preRef.current); + } + }, [state]); + + switch (state.status) { + case TxStateType.NotLoaded: { + return
Not loaded
; + } + case TxStateType.Loading: { + return
Loading...
; + } + case TxStateType.Success: { + return ( +
+

+ Please run the following command in your terminal: +

+
+
+              {state.data}
+            
+ +
+
+ ); + } + case TxStateType.Error: { + return ( +
+
{String(state.error)}
+
+ ); + } + } +}; + +function isValidTxHash(hash: string) { + const cleanHash = hash.toLowerCase().replace("0x", ""); + return /^[\da-f]{64}$/.test(cleanHash); +} diff --git a/apps/entropy-debugger/src/components/ui/button.tsx b/apps/entropy-debugger/src/components/ui/button.tsx new file mode 100644 index 000000000..9b598d87e --- /dev/null +++ b/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; + +export const Button = ({ + className, + variant, + size, + asChild = false, + ...props +}: ButtonProps) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); +}; diff --git a/apps/entropy-debugger/src/components/ui/input.tsx b/apps/entropy-debugger/src/components/ui/input.tsx new file mode 100644 index 000000000..5c2e6d538 --- /dev/null +++ b/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">) => ( + +); diff --git a/apps/entropy-debugger/src/components/ui/select.tsx b/apps/entropy-debugger/src/components/ui/select.tsx new file mode 100644 index 000000000..edb852f46 --- /dev/null +++ b/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) => ( + span]:line-clamp-1", + className, + )} + {...props} + > + {children} + + + + +); + +export const SelectScrollUpButton = ({ + className, + ...props +}: ComponentProps) => ( + + + +); + +export const SelectScrollDownButton = ({ + className, + ...props +}: ComponentProps) => ( + + + +); + +export const SelectContent = ({ + className, + children, + position = "popper", + ...props +}: ComponentProps) => ( + + + + + {children} + + + + +); + +export const SelectLabel = ({ + className, + ...props +}: ComponentProps) => ( +