Jelajahi Sumber

Merge pull request #1762 from cprussin/api-reference-improvements

Implement API reference feedback
Connor Prussin 1 tahun lalu
induk
melakukan
4d61c7a695
46 mengubah file dengan 633 tambahan dan 825 penghapusan
  1. 1 0
      apps/api-reference/.gitignore
  2. 2 6
      apps/api-reference/next.config.js
  3. 3 5
      apps/api-reference/package.json
  4. 1 0
      apps/api-reference/public/.well-known/walletconnect.txt
  5. 4 15
      apps/api-reference/src/apis/evm/common.ts
  6. 2 0
      apps/api-reference/src/apis/evm/get-ema-price-no-older-than.ts
  7. 2 0
      apps/api-reference/src/apis/evm/get-ema-price-unsafe.ts
  8. 2 0
      apps/api-reference/src/apis/evm/get-ema-price.ts
  9. 2 0
      apps/api-reference/src/apis/evm/get-price-no-older-than.ts
  10. 2 0
      apps/api-reference/src/apis/evm/get-price-unsafe.ts
  11. 1 0
      apps/api-reference/src/apis/evm/get-price.ts
  12. 6 9
      apps/api-reference/src/apis/evm/get-update-fee.tsx
  13. 1 0
      apps/api-reference/src/apis/evm/get-valid-time-period.ts
  14. 4 2
      apps/api-reference/src/apis/evm/parse-price-feed-updates-unique.tsx
  15. 4 2
      apps/api-reference/src/apis/evm/parse-price-feed-updates.tsx
  16. 4 2
      apps/api-reference/src/apis/evm/update-price-feeds-if-necessary.tsx
  17. 3 2
      apps/api-reference/src/apis/evm/update-price-feeds.tsx
  18. 0 42
      apps/api-reference/src/app/price-feeds/[chain]/[method]/layout.tsx
  19. 13 18
      apps/api-reference/src/app/price-feeds/[chain]/[method]/page.tsx
  20. 0 22
      apps/api-reference/src/app/price-feeds/[chain]/layout.tsx
  21. 0 3
      apps/api-reference/src/app/price-feeds/page.mdx
  22. 16 6
      apps/api-reference/src/components/Amplitude/index.tsx
  23. 41 14
      apps/api-reference/src/components/Button/index.tsx
  24. 13 89
      apps/api-reference/src/components/Code/index.tsx
  25. 124 0
      apps/api-reference/src/components/Code/use-highlighted-code.tsx
  26. 17 9
      apps/api-reference/src/components/EvmApi/index.tsx
  27. 13 1
      apps/api-reference/src/components/EvmApi/parameter-input.tsx
  28. 1 3
      apps/api-reference/src/components/EvmApi/parameter.ts
  29. 11 4
      apps/api-reference/src/components/EvmApi/run-button.tsx
  30. 0 60
      apps/api-reference/src/components/EvmLayout/index.tsx
  31. 60 0
      apps/api-reference/src/components/EvmProvider/index.tsx
  32. 1 1
      apps/api-reference/src/components/Header/index.tsx
  33. 17 7
      apps/api-reference/src/components/Header/nav-link.tsx
  34. 5 5
      apps/api-reference/src/components/Home/index.tsx
  35. 5 22
      apps/api-reference/src/components/InlineLink/index.tsx
  36. 2 20
      apps/api-reference/src/components/MaxWidth/index.tsx
  37. 2 8
      apps/api-reference/src/components/Paragraph/index.tsx
  38. 17 7
      apps/api-reference/src/components/Root/index.tsx
  39. 77 20
      apps/api-reference/src/components/Sidebar/index.tsx
  40. 18 0
      apps/api-reference/src/components/Styled/index.tsx
  41. 12 0
      apps/api-reference/src/markdown-components.tsx
  42. 0 18
      apps/api-reference/src/mdx-components.tsx
  43. 42 1
      apps/api-reference/src/server-config.ts
  44. 1 1
      apps/api-reference/tailwind.config.ts
  45. 19 15
      flake.nix
  46. 62 386
      pnpm-lock.yaml

+ 1 - 0
apps/api-reference/.gitignore

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

+ 2 - 6
apps/api-reference/next.config.js

@@ -1,6 +1,4 @@
-import withMDX from "@next/mdx";
-
-const config = {
+export default {
   reactStrictMode: true,
 
   pageExtensions: ["ts", "tsx", "mdx"],
@@ -24,7 +22,7 @@ const config = {
     return config;
   },
 
-  transpilePackages: ["@pyth.network/*"],
+  transpilePackages: ["@pythnetwork/*"],
 
   headers: () => [
     {
@@ -55,5 +53,3 @@ const config = {
     },
   ],
 };
-
-export default withMDX()(config);

+ 3 - 5
apps/api-reference/package.json

@@ -11,6 +11,7 @@
     "fix": "pnpm fix:format && pnpm fix:lint",
     "fix:format": "prettier --write .",
     "fix:lint": "eslint --fix .",
+    "pull:env": "VERCEL_ORG_ID=team_BKQrg3JJFLxZyTqpuYtIY0rj VERCEL_PROJECT_ID=prj_gbljYVzp0m5EpCuOF6nZpM4WMFM6 vercel env pull",
     "start:dev": "next dev",
     "start:prod": "next start",
     "test": "tsc && jest",
@@ -21,13 +22,10 @@
   },
   "dependencies": {
     "@amplitude/analytics-browser": "^2.9.0",
+    "@amplitude/plugin-autocapture-browser": "^0.9.0",
     "@floating-ui/react": "^0.26.17",
     "@headlessui/react": "^2.0.4",
     "@heroicons/react": "^2.1.4",
-    "@mdx-js/loader": "^3.0.1",
-    "@mdx-js/mdx": "^3.0.1",
-    "@mdx-js/react": "^3.0.1",
-    "@next/mdx": "^14.2.4",
     "@next/third-parties": "^14.2.4",
     "@pythnetwork/pyth-sdk-solidity": "workspace:^",
     "@tanstack/react-query": "^5.45.1",
@@ -39,6 +37,7 @@
     "pino": "^9.2.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
+    "react-markdown": "^9.0.1",
     "shiki": "^1.7.0",
     "viem": "^2.15.1",
     "wagmi": "^2.10.4",
@@ -53,7 +52,6 @@
     "@svgr/webpack": "^8.1.0",
     "@tailwindcss/forms": "^0.5.7",
     "@types/jest": "^29.5.12",
-    "@types/mdx": "^2.0.13",
     "@types/node": "^20.14.6",
     "@types/react": "^18.3.3",
     "@types/react-dom": "^18.3.0",

+ 1 - 0
apps/api-reference/public/.well-known/walletconnect.txt

@@ -0,0 +1 @@
+6486ae45-385f-4747-b8b5-885380fb4ec8=57ef2db8537f4094003f355e80ee6a28a072d4b95baba68db01bac4981ea0f1b

+ 4 - 15
apps/api-reference/src/apis/evm/common.ts

@@ -10,18 +10,14 @@ import {
 import { singletonArray, safeFetch } from "../../zod-utils";
 
 export const readApi = <ParameterName extends string>(
-  spec: Omit<ReadApi<ParameterName>, "children" | "type"> & {
-    description: string;
-  },
+  spec: Omit<ReadApi<ParameterName>, "type">,
 ) => ({
   ...spec,
   type: EvmApiType.Read,
 });
 
 export const writeApi = <ParameterName extends string>(
-  spec: Omit<WriteApi<ParameterName>, "children" | "type"> & {
-    description: string;
-  },
+  spec: Omit<WriteApi<ParameterName>, "type">,
 ) => ({
   ...spec,
   type: EvmApiType.Write,
@@ -34,19 +30,15 @@ export const ETHUSD =
 
 const HERMES_URL = "https://hermes.pyth.network";
 
-export const getLatestPriceFeed = async (feedId: string) => {
+export const getLatestPriceUpdate = async (feedId: string) => {
   const url = new URL("/v2/updates/price/latest", HERMES_URL);
   url.searchParams.set("ids[]", feedId);
-  url.searchParams.set("target_chain", "evm");
-  url.searchParams.set("binary", "true");
   return safeFetch(priceFeedSchema, url);
 };
 
 const priceFeedSchema = z.object({
   binary: z.object({
-    data: singletonArray(z.string()).transform((value) =>
-      toZeroXPrefixedHex(value),
-    ),
+    data: singletonArray(z.string()).transform((value) => `0x${value}`),
   }),
   parsed: singletonArray(
     z.object({
@@ -57,9 +49,6 @@ const priceFeedSchema = z.object({
   ),
 });
 
-const toZeroXPrefixedHex = (value: string) =>
-  `0x${Buffer.from(value, "base64").toString("hex")}`;
-
 export const solidity = <ParameterName extends string>(
   code: string | ((params: Partial<Record<ParameterName, string>>) => string),
 ) => ({

+ 2 - 0
apps/api-reference/src/apis/evm/get-ema-price-no-older-than.ts

@@ -6,6 +6,8 @@ import { ParameterType } from "../../components/EvmApi";
 
 export const getEmaPriceNoOlderThan = readApi<"id" | "age">({
   name: "getEmaPriceNoOlderThan",
+  summary:
+    "Get the exponentially weighted moving average (EMA) price object with a published timestamp from before than `age` seconds in the past.",
   description: `
 Get the latest exponentially-weighted moving average (EMA) price and confidence
 interval for the requested price feed id.  The price feed id is a 32-byte id

+ 2 - 0
apps/api-reference/src/apis/evm/get-ema-price-unsafe.ts

@@ -6,6 +6,8 @@ import { ParameterType } from "../../components/EvmApi";
 
 export const getEmaPriceUnsafe = readApi<"id">({
   name: "getEmaPriceUnsafe",
+  summary:
+    "Get the **last updated** exponentially weighted moving average (EMA) price object for the requested price feed ID. _Caution: This function may return a price arbitrarily in the past_",
   description: `
 Get the latest exponentially-weighted moving average (EMA) price and confidence
 interval for the requested price feed id.  The price feed id is a 32-byte id

+ 2 - 0
apps/api-reference/src/apis/evm/get-ema-price.ts

@@ -6,6 +6,8 @@ import { ParameterType } from "../../components/EvmApi";
 
 export const getEmaPrice = readApi<"id">({
   name: "getEmaPrice",
+  summary:
+    "Get the **latest** exponentially weighted moving average (EMA) price object for the requested price feed ID.",
   description: `
 Get the latest exponentially-weighted moving average (EMA) price and confidence
 interval for the requested price feed id.  The price feed id is a 32-byte id

+ 2 - 0
apps/api-reference/src/apis/evm/get-price-no-older-than.ts

@@ -6,6 +6,8 @@ import { ParameterType } from "../../components/EvmApi";
 
 export const getPriceNoOlderThan = readApi<"id" | "age">({
   name: "getPriceNoOlderThan",
+  summary:
+    "Get the price object with a published timestamp from before than `age` seconds in the past.",
   description: `
 Get the latest price and confidence interval for the requested price feed id, if
 it has been updated sufficiently recently.  The price feed id is a 32-byte id

+ 2 - 0
apps/api-reference/src/apis/evm/get-price-unsafe.ts

@@ -6,6 +6,8 @@ import { ParameterType } from "../../components/EvmApi";
 
 export const getPriceUnsafe = readApi<"id">({
   name: "getPriceUnsafe",
+  summary:
+    "Get the **last updated** price object for the requested price feed ID. _Caution: This function may return a price from arbitrarily in the the past_",
   description: `
 Get the latest price and confidence interval for the requested price feed id.
 The price feed id is a 32-byte id written as a hexadecimal string; see the

+ 1 - 0
apps/api-reference/src/apis/evm/get-price.ts

@@ -6,6 +6,7 @@ import { ParameterType } from "../../components/EvmApi";
 
 export const getPrice = readApi<"id">({
   name: "getPrice",
+  summary: "Get the **latest** price object for the requested price feed ID.",
   description: `
 Get the latest price and confidence interval for the requested price feed id.
 The price feed id is a 32-byte id written as a hexadecimal string; see the

+ 6 - 9
apps/api-reference/src/apis/evm/get-update-fee.tsx

@@ -5,15 +5,16 @@ import {
   readApi,
   BTCUSD,
   ETHUSD,
-  getLatestPriceFeed,
+  getLatestPriceUpdate,
   solidity,
   ethersJS,
 } from "./common";
 import { ParameterType } from "../../components/EvmApi";
-import { InlineLink } from "../../components/InlineLink";
 
 export const getUpdateFee = readApi<"updateData">({
   name: "getUpdateFee",
+  summary:
+    "Get the fee required to update the on-chain price feeds with the provided `updateData`.",
   description: `
 Get the fee required to update the on-chain price feeds with the provided
 \`updateData\`.  The returned number of wei should be sent as the transaction
@@ -24,12 +25,8 @@ can be retrieved from the [Hermes API](https://hermes.pyth.network/docs).
     {
       name: "updateData",
       type: ParameterType.HexArray,
-      description: (
-        <>
-          The price updates that you would like to submit to{" "}
-          <InlineLink href="updatePriceFeeds">updatePriceFeeds</InlineLink>
-        </>
-      ),
+      description:
+        "The price updates that you would like to submit to [updatePriceFeeds](updatePriceFeeds).",
     },
   ],
   examples: [
@@ -62,6 +59,6 @@ const [feeAmount] = await contract.getUpdateFee(updateData);
 });
 
 const getParams = async (feedId: string) => {
-  const feed = await getLatestPriceFeed(feedId);
+  const feed = await getLatestPriceUpdate(feedId);
   return { updateData: feed.binary.data };
 };

+ 1 - 0
apps/api-reference/src/apis/evm/get-valid-time-period.ts

@@ -2,6 +2,7 @@ import { readApi, solidity, ethersJS } from "./common";
 
 export const getValidTimePeriod = readApi<never>({
   name: "getValidTimePeriod",
+  summary: "Get the default valid time period of price freshness in seconds.",
   description: `
 Get the default valid time period in seconds.  This quantity is the maximum age
 of price updates returned by functions like [getPrice](getPrice) and

+ 4 - 2
apps/api-reference/src/apis/evm/parse-price-feed-updates-unique.tsx

@@ -4,7 +4,7 @@ import Eth from "cryptocurrency-icons/svg/color/eth.svg";
 import {
   BTCUSD,
   ETHUSD,
-  getLatestPriceFeed,
+  getLatestPriceUpdate,
   solidity,
   ethersJS,
   writeApi,
@@ -15,6 +15,8 @@ export const parsePriceFeedUpdatesUnique = writeApi<
   "updateData" | "priceId" | "minPublishTime" | "maxPublishTime" | "fee"
 >({
   name: "parsePriceFeedUpdatesUnique",
+  summary:
+    "Parse `updateData` to return the **first updated** prices if the prices are published within the given time range.",
   description: `
 Parse \`updateData\` and return the price feeds for the given \`priceIds\`
 within, if they are all **the first updates** published between
@@ -114,7 +116,7 @@ const getParams = async (
     readContract: (name: string, args: unknown[]) => Promise<unknown>;
   },
 ) => {
-  const feed = await getLatestPriceFeed(priceId);
+  const feed = await getLatestPriceUpdate(priceId);
   const fee = await ctx.readContract("getUpdateFee", [[feed.binary.data]]);
   if (typeof fee !== "bigint") {
     throw new TypeError("Invalid fee");

+ 4 - 2
apps/api-reference/src/apis/evm/parse-price-feed-updates.tsx

@@ -4,7 +4,7 @@ import Eth from "cryptocurrency-icons/svg/color/eth.svg";
 import {
   BTCUSD,
   ETHUSD,
-  getLatestPriceFeed,
+  getLatestPriceUpdate,
   solidity,
   ethersJS,
   writeApi,
@@ -15,6 +15,8 @@ export const parsePriceFeedUpdates = writeApi<
   "updateData" | "priceId" | "minPublishTime" | "maxPublishTime" | "fee"
 >({
   name: "parsePriceFeedUpdates",
+  summary:
+    "Parse `updateData` to return prices if the prices are published within the given time range.",
   description: `
 Parse \`updateData\` and return the price feeds for the given \`priceIds\`
 within, if they are all published between \`minPublishTime\` and
@@ -112,7 +114,7 @@ const getParams = async (
     readContract: (name: string, args: unknown[]) => Promise<unknown>;
   },
 ) => {
-  const feed = await getLatestPriceFeed(priceId);
+  const feed = await getLatestPriceUpdate(priceId);
   const fee = await ctx.readContract("getUpdateFee", [[feed.binary.data]]);
   if (typeof fee !== "bigint") {
     throw new TypeError("Invalid fee");

+ 4 - 2
apps/api-reference/src/apis/evm/update-price-feeds-if-necessary.tsx

@@ -4,7 +4,7 @@ import Eth from "cryptocurrency-icons/svg/color/eth.svg";
 import {
   BTCUSD,
   ETHUSD,
-  getLatestPriceFeed,
+  getLatestPriceUpdate,
   solidity,
   ethersJS,
   writeApi,
@@ -15,6 +15,8 @@ export const updatePriceFeedsIfNecessary = writeApi<
   "updateData" | "priceId" | "publishTime" | "fee"
 >({
   name: "updatePriceFeedsIfNecessary",
+  summary:
+    "Update the on-chain price feeds using the provided `updateData` only if the on-chain prices are older than the valid time period.",
   description: `
 Update the on-chain price feeds using the provided \`updateData\` if the
 on-chain data is not sufficiently fresh.  The caller provides two matched
@@ -107,7 +109,7 @@ const getParams = async (
     readContract: (name: string, args: unknown[]) => Promise<unknown>;
   },
 ) => {
-  const feed = await getLatestPriceFeed(priceId);
+  const feed = await getLatestPriceUpdate(priceId);
   const fee = await ctx.readContract("getUpdateFee", [[feed.binary.data]]);
   if (typeof fee !== "bigint") {
     throw new TypeError("Invalid fee");

+ 3 - 2
apps/api-reference/src/apis/evm/update-price-feeds.tsx

@@ -4,7 +4,7 @@ import Eth from "cryptocurrency-icons/svg/color/eth.svg";
 import {
   BTCUSD,
   ETHUSD,
-  getLatestPriceFeed,
+  getLatestPriceUpdate,
   solidity,
   ethersJS,
   writeApi,
@@ -13,6 +13,7 @@ import { ParameterType } from "../../components/EvmApi";
 
 export const updatePriceFeeds = writeApi<"updateData" | "fee">({
   name: "updatePriceFeeds",
+  summary: "Update the on-chain price feeds using the provided `updateData`.",
   description: `
 Update the on-chain price feeds using the provided \`updateData\`, which
 contains serialized and signed price update data from Pyth Network.  You can
@@ -81,7 +82,7 @@ const getParams = async (
     readContract: (name: string, args: unknown[]) => Promise<unknown>;
   },
 ) => {
-  const feed = await getLatestPriceFeed(feedId);
+  const feed = await getLatestPriceUpdate(feedId);
   const fee = await ctx.readContract("getUpdateFee", [[feed.binary.data]]);
   if (typeof fee !== "bigint") {
     throw new TypeError("Invalid fee");

+ 0 - 42
apps/api-reference/src/app/price-feeds/[chain]/[method]/layout.tsx

@@ -1,42 +0,0 @@
-"use client";
-
-import { notFound } from "next/navigation";
-import type { ReactNode, ComponentProps } from "react";
-
-import * as apis from "../../../../apis";
-import { EvmApi } from "../../../../components/EvmApi";
-
-type Props = {
-  params: {
-    chain: string;
-    method: string;
-  };
-  children: ReactNode;
-};
-
-const Layout = ({ params, children }: Props) => {
-  const chain: (typeof apis)[keyof typeof apis] | undefined = isKeyOf(
-    params.chain,
-    apis,
-  )
-    ? // eslint-disable-next-line import/namespace
-      apis[params.chain]
-    : undefined;
-  const api =
-    chain && isKeyOf(params.method, chain) ? chain[params.method] : undefined;
-  if (api) {
-    return (
-      <EvmApi {...(api as unknown as ComponentProps<typeof EvmApi>)}>
-        {children}
-      </EvmApi>
-    );
-  } else {
-    notFound();
-  }
-};
-export default Layout;
-
-const isKeyOf = <T extends Record<string, unknown>>(
-  value: unknown,
-  obj: T,
-): value is keyof T => typeof value === "string" && value in obj;

+ 13 - 18
apps/api-reference/src/app/price-feeds/[chain]/[method]/page.tsx

@@ -1,9 +1,10 @@
-import { evaluate } from "@mdx-js/mdx";
+"use client";
+
 import { notFound } from "next/navigation";
-import * as runtime from "react/jsx-runtime";
+import type { ComponentProps } from "react";
 
 import * as apis from "../../../../apis";
-import { useMDXComponents } from "../../../../mdx-components";
+import { EvmApi } from "../../../../components/EvmApi";
 
 type Props = {
   params: {
@@ -12,17 +13,18 @@ type Props = {
   };
 };
 
-const Page = async ({ params }: Props) => {
-  const mdxComponents = useMDXComponents({});
-  // eslint-disable-next-line import/namespace
-  const chain = isKeyOf(params.chain, apis) ? apis[params.chain] : undefined;
+const Page = ({ params }: Props) => {
+  const chain: (typeof apis)[keyof typeof apis] | undefined = isKeyOf(
+    params.chain,
+    apis,
+  )
+    ? // eslint-disable-next-line import/namespace
+      apis[params.chain]
+    : undefined;
   const api =
     chain && isKeyOf(params.method, chain) ? chain[params.method] : undefined;
   if (api) {
-    // @ts-expect-error for some reason these types aren't unifying, it's
-    // probably a dependency versioning issue
-    const { default: Description } = await evaluate(api.description, runtime);
-    return <Description components={mdxComponents} />;
+    return <EvmApi {...(api as unknown as ComponentProps<typeof EvmApi>)} />;
   } else {
     notFound();
   }
@@ -33,10 +35,3 @@ const isKeyOf = <T extends Record<string, unknown>>(
   value: unknown,
   obj: T,
 ): value is keyof T => typeof value === "string" && value in obj;
-
-export const generateStaticParams = () =>
-  Object.entries(apis).flatMap(([chain, methods]) =>
-    Object.keys(methods).map((method) => ({ chain, method })),
-  );
-
-export const dynamicParams = false;

+ 0 - 22
apps/api-reference/src/app/price-feeds/[chain]/layout.tsx

@@ -1,22 +0,0 @@
-import type { ReactNode } from "react";
-
-import { EvmLayout } from "../../../components/EvmLayout";
-
-type Props = {
-  params: {
-    chain: string;
-  };
-  children: ReactNode;
-};
-
-const Layout = ({ params, children }: Props) => {
-  switch (params.chain) {
-    case "evm": {
-      return <EvmLayout>{children}</EvmLayout>;
-    }
-    default: {
-      return children;
-    }
-  }
-};
-export default Layout;

+ 0 - 3
apps/api-reference/src/app/price-feeds/page.mdx

@@ -1,3 +0,0 @@
-# Price Feeds
-
-Select an API from the side bar to get started!

+ 16 - 6
apps/api-reference/src/components/Amplitude/index.tsx

@@ -1,16 +1,26 @@
 "use client";
 
-import { init } from "@amplitude/analytics-browser";
-import { useEffect } from "react";
+import * as amplitude from "@amplitude/analytics-browser";
+import { autocapturePlugin } from "@amplitude/plugin-autocapture-browser";
+import { useEffect, useRef } from "react";
 
 type Props = {
-  key: string;
+  apiKey: string | undefined;
 };
 
-export const Amplitude = ({ key }: Props) => {
+export const Amplitude = ({ apiKey }: Props) => {
+  const amplitudeInitialized = useRef(false);
+
   useEffect(() => {
-    init(key);
-  }, [key]);
+    if (!amplitudeInitialized.current && apiKey) {
+      amplitude.add(autocapturePlugin());
+      amplitude.init(apiKey, {
+        defaultTracking: true,
+      });
+      amplitudeInitialized.current = true;
+    }
+  }, [apiKey]);
+
   // eslint-disable-next-line unicorn/no-null
   return null;
 };

+ 41 - 14
apps/api-reference/src/components/Button/index.tsx

@@ -5,16 +5,24 @@ import {
   type ComponentProps,
   type ElementType,
   type MouseEvent,
+  type CSSProperties,
   useState,
   useCallback,
-  type CSSProperties,
 } from "react";
 
+const DEFAULT_GRADIENT_SIZE = "30rem";
+
 type ButtonProps<T extends ElementType> = Omit<ComponentProps<T>, "as"> & {
   as?: T;
   loading?: boolean | undefined;
   gradient?: boolean | undefined;
-};
+} & (
+    | { gradient?: false | undefined }
+    | {
+        gradient: true;
+        gradientSize?: string | undefined;
+      }
+  );
 
 export const Button = <T extends ElementType>({
   as,
@@ -22,6 +30,7 @@ export const Button = <T extends ElementType>({
   loading,
   disabled,
   gradient,
+  children,
   ...props
 }: ButtonProps<T>) => {
   const Component = as ?? "button";
@@ -38,23 +47,15 @@ export const Button = <T extends ElementType>({
     <Component
       disabled={loading === true || disabled === true}
       onMouseMove={updateMouse}
-      style={
-        gradient
-          ? ({
-              "--gradient-left": `${mouse.x.toString()}px`,
-              "--gradient-top": `${mouse.y.toString()}px`,
-            } as CSSProperties)
-          : {}
-      }
       className={clsx(
-        "relative overflow-hidden rounded-lg border text-sm font-medium transition-all duration-300",
+        "group relative overflow-hidden rounded-lg border text-sm font-medium transition-all duration-300",
         {
           "border-neutral-400 hover:border-pythpurple-600 hover:shadow-md dark:border-neutral-600 dark:hover:border-pythpurple-400 dark:hover:shadow-white/20":
             !loading && !disabled,
-          "before:absolute before:left-[var(--gradient-left)] before:top-[var(--gradient-top)] before:-ml-[20rem] before:-mt-[20rem] before:block before:size-[40rem] before:scale-0 before:bg-gradient-radial before:from-pythpurple-400/30 before:to-70% before:opacity-50 before:transition before:duration-500 hover:before:scale-100 hover:before:opacity-100 dark:before:from-pythpurple-600/30":
-            gradient && !loading && !disabled,
           "bg-pythpurple-600/5 hover:bg-pythpurple-600/15 dark:bg-pythpurple-400/5 dark:hover:bg-pythpurple-400/15":
             !gradient && !loading && !disabled,
+          "active:bg-pythpurple-400/10 dark:active:bg-pythpurple-600/10":
+            gradient && !loading && !disabled,
           "border-neutral-200 bg-neutral-100 text-neutral-400 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-500":
             loading === true || disabled === true,
           "cursor-not-allowed": disabled === true && loading !== true,
@@ -63,6 +64,32 @@ export const Button = <T extends ElementType>({
         className,
       )}
       {...props}
-    />
+    >
+      {gradient && !loading && !disabled && (
+        <Gradient size={props.gradientSize} x={mouse.x} y={mouse.y} />
+      )}
+      {children}
+    </Component>
   );
 };
+
+type GradientProps = {
+  x: number;
+  y: number;
+  size?: string | undefined;
+};
+
+const Gradient = ({ size = DEFAULT_GRADIENT_SIZE, x, y }: GradientProps) => (
+  <div
+    style={
+      {
+        "--gradient-left": `${x.toString()}px`,
+        "--gradient-top": `${y.toString()}px`,
+        "--gradient-size": size,
+      } as CSSProperties
+    }
+    className="pointer-events-none absolute left-0 top-0 -z-10 ml-[calc(-1_*_var(--gradient-size)_/_2)] mt-[calc(-1_*_var(--gradient-size)_/_2)] block size-[var(--gradient-size)] translate-x-[var(--gradient-left)] translate-y-[var(--gradient-top)]"
+  >
+    <div className="size-full scale-0 bg-gradient-radial from-pythpurple-400 to-70% opacity-10 transition duration-500 group-hover:scale-100 group-hover:opacity-30 group-active:scale-150 group-active:opacity-40 dark:from-pythpurple-600" />
+  </div>
+);

+ 13 - 89
apps/api-reference/src/components/Code/index.tsx

@@ -2,11 +2,12 @@ import { Transition } from "@headlessui/react";
 import { ClipboardDocumentIcon, CheckIcon } from "@heroicons/react/24/outline";
 import clsx from "clsx";
 import { useMemo, useCallback, type HTMLAttributes } from "react";
-import { useRef, useEffect, useState } from "react";
+import { useEffect, useState } from "react";
 import type { OffsetOrPosition } from "shiki";
 
-import type { Highlighter, SupportedLanguage } from "./shiki";
+import type { SupportedLanguage } from "./shiki";
 import style from "./style.module.css";
+import { useHighlightedCode } from "./use-highlighted-code";
 import { getLogger } from "../../browser-logger";
 import { Button } from "../Button";
 
@@ -87,8 +88,17 @@ const CopyButton = ({ children, className, ...props }: CopyButtonProps) => {
       className={clsx("bg-neutral-100 dark:bg-neutral-800", className)}
       {...props}
     >
+      <Button
+        onClick={copy}
+        className="rounded-md p-2 text-neutral-800 dark:text-neutral-300"
+      >
+        <ClipboardDocumentIcon className="size-4" />
+        <div className="sr-only">Copy code to clipboaord</div>
+      </Button>
       <Transition
         show={isCopied}
+        as="div"
+        className="absolute inset-0 rounded-md border border-green-500 bg-green-50 p-2 text-green-700 dark:border-green-700 dark:bg-green-950 dark:text-green-500"
         enter="transition-opacity duration-150"
         enterFrom="opacity-0"
         enterTo="opacity-100"
@@ -96,17 +106,8 @@ const CopyButton = ({ children, className, ...props }: CopyButtonProps) => {
         leaveFrom="opacity-100"
         leaveTo="opacity-0"
       >
-        <div className="absolute size-full rounded-md border border-green-500 bg-green-50 p-2 text-green-700 dark:border-green-700 dark:bg-green-950 dark:text-green-500">
-          <CheckIcon className="size-4 stroke-2" />
-        </div>
+        <CheckIcon className="size-4 stroke-2" />
       </Transition>
-      <Button
-        onClick={copy}
-        className="rounded-md p-2 text-neutral-800 dark:text-neutral-300"
-      >
-        <ClipboardDocumentIcon className="size-4" />
-        <div className="sr-only">Copy code to clipboaord</div>
-      </Button>
     </div>
   );
 };
@@ -141,80 +142,3 @@ const HighlightedCode = ({
     </div>
   );
 };
-
-const useHighlightedCode = (
-  language: SupportedLanguage,
-  code: string,
-  dimRange?: readonly [OffsetOrPosition, OffsetOrPosition] | undefined,
-) => {
-  const [highlightedCode, setHighlightedCode] = useState<string | undefined>(
-    undefined,
-  );
-  const highlighter = useRef<Highlighter | undefined>(undefined);
-  const decorations = useMemo(
-    () =>
-      dimRange
-        ? [
-            {
-              start: dimRange[0],
-              end: dimRange[1],
-              properties: {
-                class: "opacity-40 group-hover:opacity-100 transition",
-              },
-            },
-          ]
-        : undefined,
-    [dimRange],
-  );
-
-  useEffect(() => {
-    if (highlighter.current) {
-      setHighlightedCode(
-        highlighter.current.highlight(language, code, { decorations }),
-      );
-      return;
-    } else {
-      const { cancel, load } = createShikiLoader();
-      load()
-        .then((newHighlighter) => {
-          if (newHighlighter) {
-            highlighter.current = newHighlighter;
-            setHighlightedCode(
-              newHighlighter.highlight(language, code, { decorations }),
-            );
-          }
-        })
-        .catch((error: unknown) => {
-          // TODO report these errors somewhere
-          getLogger().error(error);
-        });
-      return cancel;
-    }
-  }, [code, language, decorations]);
-
-  return highlightedCode;
-};
-
-const createShikiLoader = () => {
-  let cancelled = false;
-  return {
-    load: async () => {
-      const { getHighlighter } = await import("./shiki");
-      if (cancelled) {
-        return;
-      } else {
-        const highlighter = await getHighlighter();
-        // Typescript narrows optimistically, meaning that by the time the code
-        // reaches this point, typescript things that `cancelled` can only be
-        // `false`.  However, that's not actually true and some other code could
-        // have called `cancel` during the `await` and flipped `cancelled` to
-        // false, so we should check it here too.
-        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-        return cancelled ? undefined : highlighter;
-      }
-    },
-    cancel: () => {
-      cancelled = true;
-    },
-  };
-};

+ 124 - 0
apps/api-reference/src/components/Code/use-highlighted-code.tsx

@@ -0,0 +1,124 @@
+"use client";
+
+import {
+  type ReactNode,
+  type MutableRefObject,
+  createContext,
+  useContext,
+  useState,
+  useMemo,
+  useEffect,
+  useRef,
+} from "react";
+import type { OffsetOrPosition } from "shiki";
+
+import type { Highlighter, SupportedLanguage } from "./shiki";
+import { getLogger } from "../../browser-logger";
+
+const HighlighterContext = createContext<
+  undefined | MutableRefObject<undefined | Highlighter>
+>(undefined);
+
+export const HighlighterProvider = ({
+  children,
+}: {
+  children: ReactNode | ReactNode[];
+}) => {
+  const highlighterRef = useRef<undefined | Highlighter>();
+  return (
+    <HighlighterContext.Provider value={highlighterRef}>
+      {children}
+    </HighlighterContext.Provider>
+  );
+};
+
+const useHighlighter = () => {
+  const highlighter = useContext(HighlighterContext);
+
+  if (highlighter === undefined) {
+    throw new Error(
+      "The `HighlighterProvider` component must be used to initialized the highlighter!",
+    );
+  }
+
+  return highlighter;
+};
+
+export const useHighlightedCode = (
+  language: SupportedLanguage,
+  code: string,
+  dimRange?: readonly [OffsetOrPosition, OffsetOrPosition] | undefined,
+) => {
+  const highlighter = useHighlighter();
+  const decorations = useMemo(
+    () =>
+      dimRange
+        ? [
+            {
+              start: dimRange[0],
+              end: dimRange[1],
+              properties: {
+                class: "opacity-40 group-hover:opacity-100 transition",
+              },
+            },
+          ]
+        : undefined,
+    [dimRange],
+  );
+  const [highlightedCode, setHighlightedCode] = useState<string | undefined>(
+    highlighter.current
+      ? highlighter.current.highlight(language, code, { decorations })
+      : undefined,
+  );
+
+  useEffect(() => {
+    if (highlighter.current) {
+      setHighlightedCode(
+        highlighter.current.highlight(language, code, { decorations }),
+      );
+      return;
+    } else {
+      const { cancel, load } = createShikiLoader();
+      load()
+        .then((newHighlighter) => {
+          if (newHighlighter) {
+            highlighter.current = newHighlighter;
+            setHighlightedCode(
+              newHighlighter.highlight(language, code, { decorations }),
+            );
+          }
+        })
+        .catch((error: unknown) => {
+          // TODO report these errors somewhere
+          getLogger().error(error);
+        });
+      return cancel;
+    }
+  }, [code, language, decorations, highlighter]);
+
+  return highlightedCode;
+};
+
+const createShikiLoader = () => {
+  let cancelled = false;
+  return {
+    load: async () => {
+      const { getHighlighter } = await import("./shiki");
+      if (cancelled) {
+        return;
+      } else {
+        const highlighter = await getHighlighter();
+        // Typescript narrows optimistically, meaning that by the time the code
+        // reaches this point, typescript things that `cancelled` can only be
+        // `false`.  However, that's not actually true and some other code could
+        // have called `cancel` during the `await` and flipped `cancelled` to
+        // false, so we should check it here too.
+        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+        return cancelled ? undefined : highlighter;
+      }
+    },
+    cancel: () => {
+      cancelled = true;
+    },
+  };
+};

+ 17 - 9
apps/api-reference/src/components/EvmApi/index.tsx

@@ -14,23 +14,25 @@ import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
 import PythErrorsAbi from "@pythnetwork/pyth-sdk-solidity/abis/PythErrors.json";
 import { ChainIcon } from "connectkit";
 import {
-  type ReactNode,
   type Dispatch,
   type SetStateAction,
   type ComponentProps,
+  type ElementType,
+  type SVGAttributes,
   useState,
   useCallback,
   useMemo,
-  type ComponentType,
-  type SVGAttributes,
 } from "react";
+import Markdown from "react-markdown";
 import { useSwitchChain, useChainId, useConfig } from "wagmi";
 import { readContract } from "wagmi/actions";
 
 import { getContractAddress } from "./networks";
-import { type Parameter } from "./parameter";
+import type { Parameter } from "./parameter";
 import { ParameterInput } from "./parameter-input";
 import { type EvmApiType, RunButton } from "./run-button";
+import { getLogger } from "../../browser-logger";
+import { MARKDOWN_COMPONENTS } from "../../markdown-components";
 import { useIsMounted } from "../../use-is-mounted";
 import { type SupportedLanguage, Code } from "../Code";
 import { ErrorTooltip } from "../ErrorTooltip";
@@ -48,7 +50,8 @@ type Props<ParameterName extends string> =
 
 type Common<ParameterName extends string> = {
   name: (typeof PythAbi)[number]["name"];
-  children: ReactNode;
+  summary: string;
+  description: string;
   parameters: Parameter<ParameterName>[];
   examples: Example<ParameterName>[];
   code: CodeSample<ParameterName>[];
@@ -65,7 +68,7 @@ export type WriteApi<ParameterName extends string> = Common<ParameterName> & {
 
 type Example<ParameterName extends string> = {
   name: string;
-  icon?: ComponentType<SVGAttributes<SVGSVGElement>>;
+  icon?: ElementType<SVGAttributes<SVGSVGElement>>;
   parameters: ValueOrFunctionOrAsyncFunction<Record<ParameterName, string>>;
 };
 
@@ -100,7 +103,8 @@ export type NetworkInfo = {
 
 export const EvmApi = <ParameterName extends string>({
   name,
-  children,
+  summary,
+  description,
   parameters,
   code,
   examples,
@@ -125,11 +129,14 @@ export const EvmApi = <ParameterName extends string>({
   return (
     <div className="gap-x-20 lg:grid lg:grid-cols-[2fr_1fr]">
       <h1 className="col-span-2 mb-6 font-mono text-4xl font-medium">{name}</h1>
+      <div className="col-span-2 mb-6 opacity-60">
+        <Markdown components={MARKDOWN_COMPONENTS}>{summary}</Markdown>
+      </div>
       <section>
         <h2 className="mb-4 border-b border-neutral-200 text-2xl/loose font-medium dark:border-neutral-800">
           Description
         </h2>
-        {children}
+        <Markdown components={MARKDOWN_COMPONENTS}>{description}</Markdown>
       </section>
       <section className="flex flex-col">
         <h2 className="mb-4 border-b border-neutral-200 text-2xl/loose font-medium dark:border-neutral-800">
@@ -282,7 +289,8 @@ const Example = <ParameterName extends string>({
           .then((paramsResolved) => {
             setParamValues(paramsResolved);
           })
-          .catch(() => {
+          .catch((error_: unknown) => {
+            getLogger().error(error_);
             setError(
               "An error occurred while fetching data for this example, please try again",
             );

+ 13 - 1
apps/api-reference/src/components/EvmApi/parameter-input.tsx

@@ -2,11 +2,13 @@ import {
   type ChangeEvent,
   type Dispatch,
   type SetStateAction,
+  Fragment,
   useState,
   useCallback,
   useMemo,
   useEffect,
 } from "react";
+import Markdown from "react-markdown";
 
 import {
   type Parameter,
@@ -14,6 +16,7 @@ import {
   isValid,
   getValidationError,
 } from "./parameter";
+import { MARKDOWN_COMPONENTS } from "../../markdown-components";
 import { Input } from "../Input";
 
 type ParameterProps<ParameterName extends string> = {
@@ -39,7 +42,16 @@ export const ParameterInput = <ParameterName extends string>({
     <Input
       validationError={validationError}
       label={spec.name}
-      description={spec.description}
+      description={
+        <Markdown
+          components={{
+            ...MARKDOWN_COMPONENTS,
+            p: ({ children }) => <Fragment>{children}</Fragment>,
+          }}
+        >
+          {spec.description}
+        </Markdown>
+      }
       placeholder={PLACEHOLDERS[spec.type]}
       required={true}
       value={internalValue}

+ 1 - 3
apps/api-reference/src/components/EvmApi/parameter.ts

@@ -1,9 +1,7 @@
-import type { ReactNode } from "react";
-
 export type Parameter<Name extends string> = {
   name: Name;
   type: ParameterType;
-  description: ReactNode;
+  description: string;
 };
 
 export enum ParameterType {

+ 11 - 4
apps/api-reference/src/components/EvmApi/run-button.tsx

@@ -6,7 +6,7 @@ import PythErrorsAbi from "@pythnetwork/pyth-sdk-solidity/abis/PythErrors.json";
 import { ConnectKitButton, Avatar } from "connectkit";
 import { useCallback, useMemo, useState } from "react";
 import { useAccount, useConfig } from "wagmi";
-import { readContract, writeContract } from "wagmi/actions";
+import { readContract, simulateContract, writeContract } from "wagmi/actions";
 
 import { getContractAddress } from "./networks";
 import { type Parameter, TRANSFORMS } from "./parameter";
@@ -175,7 +175,7 @@ const useRunButton = <ParameterName extends string>({
             })
             .catch((error: unknown) => {
               setModalContents({
-                error: error,
+                error,
                 parameters: paramValues,
                 networkName,
               });
@@ -193,7 +193,14 @@ const useRunButton = <ParameterName extends string>({
             });
             setStatus(Status.ShowingResults);
           } else {
-            writeContract(config, { abi, address, functionName, args, value })
+            simulateContract(config, {
+              abi,
+              address,
+              functionName,
+              args,
+              value,
+            })
+              .then(({ request }) => writeContract(config, request))
               .then((result) => {
                 setModalContents({
                   result,
@@ -203,7 +210,7 @@ const useRunButton = <ParameterName extends string>({
               })
               .catch((error: unknown) => {
                 setModalContents({
-                  error: error,
+                  error,
                   parameters: paramValues,
                   networkName,
                 });

+ 0 - 60
apps/api-reference/src/components/EvmLayout/index.tsx

@@ -1,60 +0,0 @@
-"use client";
-
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { ConnectKitProvider, getDefaultConfig } from "connectkit";
-import { useTheme } from "next-themes";
-import type { ReactNode } from "react";
-import { WagmiProvider, createConfig, http, useChainId } from "wagmi";
-import { arbitrum, avalanche, mainnet, sepolia } from "wagmi/chains";
-
-import { metadata } from "../../metadata";
-
-const config = createConfig(
-  /* @ts-expect-error connectkit's types don't unify with wagmi's types using the exactOptionalPropertyTypes typescript setting */
-  getDefaultConfig({
-    chains: [mainnet, avalanche, arbitrum, sepolia],
-    transports: {
-      [mainnet.id]: http(),
-      [avalanche.id]: http(),
-      [arbitrum.id]: http(),
-      [sepolia.id]: http(),
-    },
-    appName: metadata.applicationName,
-    appDescription: metadata.description,
-    appUrl: metadata.metadataBase.toString(),
-    appIcon: metadata.icons.apple.url,
-  }),
-);
-
-const queryClient = new QueryClient();
-
-type EvmLayoutProps = {
-  children: ReactNode;
-};
-
-export const EvmLayout = ({ children }: EvmLayoutProps) => {
-  return (
-    <WagmiProvider config={config}>
-      <QueryClientProvider client={queryClient}>
-        <ConnectKitProviderWrapper>{children}</ConnectKitProviderWrapper>
-      </QueryClientProvider>
-    </WagmiProvider>
-  );
-};
-
-const ConnectKitProviderWrapper = ({ children }: { children: ReactNode }) => {
-  const { resolvedTheme } = useTheme();
-  const chainId = useChainId();
-
-  return (
-    <ConnectKitProvider
-      mode={resolvedTheme as "light" | "dark"}
-      options={{ initialChainId: chainId }}
-      customTheme={{
-        "--ck-font-family": "var(--font-sans)",
-      }}
-    >
-      {children}
-    </ConnectKitProvider>
-  );
-};

+ 60 - 0
apps/api-reference/src/components/EvmProvider/index.tsx

@@ -0,0 +1,60 @@
+"use client";
+
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ConnectKitProvider, getDefaultConfig } from "connectkit";
+import { useTheme } from "next-themes";
+import type { ReactNode } from "react";
+import { WagmiProvider, createConfig, http, useChainId } from "wagmi";
+import { arbitrum, avalanche, mainnet, sepolia } from "wagmi/chains";
+
+import { metadata } from "../../metadata";
+
+type EvmProviderProps = {
+  children: ReactNode;
+  walletConnectProjectId: string;
+};
+
+export const EvmProvider = ({
+  children,
+  walletConnectProjectId,
+}: EvmProviderProps) => (
+  <WagmiProvider
+    config={createConfig(
+      getDefaultConfig({
+        chains: [mainnet, avalanche, arbitrum, sepolia],
+        transports: {
+          [mainnet.id]: http(),
+          [avalanche.id]: http(),
+          [arbitrum.id]: http(),
+          [sepolia.id]: http(),
+        },
+        walletConnectProjectId,
+        appName: metadata.applicationName,
+        appDescription: metadata.description,
+        appUrl: metadata.metadataBase.toString(),
+        appIcon: metadata.icons.apple.url,
+      }),
+    )}
+  >
+    <QueryClientProvider client={new QueryClient()}>
+      <ConnectKitProviderWrapper>{children}</ConnectKitProviderWrapper>
+    </QueryClientProvider>
+  </WagmiProvider>
+);
+
+const ConnectKitProviderWrapper = ({ children }: { children: ReactNode }) => {
+  const { resolvedTheme } = useTheme();
+  const chainId = useChainId();
+
+  return (
+    <ConnectKitProvider
+      mode={resolvedTheme as "light" | "dark"}
+      options={{ initialChainId: chainId }}
+      customTheme={{
+        "--ck-font-family": "var(--font-sans)",
+      }}
+    >
+      {children}
+    </ConnectKitProvider>
+  );
+};

+ 1 - 1
apps/api-reference/src/components/Header/index.tsx

@@ -31,7 +31,7 @@ export const Header = ({
           >
             <ul className="contents">
               <li className="contents">
-                <NavLink href="/price-feeds">Price Feeds</NavLink>
+                <NavLink href="/price-feeds/evm/getPrice">Price Feeds</NavLink>
               </li>
               <li className="contents">
                 <NavLink href="/benchmarks">Benchmarks</NavLink>

+ 17 - 7
apps/api-reference/src/components/Header/nav-link.tsx

@@ -3,16 +3,18 @@
 import clsx from "clsx";
 import Link from "next/link";
 import { useSelectedLayoutSegment } from "next/navigation";
-import type { ComponentProps } from "react";
+import { type ComponentProps, useMemo } from "react";
 
 const baseClasses = "font-semibold text-sm py-2 px-3";
 
-export const NavLink = ({
-  className,
-  ...props
-}: ComponentProps<typeof Link>) => {
-  const segment = useSelectedLayoutSegment();
-  return segment && `/${segment}` === props.href ? (
+type NavLinkProps = Omit<ComponentProps<typeof Link>, "href"> & {
+  href: string;
+};
+
+export const NavLink = ({ className, ...props }: NavLinkProps) => {
+  const isCurrent = useIsCurrent(props.href);
+
+  return isCurrent ? (
     <span
       className={clsx(
         "text-pythpurple-600 dark:text-pythpurple-400",
@@ -33,3 +35,11 @@ export const NavLink = ({
     />
   );
 };
+
+const useIsCurrent = (href: string) => {
+  const selectedSegment = useSelectedLayoutSegment();
+  return useMemo(
+    () => href.toString().split("/")[1] === selectedSegment,
+    [href, selectedSegment],
+  );
+};

+ 5 - 5
apps/api-reference/src/components/Home/index.tsx

@@ -1,5 +1,5 @@
 import Link from "next/link";
-import type { ComponentType, SVGProps } from "react";
+import type { ElementType, SVGProps } from "react";
 
 import Benchmarks from "./benchmarks.svg";
 import Entropy from "./entropy.svg";
@@ -21,7 +21,7 @@ export const Home = () => (
           <li className="contents">
             <ProductLink
               icon={PriceFeeds}
-              href="/price-feeds"
+              href="/price-feeds/evm/getPrice"
               name="Price Feeds"
             >
               Fetch real-time low-latency market data, on 50+ chains or off
@@ -50,7 +50,7 @@ type ProductLinkProps = {
   name: string;
   href: string;
   children: string;
-  icon: ComponentType<SVGProps<SVGSVGElement>>;
+  icon: ElementType<SVGProps<SVGSVGElement>>;
 };
 
 const ProductLink = ({
@@ -63,14 +63,14 @@ const ProductLink = ({
     as={Link}
     href={href}
     gradient
-    className="flex flex-col items-center gap-2 p-6 text-center sm:flex-row sm:gap-6 sm:pr-12 sm:text-left"
+    className="flex max-w-2xl flex-col items-center gap-2 p-6 text-center sm:flex-row sm:gap-6 sm:pr-12 sm:text-left"
   >
     <Icon className="h-24 p-3 text-pythpurple-600 dark:text-pythpurple-400" />
     <div className="flex flex-col gap-2">
       <h2 className="text-2xl font-medium text-pythpurple-600 dark:text-pythpurple-400">
         {name}
       </h2>
-      <p className="text-sm font-normal text-neutral-600 dark:text-neutral-400">
+      <p className="text-lg font-normal text-neutral-600 dark:text-neutral-400">
         {children}
       </p>
     </div>

+ 5 - 22
apps/api-reference/src/components/InlineLink/index.tsx

@@ -1,23 +1,6 @@
-import clsx from "clsx";
-import type { ComponentProps, ElementType } from "react";
+import { Styled } from "../Styled";
 
-type InlineLinkProps<T extends ElementType> = Omit<ComponentProps<T>, "as"> & {
-  as?: T;
-};
-
-export const InlineLink = <T extends ElementType>({
-  as,
-  className,
-  ...props
-}: InlineLinkProps<T>) => {
-  const Component = as ?? "a";
-  return (
-    <Component
-      className={clsx(
-        "font-medium text-pythpurple-600 hover:underline dark:text-pythpurple-400",
-        className,
-      )}
-      {...props}
-    />
-  );
-};
+export const InlineLink = Styled(
+  "a",
+  "font-medium text-pythpurple-600 hover:underline dark:text-pythpurple-400",
+);

+ 2 - 20
apps/api-reference/src/components/MaxWidth/index.tsx

@@ -1,21 +1,3 @@
-import clsx from "clsx";
-import type { ElementType, ComponentProps } from "react";
+import { Styled } from "../Styled";
 
-type MaxWidthProps<T extends ElementType> = Omit<ComponentProps<T>, "as"> & {
-  as?: T;
-};
-
-export const MaxWidth = <T extends ElementType = "div">({
-  as,
-  className,
-  ...props
-}: MaxWidthProps<T>) => {
-  const Component = as ?? "div";
-
-  return (
-    <Component
-      className={clsx("mx-auto max-w-7xl px-8", className)}
-      {...props}
-    />
-  );
-};
+export const MaxWidth = Styled("div", "mx-auto max-w-7xl px-8");

+ 2 - 8
apps/api-reference/src/components/Paragraph/index.tsx

@@ -1,9 +1,3 @@
-import clsx from "clsx";
-import type { HTMLAttributes } from "react";
+import { Styled } from "../Styled";
 
-export const Paragraph = ({
-  className,
-  ...props
-}: HTMLAttributes<HTMLElement>) => (
-  <p className={clsx("mb-6 last:mb-0", className)} {...props} />
-);
+export const Paragraph = Styled("p", "mb-6 last:mb-0");

+ 17 - 7
apps/api-reference/src/components/Root/index.tsx

@@ -4,9 +4,15 @@ import { Red_Hat_Text, Red_Hat_Mono } from "next/font/google";
 import { ThemeProvider } from "next-themes";
 import type { ReactNode } from "react";
 
-import { IS_PRODUCTION_BUILD } from "../../isomorphic-config";
-import { GOOGLE_ANALYTICS_ID, AMPLITUDE_API_KEY } from "../../server-config";
+import {
+  IS_PRODUCTION_SERVER,
+  GOOGLE_ANALYTICS_ID,
+  AMPLITUDE_API_KEY,
+  WALLETCONNECT_PROJECT_ID,
+} from "../../server-config";
 import { Amplitude } from "../Amplitude";
+import { HighlighterProvider } from "../Code/use-highlighted-code";
+import { EvmProvider } from "../EvmProvider";
 import { Footer } from "../Footer";
 import { Header } from "../Header";
 import { ReportAccessibility } from "../ReportAccessibility";
@@ -35,13 +41,17 @@ export const Root = ({ children }: Props) => (
   >
     <body className="grid size-full grid-cols-1 grid-rows-[max-content_1fr_max-content] bg-white text-pythpurple-950 dark:bg-pythpurple-900 dark:text-white">
       <ThemeProvider attribute="class">
-        <Header className="z-10 border-b border-neutral-400 dark:border-neutral-600" />
-        <div className="size-full">{children}</div>
-        <Footer className="z-10 border-t border-neutral-400 dark:border-neutral-600" />
+        <HighlighterProvider>
+          <EvmProvider walletConnectProjectId={WALLETCONNECT_PROJECT_ID}>
+            <Header className="z-10 border-b border-neutral-400 dark:border-neutral-600" />
+            <div className="size-full">{children}</div>
+            <Footer className="z-10 border-t border-neutral-400 dark:border-neutral-600" />
+          </EvmProvider>
+        </HighlighterProvider>
       </ThemeProvider>
     </body>
     {GOOGLE_ANALYTICS_ID && <GoogleAnalytics gaId={GOOGLE_ANALYTICS_ID} />}
-    {AMPLITUDE_API_KEY && <Amplitude key={AMPLITUDE_API_KEY} />}
-    {!IS_PRODUCTION_BUILD && <ReportAccessibility />}
+    {AMPLITUDE_API_KEY && <Amplitude apiKey={AMPLITUDE_API_KEY} />}
+    {!IS_PRODUCTION_SERVER && <ReportAccessibility />}
   </html>
 );

+ 77 - 20
apps/api-reference/src/components/Sidebar/index.tsx

@@ -4,9 +4,17 @@ import { Field, Label } from "@headlessui/react";
 import clsx from "clsx";
 import Link from "next/link";
 import { useSelectedLayoutSegments } from "next/navigation";
-import { type HTMLAttributes, useState, type ComponentProps } from "react";
+import {
+  type HTMLAttributes,
+  useState,
+  type ComponentProps,
+  type ElementType,
+  Fragment,
+} from "react";
+import Markdown from "react-markdown";
 
 import * as apis from "../../apis";
+import { MARKDOWN_COMPONENTS } from "../../markdown-components";
 import { Select } from "../Select";
 
 type Chain = keyof typeof apis;
@@ -22,9 +30,10 @@ const CHAIN_TO_NAME = {
 const MENU = Object.fromEntries(
   Object.entries(apis).map(([chain, methods]) => [
     chain,
-    Object.keys(methods).map((method) => ({
-      name: method,
-      href: `/price-feeds/${chain}/${method}`,
+    Object.entries(methods).map(([name, { summary }]) => ({
+      name,
+      summary,
+      href: `/price-feeds/${chain}/${name}`,
     })),
   ]),
 );
@@ -60,9 +69,11 @@ export const Sidebar = ({
           aria-label="Methods"
         >
           <ul className="contents">
-            {MENU[chain]?.map(({ name, href }) => (
+            {MENU[chain]?.map(({ name, href, summary }) => (
               <li className="contents" key={href}>
-                <MenuButton href={href}>{name}</MenuButton>
+                <MenuButton href={href} name={name}>
+                  {summary}
+                </MenuButton>
               </li>
             ))}
           </ul>
@@ -73,29 +84,75 @@ export const Sidebar = ({
   );
 };
 
-const baseMenuButtonClasses = "text-sm py-1 px-2";
+type MenuButtonProps = Omit<
+  ComponentProps<typeof Link>,
+  keyof MenuItemProps<typeof Link>
+> & {
+  name: string;
+  children: string;
+};
 
-const MenuButton = ({ className, ...props }: ComponentProps<typeof Link>) => {
+const MenuButton = ({
+  className,
+  name,
+  children,
+  ...props
+}: MenuButtonProps) => {
   const segments = useSelectedLayoutSegments();
 
   return `/price-feeds/${segments.join("/")}` === props.href ? (
-    <div
-      className={clsx(
-        "font-bold text-pythpurple-600 dark:text-pythpurple-400",
-        baseMenuButtonClasses,
-        className,
-      )}
-    >
-      {props.children}
-    </div>
+    <MenuItem
+      className={className}
+      name={name}
+      summary={children}
+      nameClassName="font-bold text-pythpurple-600 dark:text-pythpurple-400"
+    />
   ) : (
-    <Link
+    <MenuItem
+      as={Link}
       className={clsx(
-        "rounded hover:bg-neutral-200 hover:text-pythpurple-600 dark:hover:bg-neutral-800 dark:hover:text-pythpurple-400",
-        baseMenuButtonClasses,
+        "group hover:bg-neutral-200 dark:hover:bg-neutral-800",
         className,
       )}
+      nameClassName="group-hover:text-pythpurple-600 dark:group-hover:text-pythpurple-400"
+      name={name}
+      summary={children}
       {...props}
     />
   );
 };
+
+type MenuItemProps<T extends ElementType> = {
+  as?: T;
+  name: string;
+  summary: string;
+  nameClassName?: string | undefined;
+};
+
+const MenuItem = <T extends ElementType>({
+  as,
+  className,
+  name,
+  summary,
+  nameClassName,
+  ...props
+}: Omit<ComponentProps<T>, keyof MenuItemProps<T>> & MenuItemProps<T>) => {
+  const Component = as ?? "div";
+  return (
+    <Component className={clsx("rounded px-2 py-1", className)} {...props}>
+      <div className={clsx("text-sm", nameClassName)}>{name}</div>
+      <Markdown
+        className={clsx(
+          "ml-4 overflow-hidden text-ellipsis text-nowrap text-xs font-light",
+          className,
+        )}
+        components={{
+          ...MARKDOWN_COMPONENTS,
+          p: ({ children }) => <Fragment>{children}</Fragment>,
+        }}
+      >
+        {summary}
+      </Markdown>
+    </Component>
+  );
+};

+ 18 - 0
apps/api-reference/src/components/Styled/index.tsx

@@ -0,0 +1,18 @@
+import clsx from "clsx";
+import type { ComponentProps, ElementType } from "react";
+
+type StyledProps<T extends ElementType> = Omit<ComponentProps<T>, "as"> & {
+  as?: T;
+};
+
+export const Styled = (defaultElement: ElementType, classes: string) => {
+  const StyledComponent = <T extends ElementType = typeof defaultElement>({
+    as,
+    className,
+    ...props
+  }: StyledProps<T>) => {
+    const Component = as ?? defaultElement;
+    return <Component className={clsx(classes, className)} {...props} />;
+  };
+  return StyledComponent;
+};

+ 12 - 0
apps/api-reference/src/markdown-components.tsx

@@ -0,0 +1,12 @@
+import { InlineCode } from "./components/InlineCode";
+import { InlineLink } from "./components/InlineLink";
+import { Paragraph } from "./components/Paragraph";
+import { Styled } from "./components/Styled";
+
+export const MARKDOWN_COMPONENTS = {
+  h1: Styled("h1", "mb-8 text-4xl font-medium"),
+  p: Paragraph,
+  a: InlineLink,
+  code: InlineCode,
+  strong: Styled("strong", "font-semibold"),
+};

+ 0 - 18
apps/api-reference/src/mdx-components.tsx

@@ -1,18 +0,0 @@
-import type { MDXComponents } from "mdx/types";
-
-import { InlineCode } from "./components/InlineCode";
-import { InlineLink } from "./components/InlineLink";
-import { Paragraph } from "./components/Paragraph";
-
-export const useMDXComponents = (components: MDXComponents): MDXComponents => ({
-  h1: ({ children, ...props }) => (
-    <h1 className="mb-8 text-4xl font-medium" {...props}>
-      {children}
-    </h1>
-  ),
-  p: Paragraph,
-  a: InlineLink,
-  code: InlineCode,
-  strong: (props) => <strong className="font-semibold" {...props} />,
-  ...components,
-});

+ 42 - 1
apps/api-reference/src/server-config.ts

@@ -4,23 +4,64 @@
 
 import "server-only";
 
+/**
+ * Throw if the env var `key` is not set (at either runtime or build time).
+ */
 const demand = (key: string): string => {
   const value = process.env[key];
-  if (value) {
+  if (value && value !== "") {
     return value;
   } else {
     throw new Error(`Missing environment variable ${key}!`);
   }
 };
 
+/**
+ * Indicates that we're running in a github actions workflow.
+ */
+export const IS_GITHUB_ACTIONS = process.env.GITHUB_ACTIONS === "true";
+
+/**
+ * Throw if the env var `key` is not set, unless we're running in github
+ * actions.  If running in GHA, then allow the variable to be unset and return
+ * an empty string if so.
+ *
+ * This is useful because for some variables, we want an invariant that the
+ * value is always present.  However, we don't necessarily need the value just
+ * to run code checks, some of which require a build, we don't want to
+ * expose the secret to GHA unnecessarily, and we don't want to have to.
+ *
+ * So, in effect, variables marked with this will be asserted to be present in
+ * Vercel or in local dev, and will type check as `string`, but will not throw
+ * when running code checks in GHA.
+ *
+ * Note we use `IS_GITHUB_ACTIONS` and not e.g. `process.env.CI` here because
+ * both Vercel and Github Actions set `process.env.CI`, and we specifically want
+ * to only allow these variables to be nonpresent only if running a build just
+ * for running code checks.  Any build that will actually serve traffic must
+ * have these variables set.  Github Actions is the only environment that is
+ * exclusively used for running checks, so semantically this is correct, but
+ * this would need to be updated if we change infrastructure.
+ */
+const demandExceptGHA = IS_GITHUB_ACTIONS
+  ? (key: string) => process.env[key] ?? ""
+  : demand;
+
 /**
  * Indicates that this server is the live customer-facing production server.
  */
 export const IS_PRODUCTION_SERVER = process.env.VERCEL_ENV === "production";
 
+/**
+ * Throw if the env var `key` is not set in the live customer-facing production
+ * server, but allow it to be unset in any other environment.
+ */
 const demandInProduction = IS_PRODUCTION_SERVER
   ? demand
   : (key: string) => process.env[key];
 
 export const GOOGLE_ANALYTICS_ID = demandInProduction("GOOGLE_ANALYTICS_ID");
 export const AMPLITUDE_API_KEY = demandInProduction("AMPLITUDE_API_KEY");
+export const WALLETCONNECT_PROJECT_ID = demandExceptGHA(
+  "WALLETCONNECT_PROJECT_ID",
+);

+ 1 - 1
apps/api-reference/tailwind.config.ts

@@ -3,7 +3,7 @@ import type { Config } from "tailwindcss";
 
 const tailwindConfig = {
   darkMode: "class",
-  content: ["src/components/**/*.{ts,tsx}", "src/mdx-components.tsx"],
+  content: ["src/components/**/*.{ts,tsx}", "src/markdown-components.tsx"],
   plugins: [forms],
   theme: {
     extend: {

+ 19 - 15
flake.nix

@@ -19,27 +19,31 @@
         };
 
         cli-overlay = _: prev: {
-          cli = prev.lib.mkCli "cli" {
-            _noAll = true;
+          cli = let
+            pnpm = "${prev.pnpm}/bin/pnpm i && ${prev.pnpm}/bin/pnpm";
+          in
+            prev.lib.mkCli "cli" {
+              _noAll = true;
 
-            install = "${prev.pnpm}/bin/pnpm i";
+              install = "${pnpm} i";
+              start = "${pnpm} lerna run start:dev";
 
-            test = {
-              nix = {
-                lint = "${prev.statix}/bin/statix check --ignore node_modules .";
-                dead-code = "${prev.deadnix}/bin/deadnix --exclude ./node_modules .";
-                format = "${prev.alejandra}/bin/alejandra --exclude ./node_modules --check .";
+              test = {
+                nix = {
+                  lint = "${prev.statix}/bin/statix check --ignore node_modules .";
+                  dead-code = "${prev.deadnix}/bin/deadnix --exclude ./node_modules .";
+                  format = "${prev.alejandra}/bin/alejandra --exclude ./node_modules --check .";
+                };
               };
-            };
 
-            fix = {
-              nix = {
-                lint = "${prev.statix}/bin/statix fix --ignore node_modules .";
-                dead-code = "${prev.deadnix}/bin/deadnix --exclude ./node_modules -e .";
-                format = "${prev.alejandra}/bin/alejandra --exclude ./node_modules .";
+              fix = {
+                nix = {
+                  lint = "${prev.statix}/bin/statix fix --ignore node_modules .";
+                  dead-code = "${prev.deadnix}/bin/deadnix --exclude ./node_modules -e .";
+                  format = "${prev.alejandra}/bin/alejandra --exclude ./node_modules .";
+                };
               };
             };
-          };
         };
 
         pkgs = import nixpkgs {

+ 62 - 386
pnpm-lock.yaml

@@ -33,6 +33,9 @@ importers:
       '@amplitude/analytics-browser':
         specifier: ^2.9.0
         version: 2.9.0
+      '@amplitude/plugin-autocapture-browser':
+        specifier: ^0.9.0
+        version: 0.9.0
       '@floating-ui/react':
         specifier: ^0.26.17
         version: 0.26.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -42,18 +45,6 @@ importers:
       '@heroicons/react':
         specifier: ^2.1.4
         version: 2.1.4(react@18.3.1)
-      '@mdx-js/loader':
-        specifier: ^3.0.1
-        version: 3.0.1(webpack@5.91.0)
-      '@mdx-js/mdx':
-        specifier: ^3.0.1
-        version: 3.0.1
-      '@mdx-js/react':
-        specifier: ^3.0.1
-        version: 3.0.1(@types/react@18.3.3)(react@18.3.1)
-      '@next/mdx':
-        specifier: ^14.2.4
-        version: 14.2.4(@mdx-js/loader@3.0.1(webpack@5.91.0))(@mdx-js/react@3.0.1(@types/react@18.3.3)(react@18.3.1))
       '@next/third-parties':
         specifier: ^14.2.4
         version: 14.2.4(next@14.2.4(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
@@ -87,6 +78,9 @@ importers:
       react-dom:
         specifier: ^18.3.1
         version: 18.3.1(react@18.3.1)
+      react-markdown:
+        specifier: ^9.0.1
+        version: 9.0.1(@types/react@18.3.3)(react@18.3.1)
       shiki:
         specifier: ^1.7.0
         version: 1.7.0
@@ -124,9 +118,6 @@ importers:
       '@types/jest':
         specifier: ^29.5.12
         version: 29.5.12
-      '@types/mdx':
-        specifier: ^2.0.13
-        version: 2.0.13
       '@types/node':
         specifier: ^20.14.6
         version: 20.14.7
@@ -1741,6 +1732,9 @@ packages:
   '@amplitude/analytics-types@2.6.0':
     resolution: {integrity: sha512-7MSENvLCTGjec7K45JT+RcOuoPTCvq1MMq/HRLiQK/BMR4taX7f/uXldEc8b//o+ZZP45IBqFroR7Bl8LwJQrQ==}
 
+  '@amplitude/plugin-autocapture-browser@0.9.0':
+    resolution: {integrity: sha512-iLae95j9tkfZtHCrfFLOsMzqmjO3NRqB+Eslf1txFfEsbZ/2MlDQqtxklvxv46JD2IH4q/vDamyJ0gGRsse16w==}
+
   '@amplitude/plugin-page-view-tracking-browser@2.2.14':
     resolution: {integrity: sha512-3Rh8P0pK3QOd94uVeOgjc2UommeHucCvasAIdjf1QQ59wVmNua2gY3IwR8+Pyg0PJ3plmwY/La8YukeXB6A2aA==}
 
@@ -4597,20 +4591,6 @@ packages:
     peerDependencies:
       hardhat: ^2.12.6
 
-  '@mdx-js/loader@3.0.1':
-    resolution: {integrity: sha512-YbYUt7YyEOdFxhyuCWmLKf5vKhID/hJAojEUnheJk4D8iYVLFQw+BAoBWru/dHGch1omtmZOPstsmKPyBF68Tw==}
-    peerDependencies:
-      webpack: '>=5'
-
-  '@mdx-js/mdx@3.0.1':
-    resolution: {integrity: sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==}
-
-  '@mdx-js/react@3.0.1':
-    resolution: {integrity: sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==}
-    peerDependencies:
-      '@types/react': '>=16'
-      react: '>=16'
-
   '@metamask/eth-json-rpc-provider@1.0.1':
     resolution: {integrity: sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA==}
     engines: {node: '>=14.0.0'}
@@ -4791,17 +4771,6 @@ packages:
   '@next/eslint-plugin-next@14.2.3':
     resolution: {integrity: sha512-L3oDricIIjgj1AVnRdRor21gI7mShlSwU/1ZGHmqM3LzHhXXhdkrfeNY5zif25Bi5Dd7fiJHsbhoZCHfXYvlAw==}
 
-  '@next/mdx@14.2.4':
-    resolution: {integrity: sha512-eklTNNoH08xGy9UiKcohZmoLhmHAYaYm5ndPGQqJybaeNErgYL8fmp2tk5DRD0L54DNqMz97oN+CAEHqfqIVcw==}
-    peerDependencies:
-      '@mdx-js/loader': '>=0.15.0'
-      '@mdx-js/react': '>=0.15.0'
-    peerDependenciesMeta:
-      '@mdx-js/loader':
-        optional: true
-      '@mdx-js/react':
-        optional: true
-
   '@next/swc-darwin-arm64@14.2.3':
     resolution: {integrity: sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==}
     engines: {node: '>= 10'}
@@ -6743,9 +6712,6 @@ packages:
   '@types/accepts@1.3.5':
     resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==}
 
-  '@types/acorn@4.0.6':
-    resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==}
-
   '@types/adm-zip@0.5.0':
     resolution: {integrity: sha512-FCJBJq9ODsQZUNURo5ILAQueuA8WJhRvuihS3ke2iI25mJlfV2LK8jG2Qj2z2AWg8U0FtWWqBHVRetceLskSaw==}
 
@@ -6896,9 +6862,6 @@ packages:
   '@types/mdast@4.0.4':
     resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
 
-  '@types/mdx@2.0.13':
-    resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==}
-
   '@types/mime@3.0.1':
     resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
 
@@ -8151,10 +8114,6 @@ packages:
     resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
     engines: {node: '>=8'}
 
-  astring@1.8.6:
-    resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==}
-    hasBin: true
-
   async-eventemitter@0.2.4:
     resolution: {integrity: sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw==}
 
@@ -8992,9 +8951,6 @@ packages:
     resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==}
     engines: {node: '>=0.10.0'}
 
-  collapse-white-space@2.1.0:
-    resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
-
   collect-v8-coverage@1.0.1:
     resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==}
 
@@ -10470,27 +10426,12 @@ packages:
     resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
     engines: {node: '>=4.0'}
 
-  estree-util-attach-comments@3.0.0:
-    resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==}
-
-  estree-util-build-jsx@3.0.1:
-    resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==}
-
   estree-util-is-identifier-name@3.0.0:
     resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
 
-  estree-util-to-js@2.0.0:
-    resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==}
-
-  estree-util-visit@2.0.0:
-    resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==}
-
   estree-walker@2.0.2:
     resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
 
-  estree-walker@3.0.3:
-    resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
-
   esutils@2.0.3:
     resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
     engines: {node: '>=0.10.0'}
@@ -11374,9 +11315,6 @@ packages:
     resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
     engines: {node: '>= 0.4'}
 
-  hast-util-to-estree@3.1.0:
-    resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==}
-
   hast-util-to-jsx-runtime@2.3.0:
     resolution: {integrity: sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==}
 
@@ -11460,6 +11398,9 @@ packages:
   html-parse-stringify@3.0.1:
     resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
 
+  html-url-attributes@3.0.0:
+    resolution: {integrity: sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==}
+
   htmlparser2@8.0.1:
     resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==}
 
@@ -11625,9 +11566,6 @@ packages:
     resolution: {integrity: sha512-YhlQPEjNFqlGdzrBfDNRLhvoSgX7iQRgSxgsNknRQ9ITXFT7UMfVMWhBTOh2Y+25lRnGrv5Xz8yZwQ3ACR6T3A==}
     engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
 
-  inline-style-parser@0.1.1:
-    resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==}
-
   inline-style-parser@0.2.3:
     resolution: {integrity: sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==}
 
@@ -11853,9 +11791,6 @@ packages:
   is-potential-custom-element-name@1.0.1:
     resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
 
-  is-reference@3.0.2:
-    resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==}
-
   is-regex@1.1.4:
     resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
     engines: {node: '>= 0.4'}
@@ -12935,10 +12870,6 @@ packages:
     resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==}
     engines: {node: '>=8'}
 
-  markdown-extensions@2.0.0:
-    resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
-    engines: {node: '>=16'}
-
   marked@4.3.0:
     resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==}
     engines: {node: '>= 12'}
@@ -12963,9 +12894,6 @@ packages:
   mdast-util-mdx-jsx@3.1.2:
     resolution: {integrity: sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA==}
 
-  mdast-util-mdx@3.0.0:
-    resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==}
-
   mdast-util-mdxjs-esm@2.0.1:
     resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
 
@@ -13106,30 +13034,12 @@ packages:
   micromark-core-commonmark@2.0.1:
     resolution: {integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==}
 
-  micromark-extension-mdx-expression@3.0.0:
-    resolution: {integrity: sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==}
-
-  micromark-extension-mdx-jsx@3.0.0:
-    resolution: {integrity: sha512-uvhhss8OGuzR4/N17L1JwvmJIpPhAd8oByMawEKx6NVdBCbesjH4t+vjEp3ZXft9DwvlKSD07fCeI44/N0Vf2w==}
-
-  micromark-extension-mdx-md@2.0.0:
-    resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==}
-
-  micromark-extension-mdxjs-esm@3.0.0:
-    resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==}
-
-  micromark-extension-mdxjs@3.0.0:
-    resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==}
-
   micromark-factory-destination@2.0.0:
     resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==}
 
   micromark-factory-label@2.0.0:
     resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==}
 
-  micromark-factory-mdx-expression@2.0.1:
-    resolution: {integrity: sha512-F0ccWIUHRLRrYp5TC9ZYXmZo+p2AM13ggbsW4T0b5CRKP8KHVRB8t4pwtBgTxtjRmwrK0Irwm7vs2JOZabHZfg==}
-
   micromark-factory-space@2.0.0:
     resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==}
 
@@ -13160,9 +13070,6 @@ packages:
   micromark-util-encode@2.0.0:
     resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==}
 
-  micromark-util-events-to-acorn@2.0.2:
-    resolution: {integrity: sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==}
-
   micromark-util-html-tag-name@2.0.0:
     resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==}
 
@@ -14272,9 +14179,6 @@ packages:
   performance-now@2.1.0:
     resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
 
-  periscopic@3.1.0:
-    resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
-
   picocolors@1.0.0:
     resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
 
@@ -14933,6 +14837,12 @@ packages:
   react-lifecycles-compat@3.0.4:
     resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
 
+  react-markdown@9.0.1:
+    resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==}
+    peerDependencies:
+      '@types/react': '>=18'
+      react: '>=18'
+
   react-modal@3.16.1:
     resolution: {integrity: sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==}
     engines: {node: '>=8'}
@@ -15176,9 +15086,6 @@ packages:
     resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==}
     hasBin: true
 
-  remark-mdx@3.0.1:
-    resolution: {integrity: sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA==}
-
   remark-parse@11.0.0:
     resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
 
@@ -15989,9 +15896,6 @@ packages:
     engines: {node: '>=4'}
     hasBin: true
 
-  style-to-object@0.4.4:
-    resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==}
-
   style-to-object@1.0.6:
     resolution: {integrity: sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==}
 
@@ -16762,9 +16666,6 @@ packages:
   unist-util-is@6.0.0:
     resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
 
-  unist-util-position-from-estree@2.0.0:
-    resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==}
-
   unist-util-position@5.0.0:
     resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
 
@@ -18017,6 +17918,12 @@ snapshots:
 
   '@amplitude/analytics-types@2.6.0': {}
 
+  '@amplitude/plugin-autocapture-browser@0.9.0':
+    dependencies:
+      '@amplitude/analytics-client-common': 2.2.2
+      '@amplitude/analytics-types': 2.6.0
+      tslib: 2.6.3
+
   '@amplitude/plugin-page-view-tracking-browser@2.2.14':
     dependencies:
       '@amplitude/analytics-client-common': 2.2.2
@@ -18237,10 +18144,10 @@ snapshots:
       '@babel/helpers': 7.24.7
       '@babel/parser': 7.24.7
       '@babel/template': 7.24.7
-      '@babel/traverse': 7.24.7
+      '@babel/traverse': 7.24.7(supports-color@5.5.0)
       '@babel/types': 7.24.7
       convert-source-map: 2.0.0
-      debug: 4.3.5
+      debug: 4.3.5(supports-color@5.5.0)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
@@ -18459,13 +18366,6 @@ snapshots:
     dependencies:
       '@babel/types': 7.24.0
 
-  '@babel/helper-module-imports@7.24.7':
-    dependencies:
-      '@babel/traverse': 7.24.7
-      '@babel/types': 7.24.7
-    transitivePeerDependencies:
-      - supports-color
-
   '@babel/helper-module-imports@7.24.7(supports-color@5.5.0)':
     dependencies:
       '@babel/traverse': 7.24.7(supports-color@5.5.0)
@@ -18499,7 +18399,7 @@ snapshots:
     dependencies:
       '@babel/core': 7.24.7
       '@babel/helper-environment-visitor': 7.24.7
-      '@babel/helper-module-imports': 7.24.7
+      '@babel/helper-module-imports': 7.24.7(supports-color@5.5.0)
       '@babel/helper-simple-access': 7.24.7
       '@babel/helper-split-export-declaration': 7.24.7
       '@babel/helper-validator-identifier': 7.24.7
@@ -18590,7 +18490,7 @@ snapshots:
 
   '@babel/helper-simple-access@7.24.7':
     dependencies:
-      '@babel/traverse': 7.24.7
+      '@babel/traverse': 7.24.7(supports-color@5.5.0)
       '@babel/types': 7.24.7
     transitivePeerDependencies:
       - supports-color
@@ -20571,21 +20471,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/traverse@7.24.7':
-    dependencies:
-      '@babel/code-frame': 7.24.7
-      '@babel/generator': 7.24.7
-      '@babel/helper-environment-visitor': 7.24.7
-      '@babel/helper-function-name': 7.24.7
-      '@babel/helper-hoist-variables': 7.24.7
-      '@babel/helper-split-export-declaration': 7.24.7
-      '@babel/parser': 7.24.7
-      '@babel/types': 7.24.7
-      debug: 4.3.5
-      globals: 11.12.0
-    transitivePeerDependencies:
-      - supports-color
-
   '@babel/traverse@7.24.7(supports-color@5.5.0)':
     dependencies:
       '@babel/code-frame': 7.24.7
@@ -21663,7 +21548,7 @@ snapshots:
   '@eslint/eslintrc@2.1.4':
     dependencies:
       ajv: 6.12.6
-      debug: 4.3.5
+      debug: 4.3.5(supports-color@5.5.0)
       espree: 9.6.1
       globals: 13.20.0
       ignore: 5.3.1
@@ -22272,7 +22157,7 @@ snapshots:
       '@graphql-tools/utils': 8.9.0(graphql@15.8.0)
       dataloader: 2.1.0
       graphql: 15.8.0
-      tslib: 2.4.1
+      tslib: 2.6.3
       value-or-promise: 1.0.11
     optional: true
 
@@ -22388,7 +22273,7 @@ snapshots:
   '@humanwhocodes/config-array@0.11.14':
     dependencies:
       '@humanwhocodes/object-schema': 2.0.3
-      debug: 4.3.5
+      debug: 4.3.5(supports-color@5.5.0)
       minimatch: 3.1.2
     transitivePeerDependencies:
       - supports-color
@@ -24325,48 +24210,6 @@ snapshots:
       - encoding
       - supports-color
 
-  '@mdx-js/loader@3.0.1(webpack@5.91.0)':
-    dependencies:
-      '@mdx-js/mdx': 3.0.1
-      source-map: 0.7.4
-      webpack: 5.91.0
-    transitivePeerDependencies:
-      - supports-color
-
-  '@mdx-js/mdx@3.0.1':
-    dependencies:
-      '@types/estree': 1.0.5
-      '@types/estree-jsx': 1.0.5
-      '@types/hast': 3.0.4
-      '@types/mdx': 2.0.13
-      collapse-white-space: 2.1.0
-      devlop: 1.1.0
-      estree-util-build-jsx: 3.0.1
-      estree-util-is-identifier-name: 3.0.0
-      estree-util-to-js: 2.0.0
-      estree-walker: 3.0.3
-      hast-util-to-estree: 3.1.0
-      hast-util-to-jsx-runtime: 2.3.0
-      markdown-extensions: 2.0.0
-      periscopic: 3.1.0
-      remark-mdx: 3.0.1
-      remark-parse: 11.0.0
-      remark-rehype: 11.1.0
-      source-map: 0.7.4
-      unified: 11.0.5
-      unist-util-position-from-estree: 2.0.0
-      unist-util-stringify-position: 4.0.0
-      unist-util-visit: 5.0.0
-      vfile: 6.0.1
-    transitivePeerDependencies:
-      - supports-color
-
-  '@mdx-js/react@3.0.1(@types/react@18.3.3)(react@18.3.1)':
-    dependencies:
-      '@types/mdx': 2.0.13
-      '@types/react': 18.3.3
-      react: 18.3.1
-
   '@metamask/eth-json-rpc-provider@1.0.1':
     dependencies:
       '@metamask/json-rpc-engine': 7.3.3
@@ -24766,13 +24609,6 @@ snapshots:
     dependencies:
       glob: 10.3.10
 
-  '@next/mdx@14.2.4(@mdx-js/loader@3.0.1(webpack@5.91.0))(@mdx-js/react@3.0.1(@types/react@18.3.3)(react@18.3.1))':
-    dependencies:
-      source-map: 0.7.4
-    optionalDependencies:
-      '@mdx-js/loader': 3.0.1(webpack@5.91.0)
-      '@mdx-js/react': 3.0.1(@types/react@18.3.3)(react@18.3.1)
-
   '@next/swc-darwin-arm64@14.2.3':
     optional: true
 
@@ -27998,10 +27834,6 @@ snapshots:
       '@types/node': 20.14.9
     optional: true
 
-  '@types/acorn@4.0.6':
-    dependencies:
-      '@types/estree': 1.0.5
-
   '@types/adm-zip@0.5.0':
     dependencies:
       '@types/node': 20.14.9
@@ -28203,8 +28035,6 @@ snapshots:
     dependencies:
       '@types/unist': 3.0.2
 
-  '@types/mdx@2.0.13': {}
-
   '@types/mime@3.0.1': {}
 
   '@types/minimatch@3.0.5': {}
@@ -28432,7 +28262,7 @@ snapshots:
       '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.5.2)
       '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.2)
       '@typescript-eslint/visitor-keys': 6.21.0
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
       eslint: 8.57.0
       graphemer: 1.4.0
       ignore: 5.3.1
@@ -28545,7 +28375,7 @@ snapshots:
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.2)
       '@typescript-eslint/visitor-keys': 6.21.0
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
       eslint: 8.57.0
     optionalDependencies:
       typescript: 5.5.2
@@ -28661,7 +28491,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.2)
       '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.2)
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
       eslint: 8.57.0
       ts-api-utils: 1.3.0(typescript@5.5.2)
     optionalDependencies:
@@ -28752,7 +28582,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/visitor-keys': 6.21.0
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
       globby: 11.1.0
       is-glob: 4.0.3
       minimatch: 9.0.3
@@ -30011,7 +29841,7 @@ snapshots:
 
   agent-base@6.0.2:
     dependencies:
-      debug: 4.3.5
+      debug: 4.3.5(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -30427,8 +30257,6 @@ snapshots:
 
   astral-regex@2.0.0: {}
 
-  astring@1.8.6: {}
-
   async-eventemitter@0.2.4:
     dependencies:
       async: 2.6.4
@@ -30567,7 +30395,7 @@ snapshots:
 
   axios@1.7.2:
     dependencies:
-      follow-redirects: 1.15.6
+      follow-redirects: 1.15.6(debug@4.3.4)
       form-data: 4.0.0
       proxy-from-env: 1.1.0
     transitivePeerDependencies:
@@ -31528,8 +31356,6 @@ snapshots:
 
   code-point-at@1.1.0: {}
 
-  collapse-white-space@2.1.0: {}
-
   collect-v8-coverage@1.0.1: {}
 
   collect-v8-coverage@1.0.2: {}
@@ -32272,20 +32098,12 @@ snapshots:
     optionalDependencies:
       supports-color: 8.1.1
 
-  debug@4.3.4:
-    dependencies:
-      ms: 2.1.2
-
   debug@4.3.4(supports-color@8.1.1):
     dependencies:
       ms: 2.1.2
     optionalDependencies:
       supports-color: 8.1.1
 
-  debug@4.3.5:
-    dependencies:
-      ms: 2.1.2
-
   debug@4.3.5(supports-color@5.5.0):
     dependencies:
       ms: 2.1.2
@@ -33440,7 +33258,7 @@ snapshots:
       ajv: 6.12.6
       chalk: 4.1.2
       cross-spawn: 7.0.3
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
       doctrine: 3.0.0
       escape-string-regexp: 4.0.0
       eslint-scope: 7.2.2
@@ -33535,36 +33353,10 @@ snapshots:
 
   estraverse@5.3.0: {}
 
-  estree-util-attach-comments@3.0.0:
-    dependencies:
-      '@types/estree': 1.0.5
-
-  estree-util-build-jsx@3.0.1:
-    dependencies:
-      '@types/estree-jsx': 1.0.5
-      devlop: 1.1.0
-      estree-util-is-identifier-name: 3.0.0
-      estree-walker: 3.0.3
-
   estree-util-is-identifier-name@3.0.0: {}
 
-  estree-util-to-js@2.0.0:
-    dependencies:
-      '@types/estree-jsx': 1.0.5
-      astring: 1.8.6
-      source-map: 0.7.4
-
-  estree-util-visit@2.0.0:
-    dependencies:
-      '@types/estree-jsx': 1.0.5
-      '@types/unist': 3.0.2
-
   estree-walker@2.0.2: {}
 
-  estree-walker@3.0.3:
-    dependencies:
-      '@types/estree': 1.0.5
-
   esutils@2.0.3: {}
 
   etag@1.8.1: {}
@@ -34317,8 +34109,6 @@ snapshots:
 
   flow-parser@0.238.2: {}
 
-  follow-redirects@1.15.6: {}
-
   follow-redirects@1.15.6(debug@4.3.4):
     optionalDependencies:
       debug: 4.3.4(supports-color@8.1.1)
@@ -34990,27 +34780,6 @@ snapshots:
     dependencies:
       function-bind: 1.1.2
 
-  hast-util-to-estree@3.1.0:
-    dependencies:
-      '@types/estree': 1.0.5
-      '@types/estree-jsx': 1.0.5
-      '@types/hast': 3.0.4
-      comma-separated-tokens: 2.0.3
-      devlop: 1.1.0
-      estree-util-attach-comments: 3.0.0
-      estree-util-is-identifier-name: 3.0.0
-      hast-util-whitespace: 3.0.0
-      mdast-util-mdx-expression: 2.0.0
-      mdast-util-mdx-jsx: 3.1.2
-      mdast-util-mdxjs-esm: 2.0.1
-      property-information: 6.5.0
-      space-separated-tokens: 2.0.2
-      style-to-object: 0.4.4
-      unist-util-position: 5.0.0
-      zwitch: 2.0.4
-    transitivePeerDependencies:
-      - supports-color
-
   hast-util-to-jsx-runtime@2.3.0:
     dependencies:
       '@types/estree': 1.0.5
@@ -35108,6 +34877,8 @@ snapshots:
     dependencies:
       void-elements: 3.1.0
 
+  html-url-attributes@3.0.0: {}
+
   htmlparser2@8.0.1:
     dependencies:
       domelementtype: 2.3.0
@@ -35160,7 +34931,7 @@ snapshots:
     dependencies:
       '@tootallnate/once': 1.1.2
       agent-base: 6.0.2
-      debug: 4.3.5
+      debug: 4.3.5(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -35195,7 +34966,7 @@ snapshots:
   https-proxy-agent@5.0.1:
     dependencies:
       agent-base: 6.0.2
-      debug: 4.3.5
+      debug: 4.3.5(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -35293,8 +35064,6 @@ snapshots:
       validate-npm-package-license: 3.0.4
       validate-npm-package-name: 4.0.0
 
-  inline-style-parser@0.1.1: {}
-
   inline-style-parser@0.2.3: {}
 
   inquirer@8.2.5:
@@ -35486,10 +35255,6 @@ snapshots:
 
   is-potential-custom-element-name@1.0.1: {}
 
-  is-reference@3.0.2:
-    dependencies:
-      '@types/estree': 1.0.5
-
   is-regex@1.1.4:
     dependencies:
       call-bind: 1.0.7
@@ -35658,7 +35423,7 @@ snapshots:
 
   istanbul-lib-source-maps@4.0.1:
     dependencies:
-      debug: 4.3.5
+      debug: 4.3.5(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
       source-map: 0.6.1
     transitivePeerDependencies:
@@ -36923,7 +36688,7 @@ snapshots:
       '@babel/core': 7.24.7
       '@babel/generator': 7.24.7
       '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.24.7)
-      '@babel/traverse': 7.24.7
+      '@babel/traverse': 7.24.7(supports-color@5.5.0)
       '@babel/types': 7.24.7
       '@jest/transform': 27.5.1
       '@jest/types': 27.5.1
@@ -37977,8 +37742,6 @@ snapshots:
 
   map-obj@4.3.0: {}
 
-  markdown-extensions@2.0.0: {}
-
   marked@4.3.0: {}
 
   marky@1.2.5: {}
@@ -38037,16 +37800,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  mdast-util-mdx@3.0.0:
-    dependencies:
-      mdast-util-from-markdown: 2.0.1
-      mdast-util-mdx-expression: 2.0.0
-      mdast-util-mdx-jsx: 3.1.2
-      mdast-util-mdxjs-esm: 2.0.1
-      mdast-util-to-markdown: 2.1.0
-    transitivePeerDependencies:
-      - supports-color
-
   mdast-util-mdxjs-esm@2.0.1:
     dependencies:
       '@types/estree-jsx': 1.0.5
@@ -38353,57 +38106,6 @@ snapshots:
       micromark-util-symbol: 2.0.0
       micromark-util-types: 2.0.0
 
-  micromark-extension-mdx-expression@3.0.0:
-    dependencies:
-      '@types/estree': 1.0.5
-      devlop: 1.1.0
-      micromark-factory-mdx-expression: 2.0.1
-      micromark-factory-space: 2.0.0
-      micromark-util-character: 2.1.0
-      micromark-util-events-to-acorn: 2.0.2
-      micromark-util-symbol: 2.0.0
-      micromark-util-types: 2.0.0
-
-  micromark-extension-mdx-jsx@3.0.0:
-    dependencies:
-      '@types/acorn': 4.0.6
-      '@types/estree': 1.0.5
-      devlop: 1.1.0
-      estree-util-is-identifier-name: 3.0.0
-      micromark-factory-mdx-expression: 2.0.1
-      micromark-factory-space: 2.0.0
-      micromark-util-character: 2.1.0
-      micromark-util-symbol: 2.0.0
-      micromark-util-types: 2.0.0
-      vfile-message: 4.0.2
-
-  micromark-extension-mdx-md@2.0.0:
-    dependencies:
-      micromark-util-types: 2.0.0
-
-  micromark-extension-mdxjs-esm@3.0.0:
-    dependencies:
-      '@types/estree': 1.0.5
-      devlop: 1.1.0
-      micromark-core-commonmark: 2.0.1
-      micromark-util-character: 2.1.0
-      micromark-util-events-to-acorn: 2.0.2
-      micromark-util-symbol: 2.0.0
-      micromark-util-types: 2.0.0
-      unist-util-position-from-estree: 2.0.0
-      vfile-message: 4.0.2
-
-  micromark-extension-mdxjs@3.0.0:
-    dependencies:
-      acorn: 8.11.3
-      acorn-jsx: 5.3.2(acorn@8.11.3)
-      micromark-extension-mdx-expression: 3.0.0
-      micromark-extension-mdx-jsx: 3.0.0
-      micromark-extension-mdx-md: 2.0.0
-      micromark-extension-mdxjs-esm: 3.0.0
-      micromark-util-combine-extensions: 2.0.0
-      micromark-util-types: 2.0.0
-
   micromark-factory-destination@2.0.0:
     dependencies:
       micromark-util-character: 2.1.0
@@ -38417,17 +38119,6 @@ snapshots:
       micromark-util-symbol: 2.0.0
       micromark-util-types: 2.0.0
 
-  micromark-factory-mdx-expression@2.0.1:
-    dependencies:
-      '@types/estree': 1.0.5
-      devlop: 1.1.0
-      micromark-util-character: 2.1.0
-      micromark-util-events-to-acorn: 2.0.2
-      micromark-util-symbol: 2.0.0
-      micromark-util-types: 2.0.0
-      unist-util-position-from-estree: 2.0.0
-      vfile-message: 4.0.2
-
   micromark-factory-space@2.0.0:
     dependencies:
       micromark-util-character: 2.1.0
@@ -38480,17 +38171,6 @@ snapshots:
 
   micromark-util-encode@2.0.0: {}
 
-  micromark-util-events-to-acorn@2.0.2:
-    dependencies:
-      '@types/acorn': 4.0.6
-      '@types/estree': 1.0.5
-      '@types/unist': 3.0.2
-      devlop: 1.1.0
-      estree-util-visit: 2.0.0
-      micromark-util-symbol: 2.0.0
-      micromark-util-types: 2.0.0
-      vfile-message: 4.0.2
-
   micromark-util-html-tag-name@2.0.0: {}
 
   micromark-util-normalize-identifier@2.0.0:
@@ -39817,12 +39497,6 @@ snapshots:
 
   performance-now@2.1.0: {}
 
-  periscopic@3.1.0:
-    dependencies:
-      '@types/estree': 1.0.5
-      estree-walker: 3.0.3
-      is-reference: 3.0.2
-
   picocolors@1.0.0: {}
 
   picocolors@1.0.1: {}
@@ -40648,6 +40322,23 @@ snapshots:
 
   react-lifecycles-compat@3.0.4: {}
 
+  react-markdown@9.0.1(@types/react@18.3.3)(react@18.3.1):
+    dependencies:
+      '@types/hast': 3.0.4
+      '@types/react': 18.3.3
+      devlop: 1.1.0
+      hast-util-to-jsx-runtime: 2.3.0
+      html-url-attributes: 3.0.0
+      mdast-util-to-hast: 13.2.0
+      react: 18.3.1
+      remark-parse: 11.0.0
+      remark-rehype: 11.1.0
+      unified: 11.0.5
+      unist-util-visit: 5.0.0
+      vfile: 6.0.1
+    transitivePeerDependencies:
+      - supports-color
+
   react-modal@3.16.1(react-dom@16.13.1(react@16.13.1))(react@16.13.1):
     dependencies:
       exenv: 1.2.2
@@ -41044,13 +40735,6 @@ snapshots:
     dependencies:
       jsesc: 0.5.0
 
-  remark-mdx@3.0.1:
-    dependencies:
-      mdast-util-mdx: 3.0.0
-      micromark-extension-mdxjs: 3.0.0
-    transitivePeerDependencies:
-      - supports-color
-
   remark-parse@11.0.0:
     dependencies:
       '@types/mdast': 4.0.4
@@ -42007,10 +41691,6 @@ snapshots:
       minimist: 1.2.7
       through: 2.3.8
 
-  style-to-object@0.4.4:
-    dependencies:
-      inline-style-parser: 0.1.1
-
   style-to-object@1.0.6:
     dependencies:
       inline-style-parser: 0.2.3
@@ -42949,7 +42629,7 @@ snapshots:
       '@tsconfig/node14': 1.0.3
       '@tsconfig/node16': 1.0.3
       '@types/node': 20.14.9
-      acorn: 8.12.0
+      acorn: 8.11.3
       acorn-walk: 8.2.0
       arg: 4.1.3
       create-require: 1.1.1
@@ -43236,10 +42916,6 @@ snapshots:
     dependencies:
       '@types/unist': 3.0.2
 
-  unist-util-position-from-estree@2.0.0:
-    dependencies:
-      '@types/unist': 3.0.2
-
   unist-util-position@5.0.0:
     dependencies:
       '@types/unist': 3.0.2