ソースを参照

Add remaining evm apis to api reference

Connor Prussin 1 年間 前
コミット
2dceecb3e5
41 ファイル変更2531 行追加1313 行削除
  1. 20 17
      apps/api-reference/package.json
  2. 0 568
      apps/api-reference/src/apis/evm.ts
  3. 102 0
      apps/api-reference/src/apis/evm/common.ts
  4. 65 0
      apps/api-reference/src/apis/evm/get-ema-price-no-older-than.ts
  5. 56 0
      apps/api-reference/src/apis/evm/get-ema-price-unsafe.ts
  6. 61 0
      apps/api-reference/src/apis/evm/get-ema-price.ts
  7. 64 0
      apps/api-reference/src/apis/evm/get-price-no-older-than.ts
  8. 54 0
      apps/api-reference/src/apis/evm/get-price-unsafe.ts
  9. 59 0
      apps/api-reference/src/apis/evm/get-price.ts
  10. 67 0
      apps/api-reference/src/apis/evm/get-update-fee.tsx
  11. 18 0
      apps/api-reference/src/apis/evm/get-valid-time-period.ts
  12. 12 0
      apps/api-reference/src/apis/evm/index.ts
  13. 129 0
      apps/api-reference/src/apis/evm/parse-price-feed-updates-unique.tsx
  14. 127 0
      apps/api-reference/src/apis/evm/parse-price-feed-updates.tsx
  15. 121 0
      apps/api-reference/src/apis/evm/update-price-feeds-if-necessary.tsx
  16. 93 0
      apps/api-reference/src/apis/evm/update-price-feeds.tsx
  17. 1 3
      apps/api-reference/src/apis/index.tsx
  18. 1 53
      apps/api-reference/src/app/layout.tsx
  19. 12 7
      apps/api-reference/src/app/price-feeds/[chain]/[method]/layout.tsx
  20. 2 1
      apps/api-reference/src/app/price-feeds/[chain]/[method]/page.tsx
  21. 22 0
      apps/api-reference/src/app/price-feeds/[chain]/layout.tsx
  22. 38 7
      apps/api-reference/src/components/Code/index.tsx
  23. 17 3
      apps/api-reference/src/components/Code/shiki.ts
  24. 4 7
      apps/api-reference/src/components/ColorThemeSelector/index.tsx
  25. 301 0
      apps/api-reference/src/components/EvmApi/index.tsx
  26. 18 0
      apps/api-reference/src/components/EvmApi/networks.ts
  27. 82 0
      apps/api-reference/src/components/EvmApi/parameter-input.tsx
  28. 70 0
      apps/api-reference/src/components/EvmApi/parameter.ts
  29. 118 0
      apps/api-reference/src/components/EvmApi/results-modal.tsx
  30. 242 0
      apps/api-reference/src/components/EvmApi/run-button.tsx
  31. 0 190
      apps/api-reference/src/components/EvmCall/index.tsx
  32. 0 13
      apps/api-reference/src/components/EvmCall/networks.ts
  33. 0 126
      apps/api-reference/src/components/EvmCall/parameter-input.tsx
  34. 0 241
      apps/api-reference/src/components/EvmCall/run-button.tsx
  35. 60 0
      apps/api-reference/src/components/EvmLayout/index.tsx
  36. 1 1
      apps/api-reference/src/components/Sidebar/index.tsx
  37. 0 21
      apps/api-reference/src/layouts.tsx
  38. 52 0
      apps/api-reference/src/metadata.ts
  39. 11 0
      apps/api-reference/src/use-is-mounted.ts
  40. 19 0
      apps/api-reference/src/zod-utils.ts
  41. 412 55
      pnpm-lock.yaml

+ 20 - 17
apps/api-reference/package.json

@@ -20,25 +20,28 @@
     "test:unit": "jest --selectProjects unit --passWithNoTests"
   },
   "dependencies": {
-    "@amplitude/analytics-browser": "^2.8.1",
-    "@floating-ui/react": "^0.26.16",
+    "@amplitude/analytics-browser": "^2.9.0",
+    "@floating-ui/react": "^0.26.17",
     "@headlessui/react": "^2.0.4",
-    "@heroicons/react": "^2.1.3",
+    "@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.3",
-    "@next/third-parties": "^14.2.3",
-    "@pythnetwork/pyth-sdk-solidity": "^3.1.0",
-    "@wagmi/core": "^2.10.5",
+    "@next/mdx": "^14.2.4",
+    "@next/third-parties": "^14.2.4",
+    "@pythnetwork/pyth-sdk-solidity": "workspace:^",
+    "@tanstack/react-query": "^5.45.1",
     "clsx": "^2.1.1",
-    "next": "^14.2.3",
+    "connectkit": "^1.8.2",
+    "cryptocurrency-icons": "^0.18.1",
+    "next": "^14.2.4",
     "next-themes": "^0.3.0",
-    "pino": "^9.1.0",
+    "pino": "^9.2.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
-    "shiki": "^1.6.2",
-    "viem": "^2.13.3",
+    "shiki": "^1.7.0",
+    "viem": "^2.15.1",
+    "wagmi": "^2.10.4",
     "zod": "^3.23.8"
   },
   "devDependencies": {
@@ -51,16 +54,16 @@
     "@tailwindcss/forms": "^0.5.7",
     "@types/jest": "^29.5.12",
     "@types/mdx": "^2.0.13",
-    "@types/node": "^20.14.0",
+    "@types/node": "^20.14.6",
     "@types/react": "^18.3.3",
     "@types/react-dom": "^18.3.0",
     "autoprefixer": "^10.4.19",
-    "eslint": "^9.4.0",
+    "eslint": "^9.5.0",
     "jest": "^29.7.0",
     "postcss": "^8.4.38",
-    "prettier": "^3.3.0",
-    "tailwindcss": "^3.4.3",
-    "typescript": "^5.4.5",
-    "vercel": "^34.2.4"
+    "prettier": "^3.3.2",
+    "tailwindcss": "^3.4.4",
+    "typescript": "^5.5.2",
+    "vercel": "^34.2.7"
   }
 }

+ 0 - 568
apps/api-reference/src/apis/evm.ts

@@ -1,568 +0,0 @@
-import type { ComponentProps } from "react";
-
-import {
-  type EvmCall as EvmCallComponent,
-  Language,
-  ParameterType,
-} from "../components/EvmCall";
-
-type EvmCall = Omit<ComponentProps<typeof EvmCallComponent>, "children"> & {
-  description: string;
-};
-
-const BTCUSD =
-  "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43";
-const ETHUSD =
-  "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace";
-
-const getPrice: EvmCall = {
-  name: "getPrice",
-  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
-[price feed ids](https://pyth.network/developers/price-feed-ids) page to look up
-the id for a given symbol. The returned price and confidence are decimal numbers
-written in the form \`a * 10^e\`, where \`e\` is an exponent included in the
-result. For example, a price of 1234 with an exponent of -2 represents the
-number 12.34. The result also includes a \`publishTime\` which is the unix
-timestamp for the price update.
-
-This function reverts with a \`StalePrice\` error if the on-chain price has not
-been updated within the last [getValidTimePeriod()](getValidTimePeriod)
-seconds. The default valid time period is set to a reasonable default on each
-chain and is typically around 1 minute. Call
-[updatePriceFeeds](updatePriceFeeds) to pull a fresh price on-chain and solve
-this problem. If you would like to configure the valid time period, see
-[getPriceNoOlderThan](getPriceNoOlderThan). If you want the latest price
-regardless of when it was updated, see [getPriceUnsafe](getPriceUnsafe).
-
-This function reverts with a \`PriceFeedNotFound\` error if the requested feed
-id has never received a price update. This error could either mean that the
-provided price feed id is incorrect, or (more typically) that this is the first
-attempted use of that feed on-chain. In the second case, calling
-[updatePriceFeeds](updatePriceFeeds) will solve this problem.
-`,
-  parameters: [
-    {
-      name: "id",
-      type: ParameterType.Hex,
-      description: "The ID of the price feed you want to read",
-    },
-  ],
-  examples: [
-    {
-      name: "BTC/USD",
-      parameters: {
-        id: BTCUSD,
-      },
-    },
-    {
-      name: "ETH/USD",
-      parameters: {
-        id: ETHUSD,
-      },
-    },
-  ],
-  code: [
-    {
-      language: Language.Solidity,
-      code: (network, { id }) => `
-import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
-import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
-
-// ${network.name}
-address contractAddress = ${network.contractAddress}
-IPyth pyth = IPyth(contractAddress);
-
-bytes32 priceId = ${id ?? "/* <id> */"};
-PythStructs.Price memory currentBasePrice = pyth.getPrice(priceId);
-        `,
-    },
-    {
-      language: Language.EthersJSV6,
-      code: (network, { id }) => `
-import { ethers } from "ethers";
-import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json" assert { type: "json" };
-
-// ${network.name}
-const contractAddress = '${network.contractAddress}';
-const provider = ethers.getDefaultProvider('${network.rpcUrl}');
-const contract = new ethers.Contract(contractAddress, PythAbi, provider);
-
-const priceId = ${id ? `'${id}'` : "/* <id> */"};
-const [price, conf, expo, timestamp] = await contract.getPrice(priceId);
-        `,
-    },
-  ],
-};
-
-const getPriceUnsafe: EvmCall = {
-  name: "getPriceUnsafe",
-  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
-[price feed ids](https://pyth.network/developers/price-feed-ids) page to look up
-the id for a given symbol. The returned price and confidence are decimal numbers
-written in the form \`a * 10^e\`, where \`e\` is an exponent included in the
-result. For example, a price of 1234 with an exponent of -2 represents the
-number 12.34. The result also includes a \`publishTime\` which is the unix
-timestamp for the price update.
-
-**This function may return a price from arbitrarily far in the past.** It is the
-caller's responsibility to check the returned \`publishTime\` to ensure that the
-update is recent enough for their use case.
-
-This function reverts with a \`PriceFeedNotFound\` error if the requested feed
-id has never received a price update. This error could either mean that the
-provided price feed id is incorrect, or (more typically) that this is the first
-attempted use of that feed on-chain. In the second case, calling
-[updatePriceFeeds](updatePriceFeeds) will solve this problem.
-`,
-  parameters: [
-    {
-      name: "id",
-      type: ParameterType.Hex,
-      description: "The ID of the price feed you want to read",
-    },
-  ],
-  examples: [
-    {
-      name: "BTC/USD",
-      parameters: {
-        id: BTCUSD,
-      },
-    },
-    {
-      name: "ETH/USD",
-      parameters: {
-        id: ETHUSD,
-      },
-    },
-  ],
-  code: [
-    {
-      language: Language.Solidity,
-      code: (network, { id }) => `
-import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
-import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
-
-// ${network.name}
-address contractAddress = ${network.contractAddress};
-IPyth pyth = IPyth(contractAddress);
-
-bytes32 priceId = ${id ?? "/* <id> */"};
-PythStructs.Price memory currentBasePrice = pyth.getPriceUnsafe(priceId);
-        `,
-    },
-    {
-      language: Language.EthersJSV6,
-      code: (network, { id }) => `
-import { ethers } from "ethers";
-import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json" assert { type: "json" };
-
-// ${network.name}
-const contractAddress = '${network.contractAddress}';
-const provider = ethers.getDefaultProvider('${network.rpcUrl}');
-const contract = new ethers.Contract(contractAddress, PythAbi, provider);
-
-const priceId = ${id ? `'${id}'` : "/* <id> */"};
-const [price, conf, expo, timestamp] = await contract.getPriceUnsafe(priceId);
-        `,
-    },
-  ],
-};
-
-const getPriceNoOlderThan: EvmCall = {
-  name: "getPriceNoOlderThan",
-  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
-written as a hexadecimal string; see the [price feed
-ids](https://pyth.network/developers/price-feed-ids) page to look up the id for
-a given symbol.  The returned price and confidence are decimal numbers written
-in the form \`a * 10^e\`, where \`e\` is an exponent included in the result.
-For example, a price of 1234 with an exponent of -2 represents the number 12.34.
-The result also includes a \`publishTime\` which is the unix timestamp for the
-price update.
-
-The caller provides an \`age\` argument that specifies how old the price can be.
-The call reverts with a \`StalePriceError\` if the on-chain price is from more
-than \`age\` seconds in the past (with respect to the current on-chain
-timestamp).  Call [updatePriceFeeds](updatePriceFeeds) to pull a fresh price
-on-chain and solve this problem.
-
-This function reverts with a \`PriceFeedNotFound\` error if the requested feed
-id has never received a price update.  This error could either mean that the
-provided price feed id is incorrect, or (more typically) that this is the first
-attempted use of that feed on-chain. In the second case, calling
-[updatePriceFeeds](updatePriceFeeds) will solve this problem.
-`,
-  parameters: [
-    {
-      name: "id",
-      type: ParameterType.Hex,
-      description: "The ID of the price feed you want to read",
-    },
-    {
-      name: "age",
-      type: ParameterType.Int,
-      description: "Maximum age of the on-chain price in seconds.",
-    },
-  ],
-  examples: [
-    {
-      name: "BTC/USD",
-      parameters: {
-        id: BTCUSD,
-        age: "60",
-      },
-    },
-    {
-      name: "ETH/USD",
-      parameters: {
-        id: ETHUSD,
-        age: "60",
-      },
-    },
-  ],
-  code: [
-    {
-      language: Language.Solidity,
-      code: (network, { id, age }) => `
-import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
-import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
-
-// ${network.name}
-address contractAddress = ${network.contractAddress};
-IPyth pyth = IPyth(contractAddress);
-
-bytes32 priceId = ${id ?? "/* <id> */"};
-uint256 age = ${age ?? "/* <age> */"};
-PythStructs.Price memory currentBasePrice = pyth.getPriceNoOlderThan(priceId, age);
-        `,
-    },
-    {
-      language: Language.EthersJSV6,
-      code: (network, { id, age }) => `
-import { ethers } from "ethers";
-import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json" assert { type: "json" };
-
-// ${network.name}
-const contractAddress = '${network.contractAddress}';
-const provider = ethers.getDefaultProvider('${network.rpcUrl}');
-const contract = new ethers.Contract(contractAddress, PythAbi, provider);
-
-const priceId = ${id ? `'${id}'` : "/* <id> */"};
-const age = ${age ? `'${age}'` : "/* <age> */"};
-const [price, conf, expo, timestamp] = await contract.getPriceNoOlderThan(priceId, age);
-        `,
-    },
-  ],
-};
-
-const getEmaPrice: EvmCall = {
-  name: "getEmaPrice",
-  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
-written as a hexadecimal string; see the [price feed
-ids](https://pyth.network/developers/price-feed-ids) page to look up the id for
-a given symbol.  The returned price and confidence are decimal numbers written
-in the form \`a * 10^e\`, where \`e\` is an exponent included in the result.
-For example, a price of 1234 with an exponent of -2 represents the number 12.34.
-The result also includes a \`publishTime\` which is the unix timestamp for the
-price update.  The EMA methodology is described in more detail in this [blog
-post](https://pythnetwork.medium.com/whats-in-a-name-302a03e6c3e1).
-
-This function reverts with a \`StalePrice\` error if the on-chain price has not
-been updated within the last [getValidTimePeriod()](getValidTimePeriod) seconds.
-The default valid time period is set to a reasonable default on each chain and
-is typically around 1 minute.  Call [updatePriceFeeds](updatePriceFeeds) to pull
-a fresh price on-chain and solve this problem.  If you would like to configure
-the valid time period, see [getEmaPriceNoOlderThan](getEmaPriceNoOlderThan).  If
-you want the latest price regardless of when it was updated, see
-[getEmaPriceUnsafe](getEmaPriceUnsafe).
-
-This function reverts with a \`PriceFeedNotFound\` error if the requested feed
-id has never received a price update.  This error could either mean that the
-provided price feed id is incorrect, or (more typically) that this is the first
-attempted use of that feed on-chain. In the second case, calling
-[updatePriceFeeds](updatePriceFeeds) will solve this problem.
-`,
-  parameters: [
-    {
-      name: "id",
-      type: ParameterType.Hex,
-      description: "The ID of the price feed you want to read",
-    },
-  ],
-  examples: [
-    {
-      name: "BTC/USD",
-      parameters: {
-        id: BTCUSD,
-      },
-    },
-    {
-      name: "ETH/USD",
-      parameters: {
-        id: ETHUSD,
-      },
-    },
-  ],
-  code: [
-    {
-      language: Language.Solidity,
-      code: (network, { id }) => `
-import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
-import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
-
-// ${network.name}
-address contractAddress = ${network.contractAddress}
-IPyth pyth = IPyth(contractAddress);
-
-bytes32 priceId = ${id ?? "/* <id> */"};
-PythStructs.Price memory currentBasePrice = pyth.getEmaPrice(priceId);
-        `,
-    },
-    {
-      language: Language.EthersJSV6,
-      code: (network, { id }) => `
-import { ethers } from "ethers";
-import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json" assert { type: "json" };
-
-// ${network.name}
-const contractAddress = '${network.contractAddress}';
-const provider = ethers.getDefaultProvider('${network.rpcUrl}');
-const contract = new ethers.Contract(contractAddress, PythAbi, provider);
-
-const priceId = ${id ? `'${id}'` : "/* <id> */"};
-const [price, conf, expo, timestamp] = await contract.getEmaPrice(priceId);
-        `,
-    },
-  ],
-};
-
-const getEmaPriceUnsafe: EvmCall = {
-  name: "getEmaPriceUnsafe",
-  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
-written as a hexadecimal string; see the [price feed
-ids](https://pyth.network/developers/price-feed-ids) page to look up the id for
-a given symbol.  The returned price and confidence are decimal numbers written
-in the form \`a * 10^e\`, where \`e\` is an exponent included in the result.
-For example, a price of 1234 with an exponent of -2 represents the number 12.34.
-The result also includes a \`publishTime\` which is the unix timestamp for the
-price update.  The EMA methodology is described in more detail in this [blog
-post](https://pythnetwork.medium.com/whats-in-a-name-302a03e6c3e1).
-
-**This function may return a price from arbitrarily far in the past.** It is the
-caller's responsibility to check the returned \`publishTime\` to ensure that the
-update is recent enough for their use case.
-
-This function reverts with a \`PriceFeedNotFound\` error if the requested feed
-id has never received a price update.  This error could either mean that the
-provided price feed id is incorrect, or (more typically) that this is the first
-attempted use of that feed on-chain. In the second case, calling
-[updatePriceFeeds](updatePriceFeeds) will solve this problem.
-`,
-  parameters: [
-    {
-      name: "id",
-      type: ParameterType.Hex,
-      description: "The ID of the price feed you want to read",
-    },
-  ],
-  examples: [
-    {
-      name: "BTC/USD",
-      parameters: {
-        id: BTCUSD,
-      },
-    },
-    {
-      name: "ETH/USD",
-      parameters: {
-        id: ETHUSD,
-      },
-    },
-  ],
-  code: [
-    {
-      language: Language.Solidity,
-      code: (network, { id }) => `
-import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
-import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
-
-// ${network.name}
-address contractAddress = ${network.contractAddress}
-IPyth pyth = IPyth(contractAddress);
-
-bytes32 priceId = ${id ?? "/* <id> */"};
-PythStructs.Price memory currentBasePrice = pyth.getEmaPriceUnsafe(priceId);
-        `,
-    },
-    {
-      language: Language.EthersJSV6,
-      code: (network, { id }) => `
-import { ethers } from "ethers";
-import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json" assert { type: "json" };
-
-// ${network.name}
-const contractAddress = '${network.contractAddress}';
-const provider = ethers.getDefaultProvider('${network.rpcUrl}');
-const contract = new ethers.Contract(contractAddress, PythAbi, provider);
-
-const priceId = ${id ? `'${id}'` : "/* <id> */"};
-const [price, conf, expo, timestamp] = await contract.getEmaPriceUnsafe(priceId);
-        `,
-    },
-  ],
-};
-
-const getEmaPriceNoOlderThan: EvmCall = {
-  name: "getEmaPriceNoOlderThan",
-  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
-written as a hexadecimal string; see the [price feed
-ids](https://pyth.network/developers/price-feed-ids) page to look up the id for
-a given symbol.  The returned price and confidence are decimal numbers written
-in the form \`a * 10^e\`, where \`e\` is an exponent included in the result.
-For example, a price of 1234 with an exponent of -2 represents the number 12.34.
-The result also includes a \`publishTime\` which is the unix timestamp for the
-price update.  The EMA methodology is described in more detail in this [blog
-post](https://pythnetwork.medium.com/whats-in-a-name-302a03e6c3e1).
-
-The caller provides an \`age\` argument that specifies how old the price can be.
-The call reverts with a \`StalePriceError\` if the on-chain price is from more
-than \`age\` seconds in the past (with respect to the current on-chain
-timestamp).  Call [updatePriceFeeds](updatePriceFeeds) to pull a fresh price
-on-chain and solve this problem.
-
-This function reverts with a \`PriceFeedNotFound\` error if the requested feed
-id has never received a price update.  This error could either mean that the
-provided price feed id is incorrect, or (more typically) that this is the first
-attempted use of that feed on-chain. In the second case, calling
-[updatePriceFeeds](updatePriceFeeds) will solve this problem.
-`,
-  parameters: [
-    {
-      name: "id",
-      type: ParameterType.Hex,
-      description: "The ID of the price feed you want to read",
-    },
-    {
-      name: "age",
-      type: ParameterType.Int,
-      description: "Maximum age of the on-chain price in seconds.",
-    },
-  ],
-  examples: [
-    {
-      name: "BTC/USD",
-      parameters: {
-        id: BTCUSD,
-        age: "60",
-      },
-    },
-    {
-      name: "ETH/USD",
-      parameters: {
-        id: ETHUSD,
-        age: "60",
-      },
-    },
-  ],
-  code: [
-    {
-      language: Language.Solidity,
-      code: (network, { id, age }) => `
-import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
-import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
-
-// ${network.name}
-address contractAddress = ${network.contractAddress};
-IPyth pyth = IPyth(contractAddress);
-
-bytes32 priceId = ${id ?? "/* <id> */"};
-uint256 age = ${age ?? "/* <age> */"};
-PythStructs.Price memory currentBasePrice = pyth.getEmaPriceNoOlderThan(priceId, age);
-        `,
-    },
-    {
-      language: Language.EthersJSV6,
-      code: (network, { id, age }) => `
-import { ethers } from "ethers";
-import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json" assert { type: "json" };
-
-// ${network.name}
-const contractAddress = '${network.contractAddress}';
-const provider = ethers.getDefaultProvider('${network.rpcUrl}');
-const contract = new ethers.Contract(contractAddress, PythAbi, provider);
-
-const priceId = ${id ? `'${id}'` : "/* <id> */"};
-const age = ${age ? `'${age}'` : "/* <age> */"};
-const [price, conf, expo, timestamp] = await contract.getEmaPriceNoOlderThan(priceId, age);
-        `,
-    },
-  ],
-};
-
-const getValidTimePeriod: EvmCall = {
-  name: "getValidTimePeriod",
-  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
-[getEmaPrice](getEmaPrice); these functions revert if the current on-chain price
-is older than this period.  The valid time period is configured to be a sane
-default for each blockchain.
-`,
-  parameters: [],
-  examples: [],
-  code: [
-    {
-      language: Language.Solidity,
-      code: (network) => `
-import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
-import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
-
-// ${network.name}
-address contractAddress = ${network.contractAddress};
-IPyth pyth = IPyth(contractAddress);
-
-uint validTimePeriod = pyth.getValidTimePeriod();
-        `,
-    },
-    {
-      language: Language.EthersJSV6,
-      code: (network) => `
-import { ethers } from "ethers";
-import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json" assert { type: "json" };
-
-// ${network.name}
-const contractAddress = '${network.contractAddress}';
-const provider = ethers.getDefaultProvider('${network.rpcUrl}');
-const contract = new ethers.Contract(contractAddress, PythAbi, provider);
-
-const [validTimePeriod] = await contract.getValidTimePeriod();
-        `,
-    },
-  ],
-};
-
-export const evm = {
-  getEmaPrice,
-  getEmaPriceNoOlderThan,
-  getEmaPriceUnsafe,
-  getPrice,
-  getPriceNoOlderThan,
-  getPriceUnsafe,
-  //     getUpdateFee
-  getValidTimePeriod,
-  //     parsePriceFeedUpdates
-  //     parsePriceFeedUpdatesUnique
-  //     updatePriceFeeds
-  //     updatePriceFeedsIfNecessary
-};

+ 102 - 0
apps/api-reference/src/apis/evm/common.ts

@@ -0,0 +1,102 @@
+import { z } from "zod";
+
+import {
+  type ReadApi,
+  type WriteApi,
+  type NetworkInfo,
+  EvmApiType,
+  Language,
+} from "../../components/EvmApi";
+import { singletonArray, safeFetch } from "../../zod-utils";
+
+export const readApi = <ParameterName extends string>(
+  spec: Omit<ReadApi<ParameterName>, "children" | "type"> & {
+    description: string;
+  },
+) => ({
+  ...spec,
+  type: EvmApiType.Read,
+});
+
+export const writeApi = <ParameterName extends string>(
+  spec: Omit<WriteApi<ParameterName>, "children" | "type"> & {
+    description: string;
+  },
+) => ({
+  ...spec,
+  type: EvmApiType.Write,
+});
+
+export const BTCUSD =
+  "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43";
+export const ETHUSD =
+  "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace";
+
+const HERMES_URL = "https://hermes.pyth.network";
+
+export const getLatestPriceFeed = async (feedId: string) => {
+  const url = new URL("/api/latest_price_feeds", HERMES_URL);
+  url.searchParams.set("ids[]", feedId);
+  url.searchParams.set("target_chain", "evm");
+  url.searchParams.set("binary", "true");
+  return safeFetch(priceFeedSchema, url);
+};
+
+const priceFeedSchema = singletonArray(
+  z.object({
+    vaa: z.string().transform((value) => toZeroXPrefixedHex(value)),
+    price: z.object({
+      publish_time: z.number(),
+    }),
+  }),
+);
+
+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),
+) => ({
+  language: Language.Solidity,
+  dimRange: [
+    { line: 0, character: 0 },
+    { line: 7, character: 0 },
+  ] as const,
+  code: (
+    network: NetworkInfo,
+    params: Partial<Record<ParameterName, string>>,
+  ) => `
+import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
+import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
+
+// ${network.name}
+address contractAddress = ${network.contractAddress}
+IPyth pyth = IPyth(contractAddress);
+
+${typeof code === "string" ? code.trim() : code(params).trim()}
+  `,
+});
+
+export const ethersJS = <ParameterName extends string>(
+  code: string | ((params: Partial<Record<ParameterName, string>>) => string),
+) => ({
+  language: Language.EthersJSV6,
+  dimRange: [
+    { line: 0, character: 0 },
+    { line: 8, character: 0 },
+  ] as const,
+  code: (
+    network: NetworkInfo,
+    params: Partial<Record<ParameterName, string>>,
+  ) => `
+import { ethers } from 'ethers';
+import PythAbi from '@pythnetwork/pyth-sdk-solidity/abis/IPyth.json' assert { type: 'json' };
+
+// ${network.name}
+const contractAddress = '${network.contractAddress}';
+const provider = ethers.getDefaultProvider('${network.rpcUrl}');
+const contract = new ethers.Contract(contractAddress, PythAbi, provider);
+
+${typeof code === "string" ? code.trim() : code(params).trim()}
+  `,
+});

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

@@ -0,0 +1,65 @@
+import Btc from "cryptocurrency-icons/svg/color/btc.svg";
+import Eth from "cryptocurrency-icons/svg/color/eth.svg";
+
+import { readApi, BTCUSD, ETHUSD, solidity, ethersJS } from "./common";
+import { ParameterType } from "../../components/EvmApi";
+
+export const getEmaPriceNoOlderThan = readApi<"id" | "age">({
+  name: "getEmaPriceNoOlderThan",
+  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
+written as a hexadecimal string; see the [price feed
+ids](https://pyth.network/developers/price-feed-ids) page to look up the id for
+a given symbol.  The returned price and confidence are decimal numbers written
+in the form \`a * 10^e\`, where \`e\` is an exponent included in the result.
+For example, a price of 1234 with an exponent of -2 represents the number 12.34.
+The result also includes a \`publishTime\` which is the unix timestamp for the
+price update.  The EMA methodology is described in more detail in this [blog
+post](https://pythnetwork.medium.com/whats-in-a-name-302a03e6c3e1).
+
+The caller provides an \`age\` argument that specifies how old the price can be.
+The call reverts with a \`StalePriceError\` if the on-chain price is from more
+than \`age\` seconds in the past (with respect to the current on-chain
+timestamp).  Call [updatePriceFeeds](updatePriceFeeds) to pull a fresh price
+on-chain and solve this problem.
+
+This function reverts with a \`PriceFeedNotFound\` error if the requested feed
+id has never received a price update.  This error could either mean that the
+provided price feed id is incorrect, or (more typically) that this is the first
+attempted use of that feed on-chain. In the second case, calling
+[updatePriceFeeds](updatePriceFeeds) will solve this problem.
+  `,
+  parameters: [
+    {
+      name: "id",
+      type: ParameterType.Hex,
+      description: "The ID of the price feed you want to read",
+    },
+    {
+      name: "age",
+      type: ParameterType.Int,
+      description: "Maximum age of the on-chain price in seconds.",
+    },
+  ],
+  examples: [
+    { name: "BTC/USD", icon: Btc, parameters: { id: BTCUSD, age: "60" } },
+    { name: "ETH/USD", icon: Eth, parameters: { id: ETHUSD, age: "60" } },
+  ],
+  code: [
+    solidity(
+      ({ id, age }) => `
+bytes32 priceId = ${id ?? "/* <id> */"};
+uint256 age = ${age ?? "/* <age> */"};
+PythStructs.Price memory currentBasePrice = pyth.getEmaPriceNoOlderThan(priceId, age);
+    `,
+    ),
+    ethersJS(
+      ({ id, age }) => `
+const priceId = ${id ? `'${id}'` : "/* <id> */"};
+const age = ${age ? `'${age}'` : "/* <age> */"};
+const [price, conf, expo, timestamp] = await contract.getEmaPriceNoOlderThan(priceId, age);
+    `,
+    ),
+  ],
+});

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

@@ -0,0 +1,56 @@
+import Btc from "cryptocurrency-icons/svg/color/btc.svg";
+import Eth from "cryptocurrency-icons/svg/color/eth.svg";
+
+import { readApi, BTCUSD, ETHUSD, solidity, ethersJS } from "./common";
+import { ParameterType } from "../../components/EvmApi";
+
+export const getEmaPriceUnsafe = readApi<"id">({
+  name: "getEmaPriceUnsafe",
+  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
+written as a hexadecimal string; see the [price feed
+ids](https://pyth.network/developers/price-feed-ids) page to look up the id for
+a given symbol.  The returned price and confidence are decimal numbers written
+in the form \`a * 10^e\`, where \`e\` is an exponent included in the result.
+For example, a price of 1234 with an exponent of -2 represents the number 12.34.
+The result also includes a \`publishTime\` which is the unix timestamp for the
+price update.  The EMA methodology is described in more detail in this [blog
+post](https://pythnetwork.medium.com/whats-in-a-name-302a03e6c3e1).
+
+**This function may return a price from arbitrarily far in the past.** It is the
+caller's responsibility to check the returned \`publishTime\` to ensure that the
+update is recent enough for their use case.
+
+This function reverts with a \`PriceFeedNotFound\` error if the requested feed
+id has never received a price update.  This error could either mean that the
+provided price feed id is incorrect, or (more typically) that this is the first
+attempted use of that feed on-chain. In the second case, calling
+[updatePriceFeeds](updatePriceFeeds) will solve this problem.
+  `,
+  parameters: [
+    {
+      name: "id",
+      type: ParameterType.Hex,
+      description: "The ID of the price feed you want to read",
+    },
+  ],
+  examples: [
+    { name: "BTC/USD", icon: Btc, parameters: { id: BTCUSD } },
+    { name: "ETH/USD", icon: Eth, parameters: { id: ETHUSD } },
+  ],
+  code: [
+    solidity(
+      ({ id }) => `
+bytes32 priceId = ${id ?? "/* <id> */"};
+PythStructs.Price memory currentBasePrice = pyth.getEmaPriceUnsafe(priceId);
+    `,
+    ),
+    ethersJS(
+      ({ id }) => `
+const priceId = ${id ? `'${id}'` : "/* <id> */"};
+const [price, conf, expo, timestamp] = await contract.getEmaPriceUnsafe(priceId);
+    `,
+    ),
+  ],
+});

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

@@ -0,0 +1,61 @@
+import Btc from "cryptocurrency-icons/svg/color/btc.svg";
+import Eth from "cryptocurrency-icons/svg/color/eth.svg";
+
+import { readApi, BTCUSD, ETHUSD, solidity, ethersJS } from "./common";
+import { ParameterType } from "../../components/EvmApi";
+
+export const getEmaPrice = readApi<"id">({
+  name: "getEmaPrice",
+  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
+written as a hexadecimal string; see the [price feed
+ids](https://pyth.network/developers/price-feed-ids) page to look up the id for
+a given symbol.  The returned price and confidence are decimal numbers written
+in the form \`a * 10^e\`, where \`e\` is an exponent included in the result.
+For example, a price of 1234 with an exponent of -2 represents the number 12.34.
+The result also includes a \`publishTime\` which is the unix timestamp for the
+price update.  The EMA methodology is described in more detail in this [blog
+post](https://pythnetwork.medium.com/whats-in-a-name-302a03e6c3e1).
+
+This function reverts with a \`StalePrice\` error if the on-chain price has not
+been updated within the last [getValidTimePeriod()](getValidTimePeriod) seconds.
+The default valid time period is set to a reasonable default on each chain and
+is typically around 1 minute.  Call [updatePriceFeeds](updatePriceFeeds) to pull
+a fresh price on-chain and solve this problem.  If you would like to configure
+the valid time period, see [getEmaPriceNoOlderThan](getEmaPriceNoOlderThan).  If
+you want the latest price regardless of when it was updated, see
+[getEmaPriceUnsafe](getEmaPriceUnsafe).
+
+This function reverts with a \`PriceFeedNotFound\` error if the requested feed
+id has never received a price update.  This error could either mean that the
+provided price feed id is incorrect, or (more typically) that this is the first
+attempted use of that feed on-chain. In the second case, calling
+[updatePriceFeeds](updatePriceFeeds) will solve this problem.
+  `,
+  parameters: [
+    {
+      name: "id",
+      type: ParameterType.Hex,
+      description: "The ID of the price feed you want to read",
+    },
+  ],
+  examples: [
+    { name: "BTC/USD", icon: Btc, parameters: { id: BTCUSD } },
+    { name: "ETH/USD", icon: Eth, parameters: { id: ETHUSD } },
+  ],
+  code: [
+    solidity(
+      ({ id }) => `
+bytes32 priceId = ${id ?? "/* <id> */"};
+PythStructs.Price memory currentBasePrice = pyth.getEmaPrice(priceId);
+    `,
+    ),
+    ethersJS(
+      ({ id }) => `
+const priceId = ${id ? `'${id}'` : "/* <id> */"};
+const [price, conf, expo, timestamp] = await contract.getEmaPrice(priceId);
+    `,
+    ),
+  ],
+});

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

@@ -0,0 +1,64 @@
+import Btc from "cryptocurrency-icons/svg/color/btc.svg";
+import Eth from "cryptocurrency-icons/svg/color/eth.svg";
+
+import { readApi, BTCUSD, ETHUSD, solidity, ethersJS } from "./common";
+import { ParameterType } from "../../components/EvmApi";
+
+export const getPriceNoOlderThan = readApi<"id" | "age">({
+  name: "getPriceNoOlderThan",
+  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
+written as a hexadecimal string; see the [price feed
+ids](https://pyth.network/developers/price-feed-ids) page to look up the id for
+a given symbol.  The returned price and confidence are decimal numbers written
+in the form \`a * 10^e\`, where \`e\` is an exponent included in the result.
+For example, a price of 1234 with an exponent of -2 represents the number 12.34.
+The result also includes a \`publishTime\` which is the unix timestamp for the
+price update.
+
+The caller provides an \`age\` argument that specifies how old the price can be.
+The call reverts with a \`StalePriceError\` if the on-chain price is from more
+than \`age\` seconds in the past (with respect to the current on-chain
+timestamp).  Call [updatePriceFeeds](updatePriceFeeds) to pull a fresh price
+on-chain and solve this problem.
+
+This function reverts with a \`PriceFeedNotFound\` error if the requested feed
+id has never received a price update.  This error could either mean that the
+provided price feed id is incorrect, or (more typically) that this is the first
+attempted use of that feed on-chain. In the second case, calling
+[updatePriceFeeds](updatePriceFeeds) will solve this problem.
+  `,
+  parameters: [
+    {
+      name: "id",
+      type: ParameterType.Hex,
+      description: "The ID of the price feed you want to read",
+    },
+    {
+      name: "age",
+      type: ParameterType.Int,
+      description: "Maximum age of the on-chain price in seconds.",
+    },
+  ],
+  examples: [
+    { name: "BTC/USD", icon: Btc, parameters: { id: BTCUSD, age: "60" } },
+    { name: "ETH/USD", icon: Eth, parameters: { id: ETHUSD, age: "60" } },
+  ],
+  code: [
+    solidity(
+      ({ id, age }) => `
+bytes32 priceId = ${id ?? "/* <id> */"};
+uint256 age = ${age ?? "/* <age> */"};
+PythStructs.Price memory currentBasePrice = pyth.getPriceNoOlderThan(priceId, age);
+    `,
+    ),
+    ethersJS(
+      ({ id, age }) => `
+const priceId = ${id ? `'${id}'` : "/* <id> */"};
+const age = ${age ? `'${age}'` : "/* <age> */"};
+const [price, conf, expo, timestamp] = await contract.getPriceNoOlderThan(priceId, age);
+    `,
+    ),
+  ],
+});

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

@@ -0,0 +1,54 @@
+import Btc from "cryptocurrency-icons/svg/color/btc.svg";
+import Eth from "cryptocurrency-icons/svg/color/eth.svg";
+
+import { readApi, BTCUSD, ETHUSD, solidity, ethersJS } from "./common";
+import { ParameterType } from "../../components/EvmApi";
+
+export const getPriceUnsafe = readApi<"id">({
+  name: "getPriceUnsafe",
+  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
+[price feed ids](https://pyth.network/developers/price-feed-ids) page to look up
+the id for a given symbol. The returned price and confidence are decimal numbers
+written in the form \`a * 10^e\`, where \`e\` is an exponent included in the
+result. For example, a price of 1234 with an exponent of -2 represents the
+number 12.34. The result also includes a \`publishTime\` which is the unix
+timestamp for the price update.
+
+**This function may return a price from arbitrarily far in the past.** It is the
+caller's responsibility to check the returned \`publishTime\` to ensure that the
+update is recent enough for their use case.
+
+This function reverts with a \`PriceFeedNotFound\` error if the requested feed
+id has never received a price update. This error could either mean that the
+provided price feed id is incorrect, or (more typically) that this is the first
+attempted use of that feed on-chain. In the second case, calling
+[updatePriceFeeds](updatePriceFeeds) will solve this problem.
+  `,
+  parameters: [
+    {
+      name: "id",
+      type: ParameterType.Hex,
+      description: "The ID of the price feed you want to read",
+    },
+  ],
+  examples: [
+    { name: "BTC/USD", icon: Btc, parameters: { id: BTCUSD } },
+    { name: "ETH/USD", icon: Eth, parameters: { id: ETHUSD } },
+  ],
+  code: [
+    solidity(
+      ({ id }) => `
+bytes32 priceId = ${id ?? "/* <id> */"};
+PythStructs.Price memory currentBasePrice = pyth.getPriceUnsafe(priceId);
+    `,
+    ),
+    ethersJS(
+      ({ id }) => `
+const priceId = ${id ? `'${id}'` : "/* <id> */"};
+const [price, conf, expo, timestamp] = await contract.getPriceUnsafe(priceId);
+    `,
+    ),
+  ],
+});

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

@@ -0,0 +1,59 @@
+import Btc from "cryptocurrency-icons/svg/color/btc.svg";
+import Eth from "cryptocurrency-icons/svg/color/eth.svg";
+
+import { readApi, BTCUSD, ETHUSD, solidity, ethersJS } from "./common";
+import { ParameterType } from "../../components/EvmApi";
+
+export const getPrice = readApi<"id">({
+  name: "getPrice",
+  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
+[price feed ids](https://pyth.network/developers/price-feed-ids) page to look up
+the id for a given symbol. The returned price and confidence are decimal numbers
+written in the form \`a * 10^e\`, where \`e\` is an exponent included in the
+result. For example, a price of 1234 with an exponent of -2 represents the
+number 12.34. The result also includes a \`publishTime\` which is the unix
+timestamp for the price update.
+
+This function reverts with a \`StalePrice\` error if the on-chain price has not
+been updated within the last [getValidTimePeriod()](getValidTimePeriod)
+seconds. The default valid time period is set to a reasonable default on each
+chain and is typically around 1 minute. Call
+[updatePriceFeeds](updatePriceFeeds) to pull a fresh price on-chain and solve
+this problem. If you would like to configure the valid time period, see
+[getPriceNoOlderThan](getPriceNoOlderThan). If you want the latest price
+regardless of when it was updated, see [getPriceUnsafe](getPriceUnsafe).
+
+This function reverts with a \`PriceFeedNotFound\` error if the requested feed
+id has never received a price update. This error could either mean that the
+provided price feed id is incorrect, or (more typically) that this is the first
+attempted use of that feed on-chain. In the second case, calling
+[updatePriceFeeds](updatePriceFeeds) will solve this problem.
+  `,
+  parameters: [
+    {
+      name: "id",
+      type: ParameterType.Hex,
+      description: "The ID of the price feed you want to read",
+    },
+  ],
+  examples: [
+    { name: "BTC/USD", icon: Btc, parameters: { id: BTCUSD } },
+    { name: "ETH/USD", icon: Eth, parameters: { id: ETHUSD } },
+  ],
+  code: [
+    solidity(
+      ({ id }) => `
+bytes32 priceId = ${id ?? "/* <id> */"};
+PythStructs.Price memory currentBasePrice = pyth.getPrice(priceId);
+    `,
+    ),
+    ethersJS(
+      ({ id }) => `
+const priceId = ${id ? `'${id}'` : "/* <id> */"};
+const [price, conf, expo, timestamp] = await contract.getPrice(priceId);
+    `,
+    ),
+  ],
+});

+ 67 - 0
apps/api-reference/src/apis/evm/get-update-fee.tsx

@@ -0,0 +1,67 @@
+import Btc from "cryptocurrency-icons/svg/color/btc.svg";
+import Eth from "cryptocurrency-icons/svg/color/eth.svg";
+
+import {
+  readApi,
+  BTCUSD,
+  ETHUSD,
+  getLatestPriceFeed,
+  solidity,
+  ethersJS,
+} from "./common";
+import { ParameterType } from "../../components/EvmApi";
+import { InlineLink } from "../../components/InlineLink";
+
+export const getUpdateFee = readApi<"updateData">({
+  name: "getUpdateFee",
+  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
+value when calling [updatePriceFeeds](update-price-feeds).  The \`updateData\`
+can be retrieved from the [Hermes API](https://hermes.pyth.network/docs).
+  `,
+  parameters: [
+    {
+      name: "updateData",
+      type: ParameterType.HexArray,
+      description: (
+        <>
+          The price updates that you would like to submit to{" "}
+          <InlineLink href="updatePriceFeeds">updatePriceFeeds</InlineLink>
+        </>
+      ),
+    },
+  ],
+  examples: [
+    {
+      name: "Latest BTC/USD update data",
+      icon: Btc,
+      parameters: () => getParams(BTCUSD),
+    },
+    {
+      name: "Latest ETH/USD update data",
+      icon: Eth,
+      parameters: () => getParams(ETHUSD),
+    },
+  ],
+  code: [
+    solidity(
+      ({ updateData }) => `
+bytes[] memory updateData = new bytes[](1);
+updateData[0] = ${updateData ? `hex"${updateData}` : "/* <updateData> */"};
+uint feeAmount = pyth.getUpdateFee(updateData);
+    `,
+    ),
+    ethersJS(
+      ({ updateData }) => `
+const updateData = ${updateData ? `['${updateData}']` : "/* <updateData> */"};
+const [feeAmount] = await contract.getUpdateFee(updateData);
+    `,
+    ),
+  ],
+});
+
+const getParams = async (feedId: string) => {
+  const feed = await getLatestPriceFeed(feedId);
+  return { updateData: feed.vaa };
+};

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

@@ -0,0 +1,18 @@
+import { readApi, solidity, ethersJS } from "./common";
+
+export const getValidTimePeriod = readApi<never>({
+  name: "getValidTimePeriod",
+  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
+[getEmaPrice](getEmaPrice); these functions revert if the current on-chain price
+is older than this period.  The valid time period is configured to be a sane
+default for each blockchain.
+  `,
+  parameters: [],
+  examples: [],
+  code: [
+    solidity("uint validTimePeriod = pyth.getValidTimePeriod();"),
+    ethersJS("const [validTimePeriod] = await contract.getValidTimePeriod();"),
+  ],
+});

+ 12 - 0
apps/api-reference/src/apis/evm/index.ts

@@ -0,0 +1,12 @@
+export { getEmaPrice } from "./get-ema-price";
+export { getEmaPriceNoOlderThan } from "./get-ema-price-no-older-than";
+export { getEmaPriceUnsafe } from "./get-ema-price-unsafe";
+export { getPrice } from "./get-price";
+export { getPriceNoOlderThan } from "./get-price-no-older-than";
+export { getPriceUnsafe } from "./get-price-unsafe";
+export { getUpdateFee } from "./get-update-fee";
+export { getValidTimePeriod } from "./get-valid-time-period";
+export { parsePriceFeedUpdates } from "./parse-price-feed-updates";
+export { parsePriceFeedUpdatesUnique } from "./parse-price-feed-updates-unique";
+export { updatePriceFeeds } from "./update-price-feeds";
+export { updatePriceFeedsIfNecessary } from "./update-price-feeds-if-necessary";

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

@@ -0,0 +1,129 @@
+import Btc from "cryptocurrency-icons/svg/color/btc.svg";
+import Eth from "cryptocurrency-icons/svg/color/eth.svg";
+
+import {
+  BTCUSD,
+  ETHUSD,
+  getLatestPriceFeed,
+  solidity,
+  ethersJS,
+  writeApi,
+} from "./common";
+import { ParameterType } from "../../components/EvmApi";
+
+export const parsePriceFeedUpdatesUnique = writeApi<
+  "updateData" | "priceId" | "minPublishTime" | "maxPublishTime" | "fee"
+>({
+  name: "parsePriceFeedUpdatesUnique",
+  description: `
+Parse \`updateData\` and return the price feeds for the given \`priceIds\`
+within, if they are all **the first updates** published between
+\`minPublishTime\` and \`maxPublishTime\`.  That is to say, if \`prevPublishTime
+< minPublishTime <= publishTime <= maxPublishTime\` where \`prevPublishTime\` is
+the publish time of the previous update for the given price feed.  These updates
+are unique per \`priceId\` and \`minPublishTime\`.  This will guarantee no
+updates exist for the given \`priceIds\` earlier than the returned updates and
+still in the given time range.  If you do not need the uniqueness guarantee,
+consider using [parsePriceFeedUpdates](parse-price-feed-updates) instead.  Use
+this function if you want to use a Pyth price for a fixed time and not the most
+recent price; otherwise, consider using [updatePriceFeeds](update-price-feeds)
+followed by [getPrice](get-price) or one of its variants.  Unlike
+\`updatePriceFeeds\`, calling this function will not update the on-chain price.
+
+This method requires the caller to pay a fee in wei; the required fee can be
+computed by calling [getUpdateFee](get-update-fee) with \`updateData\`.
+
+Reverts if the transferred fee is not sufficient, or \`updateData\` is invalid,
+or \`updateData\` does not contain an update for any of the given \`priceIds\`
+within the given time range.
+  `,
+  parameters: [
+    {
+      name: "updateData",
+      type: ParameterType.HexArray,
+      description: "The price update data to parse.",
+    },
+    {
+      name: "priceId",
+      type: ParameterType.HexArray,
+      description: "The price ids whose feeds will be returned.",
+    },
+    {
+      name: "minPublishTime",
+      type: ParameterType.Int,
+      description: "The minimum timestamp for each returned feed.",
+    },
+    {
+      name: "maxPublishTime",
+      type: ParameterType.Int,
+      description: "The maximum timestamp for each returned feed.",
+    },
+    {
+      name: "fee",
+      type: ParameterType.Int,
+      description:
+        "The update fee in wei. This fee is sent as the value of the transaction.",
+    },
+  ],
+  valueParam: "fee",
+  examples: [
+    {
+      name: "Latest BTC/USD update data",
+      icon: Btc,
+      parameters: (ctx) => getParams(BTCUSD, ctx),
+    },
+    {
+      name: "Latest ETH/USD update data",
+      icon: Eth,
+      parameters: (ctx) => getParams(ETHUSD, ctx),
+    },
+  ],
+  code: [
+    solidity(
+      ({ updateData, priceId, minPublishTime, maxPublishTime, fee }) => `
+bytes[] memory updateData = new bytes[](1);
+updateData[0] = ${updateData ? `hex"${updateData}` : "/* <updateData> */"};
+
+bytes32[] memory priceIds = new bytes32[](1);
+priceIds[0] = ${priceId ?? "/* <priceId> */"};
+
+uint64 minPublishTime = ${minPublishTime ?? "/* <minPublishTime> */"};
+uint64 maxPublishTime = ${maxPublishTime ?? "/* <maxPublishTime> */"};
+
+uint fee = ${fee ?? "/* <fee> */"};
+pyth.parsePriceFeedUpdatesUnique{value: fee}(updateData, priceIds, minPublishTime, maxPublishTime);
+    `,
+    ),
+    ethersJS(
+      ({ updateData, priceId, minPublishTime, maxPublishTime, fee }) => `
+const updateData = ${updateData ? `['${updateData}']` : "/* <updateData> */"};
+const priceIds = ${priceId ? `['${priceId}']` : "/* <priceId> */"};
+const minPublishTime = ethers.toBigInt(${minPublishTime ?? "/* <minPublishTime> */"});
+const maxPublishTime = ethers.toBigInt(${maxPublishTime ?? "/* <maxPublishTime> */"});
+const fee = ethers.toBigInt(${fee ?? "/* <fee> */"});
+const tx = await contract.parsePriceFeedUpdatesUnique(updateData, priceIds, minPublishTime, maxPublishTime, {value: fee});
+const receipt = await tx.wait();
+    `,
+    ),
+  ],
+});
+
+const getParams = async (
+  priceId: string,
+  ctx: {
+    readContract: (name: string, args: unknown[]) => Promise<unknown>;
+  },
+) => {
+  const feed = await getLatestPriceFeed(priceId);
+  const fee = await ctx.readContract("getUpdateFee", [[feed.vaa]]);
+  if (typeof fee !== "bigint") {
+    throw new TypeError("Invalid fee");
+  }
+  return {
+    updateData: feed.vaa,
+    priceId,
+    minPublishTime: (feed.price.publish_time - 5).toString(),
+    maxPublishTime: (feed.price.publish_time + 5).toString(),
+    fee: fee.toString(),
+  };
+};

+ 127 - 0
apps/api-reference/src/apis/evm/parse-price-feed-updates.tsx

@@ -0,0 +1,127 @@
+import Btc from "cryptocurrency-icons/svg/color/btc.svg";
+import Eth from "cryptocurrency-icons/svg/color/eth.svg";
+
+import {
+  BTCUSD,
+  ETHUSD,
+  getLatestPriceFeed,
+  solidity,
+  ethersJS,
+  writeApi,
+} from "./common";
+import { ParameterType } from "../../components/EvmApi";
+
+export const parsePriceFeedUpdates = writeApi<
+  "updateData" | "priceId" | "minPublishTime" | "maxPublishTime" | "fee"
+>({
+  name: "parsePriceFeedUpdates",
+  description: `
+Parse \`updateData\` and return the price feeds for the given \`priceIds\`
+within, if they are all published between \`minPublishTime\` and
+\`maxPublishTime\` (\`minPublishTime <= publishTime <= maxPublishTime\`).  Use
+this function if you want to use a Pyth price for a fixed time and not the most
+recent price; otherwise, consider using [updatePriceFeeds](update-price-feeds)
+followed by [getPrice](get-price) or one of its variants.  Unlike
+\`updatePriceFeeds\`, calling this function will not update the on-chain price.
+
+If you need to make sure the price update is the earliest update after the
+\`minPublishTime\` consider using
+[parsePriceFeedUpdatesUnique](parse-price-feed-updates-unique).
+
+This method requires the caller to pay a fee in wei; the required fee can be
+computed by calling [getUpdateFee](get-update-fee) with \`updateData\`.
+
+Reverts if the transferred fee is not sufficient, or \`updateData\` is invalid,
+or \`updateData\` does not contain an update for any of the given \`priceIds\`
+within the given time range.
+  `,
+  parameters: [
+    {
+      name: "updateData",
+      type: ParameterType.HexArray,
+      description: "The price update data to parse.",
+    },
+    {
+      name: "priceId",
+      type: ParameterType.HexArray,
+      description: "The price ids whose feeds will be returned.",
+    },
+    {
+      name: "minPublishTime",
+      type: ParameterType.Int,
+      description: "The minimum timestamp for each returned feed.",
+    },
+    {
+      name: "maxPublishTime",
+      type: ParameterType.Int,
+      description: "The maximum timestamp for each returned feed.",
+    },
+    {
+      name: "fee",
+      type: ParameterType.Int,
+      description:
+        "The update fee in wei. This fee is sent as the value of the transaction.",
+    },
+  ],
+  valueParam: "fee",
+  examples: [
+    {
+      name: "Latest BTC/USD update data",
+      icon: Btc,
+      parameters: (ctx) => getParams(BTCUSD, ctx),
+    },
+    {
+      name: "Latest ETH/USD update data",
+      icon: Eth,
+      parameters: (ctx) => getParams(ETHUSD, ctx),
+    },
+  ],
+  code: [
+    solidity(
+      ({ updateData, priceId, minPublishTime, maxPublishTime, fee }) => `
+bytes[] memory updateData = new bytes[](1);
+updateData[0] = ${updateData ? `hex"${updateData}` : "/* <updateData> */"};
+
+bytes32[] memory priceIds = new bytes32[](1);
+priceIds[0] = ${priceId ?? "/* <priceId> */"};
+
+uint64 minPublishTime = ${minPublishTime ?? "/* <minPublishTime> */"};
+uint64 maxPublishTime = ${maxPublishTime ?? "/* <maxPublishTime> */"};
+
+uint fee = ${fee ?? "/* <fee> */"};
+pyth.parsePriceFeedUpdates{value: fee}(updateData, priceIds, minPublishTime, maxPublishTime);
+    `,
+    ),
+    ethersJS(
+      ({ updateData, priceId, minPublishTime, maxPublishTime, fee }) => `
+const updateData = ${updateData ? `['${updateData}']` : "/* <updateData> */"};
+const priceIds = ${priceId ? `['${priceId}']` : "/* <priceId> */"};
+const minPublishTime = ethers.toBigInt(${minPublishTime ?? "/* <minPublishTime> */"});
+const maxPublishTime = ethers.toBigInt(${maxPublishTime ?? "/* <maxPublishTime> */"});
+const fee = ethers.toBigInt(${fee ?? "/* <fee> */"});
+const tx = await contract.parsePriceFeedUpdates(updateData, priceIds, minPublishTime, maxPublishTime, {value: fee});
+const receipt = await tx.wait();
+    `,
+    ),
+  ],
+});
+
+const getParams = async (
+  priceId: string,
+  ctx: {
+    readContract: (name: string, args: unknown[]) => Promise<unknown>;
+  },
+) => {
+  const feed = await getLatestPriceFeed(priceId);
+  const fee = await ctx.readContract("getUpdateFee", [[feed.vaa]]);
+  if (typeof fee !== "bigint") {
+    throw new TypeError("Invalid fee");
+  }
+  return {
+    updateData: feed.vaa,
+    priceId,
+    minPublishTime: (feed.price.publish_time - 5).toString(),
+    maxPublishTime: (feed.price.publish_time + 5).toString(),
+    fee: fee.toString(),
+  };
+};

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

@@ -0,0 +1,121 @@
+import Btc from "cryptocurrency-icons/svg/color/btc.svg";
+import Eth from "cryptocurrency-icons/svg/color/eth.svg";
+
+import {
+  BTCUSD,
+  ETHUSD,
+  getLatestPriceFeed,
+  solidity,
+  ethersJS,
+  writeApi,
+} from "./common";
+import { ParameterType } from "../../components/EvmApi";
+
+export const updatePriceFeedsIfNecessary = writeApi<
+  "updateData" | "priceId" | "publishTime" | "fee"
+>({
+  name: "updatePriceFeedsIfNecessary",
+  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
+arrays, \`priceIds\` and \`publishTimes\`.  This function applies the update if
+there exists an index \`i\` such that \`priceIds[i]\`'s last \`publishTime\` is
+before than \`publishTimes[i]\`.  Callers should typically pass
+\`publishTimes[i]\` to be equal to the publishTime of the corresponding price id
+in \`updateData\`.  If this condition is not satisfied, the call will revert
+with a \`NoFreshUpdate\` error.
+
+This method is a variant of [updatePriceFeeds](update-price-feeds) that reduces
+gas usage when multiple callers are sending the same price updates.
+
+This function requires the caller to pay a fee to perform the update.  The
+required fee for a given set of updates can be computed by passing them to
+[getUpdateFee](get-update-fee).
+
+Reverts if the required fee is not paid, or the \`updateData\` is incorrectly
+signed or formatted.
+  `,
+  parameters: [
+    {
+      name: "updateData",
+      type: ParameterType.HexArray,
+      description: "The price update data for the contract to verify.",
+    },
+    {
+      name: "priceId",
+      type: ParameterType.HexArray,
+      description: "The price ids to update.",
+    },
+    {
+      name: "publishTime",
+      type: ParameterType.IntArray,
+      description:
+        "The timestamp for each price id that determines whether to apply the update.",
+    },
+    {
+      name: "fee",
+      type: ParameterType.Int,
+      description:
+        "The update fee in wei. This fee is sent as the value of the transaction.",
+    },
+  ],
+  valueParam: "fee",
+  examples: [
+    {
+      name: "Latest BTC/USD update data",
+      icon: Btc,
+      parameters: (ctx) => getParams(BTCUSD, ctx),
+    },
+    {
+      name: "Latest ETH/USD update data",
+      icon: Eth,
+      parameters: (ctx) => getParams(ETHUSD, ctx),
+    },
+  ],
+  code: [
+    solidity(
+      ({ updateData, priceId, publishTime, fee }) => `
+bytes[] memory updateData = new bytes[](1);
+updateData[0] = ${updateData ? `hex"${updateData}` : "/* <updateData> */"};
+
+bytes32[] memory priceIds = new bytes32[](1);
+priceIds[0] = ${priceId ?? "/* <priceId> */"};
+
+uint64[] memory publishTimes = new uint64[](1);
+publishTimes[0] = ${publishTime ?? "/* <publishTime> */"};
+
+uint fee = ${fee ?? "/* <fee> */"};
+pyth.updatePriceFeedsIfNecessary{value: fee}(updateData, priceIds, publishTimes);
+    `,
+    ),
+    ethersJS(
+      ({ updateData, priceId, publishTime, fee }) => `
+const updateData = ${updateData ? `['${updateData}']` : "/* <updateData> */"};
+const priceIds = ${priceId ? `['${priceId}']` : "/* <priceId> */"};
+const publishTimes = ${publishTime ? `[ethers.toBigInt(${publishTime})]` : "/* <publishTime> */"};
+const fee = ethers.toBigInt(${fee ?? "/* <fee> */"});
+const tx = await contract.updatePriceFeedsIfNecessary(updateData, priceIds, publishTimes, {value: fee});
+const receipt = await tx.wait();
+    `,
+    ),
+  ],
+});
+
+const getParams = async (
+  priceId: string,
+  ctx: {
+    readContract: (name: string, args: unknown[]) => Promise<unknown>;
+  },
+) => {
+  const feed = await getLatestPriceFeed(priceId);
+  const fee = await ctx.readContract("getUpdateFee", [[feed.vaa]]);
+  if (typeof fee !== "bigint") {
+    throw new TypeError("Invalid fee");
+  }
+  return {
+    updateData: feed.vaa,
+    priceId,
+    publishTime: feed.price.publish_time.toString(),
+    fee: fee.toString(),
+  };
+};

+ 93 - 0
apps/api-reference/src/apis/evm/update-price-feeds.tsx

@@ -0,0 +1,93 @@
+import Btc from "cryptocurrency-icons/svg/color/btc.svg";
+import Eth from "cryptocurrency-icons/svg/color/eth.svg";
+
+import {
+  BTCUSD,
+  ETHUSD,
+  getLatestPriceFeed,
+  solidity,
+  ethersJS,
+  writeApi,
+} from "./common";
+import { ParameterType } from "../../components/EvmApi";
+
+export const updatePriceFeeds = writeApi<"updateData" | "fee">({
+  name: "updatePriceFeeds",
+  description: `
+Update the on-chain price feeds using the provided \`updateData\`, which
+contains serialized and signed price update data from Pyth Network.  You can
+retrieve the latest price \`updateData\` for a given set of price feeds from the
+[Hermes API](https://hermes.pyth.network/docs).  This function updates the
+on-chain price if the provided update is more recent than the current on-chain
+price.  Otherwise, the provided update will be ignored.  The function call will
+succeed even if the update is ignored.
+
+This function requires the caller to pay a fee to perform the update.  The
+required fee for a given set of updates can be computed by passing them to
+[getUpdateFee](get-update-fee).
+
+Reverts if the required fee is not paid, or the \`updateData\` is incorrectly
+signed or formatted.
+  `,
+  parameters: [
+    {
+      name: "updateData",
+      type: ParameterType.HexArray,
+      description: "The price update data for the contract to verify.",
+    },
+    {
+      name: "fee",
+      type: ParameterType.Int,
+      description:
+        "The update fee in wei. This fee is sent as the value of the transaction.",
+    },
+  ],
+  valueParam: "fee",
+  examples: [
+    {
+      name: "Latest BTC/USD update data",
+      icon: Btc,
+      parameters: (ctx) => getParams(BTCUSD, ctx),
+    },
+    {
+      name: "Latest ETH/USD update data",
+      icon: Eth,
+      parameters: (ctx) => getParams(ETHUSD, ctx),
+    },
+  ],
+  code: [
+    solidity(
+      ({ updateData, fee }) => `
+bytes[] memory updateData = new bytes[](1);
+updateData[0] = ${updateData ? `hex"${updateData}` : "/* <updateData> */"};
+uint fee = ${fee ?? "/* <fee> */"};
+pyth.updatePriceFeeds{value: fee}(updateData);
+    `,
+    ),
+    ethersJS(
+      ({ updateData, fee }) => `
+const updateData = ${updateData ? `['${updateData}']` : "/* <updateData> */"};
+const fee = ethers.toBigInt(${fee ?? "/* <fee> */"});
+const tx = await contract.updatePriceFeeds(updateData, {value: fee});
+const receipt = await tx.wait();
+    `,
+    ),
+  ],
+});
+
+const getParams = async (
+  feedId: string,
+  ctx: {
+    readContract: (name: string, args: unknown[]) => Promise<unknown>;
+  },
+) => {
+  const feed = await getLatestPriceFeed(feedId);
+  const fee = await ctx.readContract("getUpdateFee", [[feed.vaa]]);
+  if (typeof fee !== "bigint") {
+    throw new TypeError("Invalid fee");
+  }
+  return {
+    updateData: feed.vaa,
+    fee: fee.toString(),
+  };
+};

+ 1 - 3
apps/api-reference/src/apis/index.tsx

@@ -1,3 +1 @@
-import { evm } from "./evm";
-
-export const apis = { evm };
+export * as evm from "./evm";

+ 1 - 53
apps/api-reference/src/app/layout.tsx

@@ -1,56 +1,4 @@
-import type { Metadata, Viewport } from "next";
-
 import "../tailwind.css";
 
 export { Root as default } from "../components/Root";
-
-export const metadata: Metadata = {
-  metadataBase: new URL("https://api-reference.pyth.network"),
-  title: {
-    default: "Pyth Network API Reference",
-    template: "%s | Pyth Network API Reference",
-  },
-  applicationName: "Pyth Network API Reference",
-  description:
-    "API reference, interactive explorer, and documentation for Pyth network products.",
-  referrer: "strict-origin-when-cross-origin",
-  openGraph: {
-    type: "website",
-  },
-  twitter: {
-    creator: "@PythNetwork",
-    card: "summary_large_image",
-  },
-  icons: {
-    icon: [
-      {
-        media: "(prefers-color-scheme: light)",
-        type: "image/x-icon",
-        url: "/favicon.ico",
-      },
-      {
-        media: "(prefers-color-scheme: dark)",
-        type: "image/x-icon",
-        url: "/favicon-light.ico",
-      },
-      {
-        type: "image/png",
-        sizes: "32x32",
-        url: "/favicon-32x32.png",
-      },
-      {
-        type: "image/png",
-        sizes: "16x16",
-        url: "/favicon-16x16.png",
-      },
-    ],
-    apple: {
-      url: "/apple-touch-icon.png",
-      sizes: "180x180",
-    },
-  },
-};
-
-export const viewport: Viewport = {
-  themeColor: "#242235",
-};
+export { metadata, viewport } from "../metadata";

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

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

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

@@ -2,7 +2,7 @@ import { evaluate } from "@mdx-js/mdx";
 import { notFound } from "next/navigation";
 import * as runtime from "react/jsx-runtime";
 
-import { apis } from "../../../../apis";
+import * as apis from "../../../../apis";
 import { useMDXComponents } from "../../../../mdx-components";
 
 type Props = {
@@ -14,6 +14,7 @@ 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 api =
     chain && isKeyOf(params.method, chain) ? chain[params.method] : undefined;

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

@@ -0,0 +1,22 @@
+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;

+ 38 - 7
apps/api-reference/src/components/Code/index.tsx

@@ -3,6 +3,7 @@ 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 type { OffsetOrPosition } from "shiki";
 
 import type { Highlighter, SupportedLanguage } from "./shiki";
 import style from "./style.module.css";
@@ -14,9 +15,10 @@ export type { SupportedLanguage } from "./shiki";
 type CodeProps = {
   language: SupportedLanguage;
   children: string;
+  dimRange?: readonly [OffsetOrPosition, OffsetOrPosition] | undefined;
 };
 
-export const Code = ({ language, children }: CodeProps) => {
+export const Code = ({ language, children, dimRange }: CodeProps) => {
   const chompedCode = useMemo(() => chomp(children), [children]);
 
   return (
@@ -24,7 +26,11 @@ export const Code = ({ language, children }: CodeProps) => {
       <CopyButton className="absolute right-4 top-4 opacity-0 transition group-hover:opacity-100">
         {chompedCode}
       </CopyButton>
-      <HighlightedCode language={language} className={style.code}>
+      <HighlightedCode
+        language={language}
+        className={style.code}
+        dimRange={dimRange}
+      >
         {chompedCode}
       </HighlightedCode>
     </div>
@@ -108,14 +114,16 @@ const CopyButton = ({ children, className, ...props }: CopyButtonProps) => {
 type HighlightedCodeProps = Omit<HTMLAttributes<HTMLElement>, "children"> & {
   language: SupportedLanguage;
   children: string;
+  dimRange?: readonly [OffsetOrPosition, OffsetOrPosition] | undefined;
 };
 
 const HighlightedCode = ({
   language,
   children,
+  dimRange,
   ...props
 }: HighlightedCodeProps) => {
-  const highlightedCode = useHighlightedCode(language, children);
+  const highlightedCode = useHighlightedCode(language, children, dimRange);
 
   return highlightedCode ? (
     <div dangerouslySetInnerHTML={{ __html: highlightedCode }} {...props} />
@@ -134,15 +142,36 @@ const HighlightedCode = ({
   );
 };
 
-const useHighlightedCode = (language: SupportedLanguage, code: string) => {
+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));
+      setHighlightedCode(
+        highlighter.current.highlight(language, code, { decorations }),
+      );
       return;
     } else {
       const { cancel, load } = createShikiLoader();
@@ -150,7 +179,9 @@ const useHighlightedCode = (language: SupportedLanguage, code: string) => {
         .then((newHighlighter) => {
           if (newHighlighter) {
             highlighter.current = newHighlighter;
-            setHighlightedCode(newHighlighter.highlight(language, code));
+            setHighlightedCode(
+              newHighlighter.highlight(language, code, { decorations }),
+            );
           }
         })
         .catch((error: unknown) => {
@@ -159,7 +190,7 @@ const useHighlightedCode = (language: SupportedLanguage, code: string) => {
         });
       return cancel;
     }
-  }, [code, language]);
+  }, [code, language, decorations]);
 
   return highlightedCode;
 };

+ 17 - 3
apps/api-reference/src/components/Code/shiki.ts

@@ -1,5 +1,6 @@
 import {
   type HighlighterCore,
+  type DecorationItem,
   getHighlighterCore as shikiGetHighlighterCore,
 } from "shiki/core";
 import javascript from "shiki/langs/javascript.mjs";
@@ -9,11 +10,19 @@ import lightPlus from "shiki/themes/light-plus.mjs";
 import loadWasm from "shiki/wasm";
 
 export type Highlighter = {
-  highlight: (lang: SupportedLanguage, code: string) => string;
+  highlight: (
+    lang: SupportedLanguage,
+    code: string,
+    options?: HighlightOptions | undefined,
+  ) => string;
 };
 
 export type SupportedLanguage = "javascript" | "solidity";
 
+export type HighlightOptions = {
+  decorations?: DecorationItem[] | undefined;
+};
+
 export const getHighlighter = async (): Promise<Highlighter> => {
   const highlighterCore = await shikiGetHighlighterCore({
     langs: [javascript, solidity],
@@ -22,8 +31,11 @@ export const getHighlighter = async (): Promise<Highlighter> => {
   });
 
   return {
-    highlight: (lang: SupportedLanguage, code: string) =>
-      highlight(highlighterCore, lang, code),
+    highlight: (
+      lang: SupportedLanguage,
+      code: string,
+      options?: HighlightOptions | undefined,
+    ) => highlight(highlighterCore, lang, code, options),
   };
 };
 
@@ -31,6 +43,7 @@ const highlight = (
   highlighter: HighlighterCore,
   lang: SupportedLanguage,
   code: string,
+  options?: HighlightOptions | undefined,
 ) =>
   highlighter.codeToHtml(code, {
     lang,
@@ -38,4 +51,5 @@ const highlight = (
       light: "light-plus",
       dark: "dark-plus",
     },
+    ...(options?.decorations && { decorations: options.decorations }),
   });

+ 4 - 7
apps/api-reference/src/components/ColorThemeSelector/index.tsx

@@ -8,8 +8,9 @@ import {
 } from "@heroicons/react/24/outline";
 import clsx from "clsx";
 import { useTheme } from "next-themes";
-import { createElement, useState, useEffect } from "react";
+import { createElement } from "react";
 
+import { useIsMounted } from "../../use-is-mounted";
 import { Select } from "../Select";
 
 const VALID_THEMES = ["system", "light", "dark"] as const;
@@ -30,7 +31,7 @@ const buttonClasses =
   "grid place-content-center w-12 h-10 px-3 rounded text-neutral-500";
 
 export const ColorThemeSelector = () => {
-  const [mounted, setMounted] = useState(false);
+  const isMounted = useIsMounted();
   const {
     theme: themeFromNextThemes,
     setTheme,
@@ -43,11 +44,7 @@ export const ColorThemeSelector = () => {
     ? resolvedThemeFromNextThemes
     : "system";
 
-  useEffect(() => {
-    setMounted(true);
-  }, []);
-
-  return mounted ? (
+  return isMounted ? (
     <Select
       value={theme}
       onChange={setTheme}

+ 301 - 0
apps/api-reference/src/components/EvmApi/index.tsx

@@ -0,0 +1,301 @@
+"use client";
+
+import {
+  Tab,
+  TabGroup,
+  TabList,
+  TabPanel,
+  TabPanels,
+  Field,
+  Label,
+} from "@headlessui/react";
+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,
+  useState,
+  useCallback,
+  useMemo,
+  type ComponentType,
+  type SVGAttributes,
+} from "react";
+import { useSwitchChain, useChainId, useConfig } from "wagmi";
+import { readContract } from "wagmi/actions";
+
+import { getContractAddress } from "./networks";
+import { type Parameter } from "./parameter";
+import { ParameterInput } from "./parameter-input";
+import { type EvmApiType, RunButton } from "./run-button";
+import { useIsMounted } from "../../use-is-mounted";
+import { type SupportedLanguage, Code } from "../Code";
+import { InlineLink } from "../InlineLink";
+import { Select } from "../Select";
+
+export { ParameterType } from "./parameter";
+export { EvmApiType } from "./run-button";
+
+const abi = [...PythAbi, ...PythErrorsAbi] as const;
+
+type Props<ParameterName extends string> =
+  | ReadApi<ParameterName>
+  | WriteApi<ParameterName>;
+
+type Common<ParameterName extends string> = {
+  name: (typeof PythAbi)[number]["name"];
+  children: ReactNode;
+  parameters: Parameter<ParameterName>[];
+  examples: Example<ParameterName>[];
+  code: CodeSample<ParameterName>[];
+};
+
+export type ReadApi<ParameterName extends string> = Common<ParameterName> & {
+  type: EvmApiType.Read;
+};
+
+export type WriteApi<ParameterName extends string> = Common<ParameterName> & {
+  type: EvmApiType.Write;
+  valueParam: ParameterName;
+};
+
+type Example<ParameterName extends string> = {
+  name: string;
+  icon?: ComponentType<SVGAttributes<SVGSVGElement>>;
+  parameters: ValueOrFunctionOrAsyncFunction<Record<ParameterName, string>>;
+};
+
+type ValueOrFunctionOrAsyncFunction<T> =
+  | T
+  | ((ctx: ContractContext) => T)
+  | ((ctx: ContractContext) => Promise<T>);
+
+type ContractContext = {
+  readContract: (functionName: string, args: unknown[]) => Promise<unknown>;
+};
+
+export enum Language {
+  Solidity,
+  EthersJSV6,
+}
+
+type CodeSample<ParameterName extends string> = {
+  language: Language;
+  dimRange: ComponentProps<typeof Code>["dimRange"];
+  code: (
+    network: NetworkInfo,
+    params: Partial<Record<ParameterName, string>>,
+  ) => string;
+};
+
+export type NetworkInfo = {
+  name: string;
+  rpcUrl: string;
+  contractAddress: string;
+};
+
+export const EvmApi = <ParameterName extends string>({
+  name,
+  children,
+  parameters,
+  code,
+  examples,
+  ...props
+}: Props<ParameterName>) => {
+  const [paramValues, setParamValues] = useState<
+    Partial<Record<ParameterName, string>>
+  >({});
+  const chainId = useChainId();
+  const { chains, switchChain } = useSwitchChain();
+  const isMounted = useIsMounted();
+  const currentChain = useMemo(() => {
+    const chain = isMounted
+      ? chains.find((chain) => chain.id === chainId)
+      : chains[0];
+    if (chain === undefined) {
+      throw new Error(`Invalid current chain id: ${chainId.toString()}`);
+    }
+    return chain;
+  }, [chainId, chains, isMounted]);
+
+  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>
+      <section>
+        <h2 className="mb-4 border-b border-neutral-200 text-2xl/loose font-medium dark:border-neutral-800">
+          Description
+        </h2>
+        {children}
+      </section>
+      <section className="flex flex-col">
+        <h2 className="mb-4 border-b border-neutral-200 text-2xl/loose font-medium dark:border-neutral-800">
+          Arguments
+        </h2>
+        <div className="mb-8">
+          {parameters.length > 0 ? (
+            <ul className="flex flex-col gap-4">
+              {parameters.map((parameter) => (
+                <li key={parameter.name} className="contents">
+                  <ParameterInput
+                    spec={parameter}
+                    value={paramValues[parameter.name]}
+                    setParamValues={setParamValues}
+                  />
+                </li>
+              ))}
+            </ul>
+          ) : (
+            <div className="rounded-lg bg-neutral-200 p-8 text-center text-sm dark:bg-neutral-800">
+              This API takes no arguments
+            </div>
+          )}
+        </div>
+        <div className="grow" />
+        {examples.length > 0 && (
+          <div className="mb-8">
+            <h3 className="text-sm font-semibold">Examples</h3>
+            <ul className="ml-2 text-sm">
+              {examples.map((example) => (
+                <li key={example.name}>
+                  <Example example={example} setParamValues={setParamValues} />
+                </li>
+              ))}
+            </ul>
+          </div>
+        )}
+        <Field className="mb-4 flex w-full flex-row items-center gap-2">
+          <Label className="text-sm font-bold">Network</Label>
+          <Select
+            value={currentChain}
+            onChange={({ id }) => {
+              switchChain({ chainId: id });
+            }}
+            renderButtonContents={({ id, name }) => (
+              <div className="flex h-8 flex-row items-center gap-2">
+                {isMounted && (
+                  <>
+                    <ChainIcon id={id} />
+                    <span>{name}</span>
+                  </>
+                )}
+              </div>
+            )}
+            renderOption={({ id, name }) => (
+              <div className="flex flex-row items-center gap-2">
+                <ChainIcon id={id} />
+                <span>{name}</span>
+              </div>
+            )}
+            options={chains}
+            buttonClassName="grow"
+          />
+        </Field>
+        <RunButton
+          functionName={name}
+          parameters={parameters}
+          paramValues={paramValues}
+          {...props}
+        />
+      </section>
+      <TabGroup className="col-span-2 mt-24">
+        <TabList className="mb-4 flex flex-row gap-2 border-b border-neutral-200 pb-px dark:border-neutral-800">
+          {code.map(({ language }) => (
+            <Tab
+              key={LANGUAGE_TO_DISPLAY_NAME[language]}
+              className="mb-[-2px] border-b-2 border-transparent px-2 text-sm font-medium leading-loose hover:text-pythpurple-600 data-[selected]:cursor-default data-[selected]:border-pythpurple-600 data-[selected]:text-pythpurple-600 dark:hover:text-pythpurple-400 dark:data-[selected]:border-pythpurple-400 dark:data-[selected]:text-pythpurple-400"
+            >
+              {LANGUAGE_TO_DISPLAY_NAME[language]}
+            </Tab>
+          ))}
+        </TabList>
+        <TabPanels>
+          {code.map(({ code: codeContents, language, dimRange }) => (
+            <TabPanel key={LANGUAGE_TO_DISPLAY_NAME[language]}>
+              <Code
+                language={LANUGAGE_TO_SHIKI_NAME[language]}
+                dimRange={dimRange}
+              >
+                {codeContents(
+                  isMounted
+                    ? {
+                        name: currentChain.name,
+                        rpcUrl: currentChain.rpcUrls.default.http[0] ?? "",
+                        contractAddress: getContractAddress(chainId) ?? "",
+                      }
+                    : { name: "", rpcUrl: "", contractAddress: "" },
+                  paramValues,
+                )}
+              </Code>
+            </TabPanel>
+          ))}
+        </TabPanels>
+      </TabGroup>
+    </div>
+  );
+};
+
+const LANGUAGE_TO_DISPLAY_NAME = {
+  [Language.Solidity]: "Solidity",
+  [Language.EthersJSV6]: "ethers.js v6",
+};
+
+const LANUGAGE_TO_SHIKI_NAME: Record<Language, SupportedLanguage> = {
+  [Language.Solidity]: "solidity",
+  [Language.EthersJSV6]: "javascript",
+};
+
+type ExampleProps<ParameterName extends string> = {
+  example: Example<ParameterName>;
+  setParamValues: Dispatch<
+    SetStateAction<Partial<Record<ParameterName, string>>>
+  >;
+};
+
+const Example = <ParameterName extends string>({
+  example,
+  setParamValues,
+}: ExampleProps<ParameterName>) => {
+  const config = useConfig();
+
+  const updateValues = useCallback(() => {
+    if (typeof example.parameters === "function") {
+      const address = getContractAddress(config.state.chainId);
+      if (!address) {
+        throw new Error(
+          `No contract for chain id: ${config.state.chainId.toString()}`,
+        );
+      }
+      const params = example.parameters({
+        readContract: (functionName, args) =>
+          readContract(config, { abi, address, functionName, args }),
+      });
+      if (params instanceof Promise) {
+        params
+          .then((paramsResolved) => {
+            setParamValues(paramsResolved);
+          })
+          .catch(() => {
+            /* TODO add some UI when this errors */
+          });
+      } else {
+        setParamValues(params);
+      }
+    } else {
+      setParamValues(example.parameters);
+    }
+  }, [example, setParamValues, config]);
+  const Icon = example.icon;
+
+  return (
+    <InlineLink
+      as="button"
+      onClick={updateValues}
+      className="flex flex-row items-center gap-2"
+    >
+      {Icon && <Icon className="h-4" />}
+      <span>{example.name}</span>
+    </InlineLink>
+  );
+};

+ 18 - 0
apps/api-reference/src/components/EvmApi/networks.ts

@@ -0,0 +1,18 @@
+import { arbitrum, avalanche, mainnet, sepolia } from "wagmi/chains";
+
+export const getContractAddress = (networkId: number) =>
+  isSupportedNetwork(networkId)
+    ? NETWORK_TO_CONTRACT_ADDRESS[networkId]
+    : undefined;
+
+const NETWORK_TO_CONTRACT_ADDRESS = {
+  [mainnet.id]: "0x4305FB66699C3B2702D4d05CF36551390A4c69C6",
+  [avalanche.id]: "0x4305FB66699C3B2702D4d05CF36551390A4c69C6",
+  [arbitrum.id]: "0xff1a0f4744e8582DF1aE09D5611b887B6a12925C",
+  [sepolia.id]: "0xDd24F84d36BF92C65F92307595335bdFab5Bbd21",
+} as const;
+
+const isSupportedNetwork = (
+  networkId: number,
+): networkId is keyof typeof NETWORK_TO_CONTRACT_ADDRESS =>
+  networkId in NETWORK_TO_CONTRACT_ADDRESS;

+ 82 - 0
apps/api-reference/src/components/EvmApi/parameter-input.tsx

@@ -0,0 +1,82 @@
+import {
+  type ChangeEvent,
+  type Dispatch,
+  type SetStateAction,
+  useState,
+  useCallback,
+  useMemo,
+  useEffect,
+} from "react";
+
+import {
+  type Parameter,
+  PLACEHOLDERS,
+  isValid,
+  getValidationError,
+} from "./parameter";
+import { Input } from "../Input";
+
+type ParameterProps<ParameterName extends string> = {
+  spec: Parameter<ParameterName>;
+  value: string | undefined;
+  setParamValues: Dispatch<
+    SetStateAction<Partial<Record<ParameterName, string>>>
+  >;
+};
+
+export const ParameterInput = <ParameterName extends string>({
+  spec,
+  value,
+  setParamValues,
+}: ParameterProps<ParameterName>) => {
+  const { validationError, internalValue, onChange } = useParameterInput(
+    spec,
+    value,
+    setParamValues,
+  );
+
+  return (
+    <Input
+      validationError={validationError}
+      label={spec.name}
+      description={spec.description}
+      placeholder={PLACEHOLDERS[spec.type]}
+      required={true}
+      value={internalValue}
+      onChange={onChange}
+    />
+  );
+};
+
+const useParameterInput = <ParameterName extends string>(
+  spec: Parameter<ParameterName>,
+  value: string | undefined,
+  setParamValues: Dispatch<
+    SetStateAction<Partial<Record<ParameterName, string>>>
+  >,
+) => {
+  const [internalValue, setInternalValue] = useState(value ?? "");
+  const validationError = useMemo(
+    () => (internalValue ? getValidationError(spec, internalValue) : undefined),
+    [internalValue, spec],
+  );
+  const onChange = useCallback(
+    (e: ChangeEvent<HTMLInputElement>) => {
+      const value = e.target.value;
+      setInternalValue(value);
+      setParamValues((paramValues) => ({
+        ...paramValues,
+        [spec.name]: value === "" || !isValid(spec, value) ? undefined : value,
+      }));
+    },
+    [setParamValues, spec],
+  );
+
+  useEffect(() => {
+    if (value) {
+      setInternalValue(value);
+    }
+  }, [value]);
+
+  return { internalValue, validationError, onChange };
+};

+ 70 - 0
apps/api-reference/src/components/EvmApi/parameter.ts

@@ -0,0 +1,70 @@
+import type { ReactNode } from "react";
+
+export type Parameter<Name extends string> = {
+  name: Name;
+  type: ParameterType;
+  description: ReactNode;
+};
+
+export enum ParameterType {
+  HexArray,
+  Hex,
+  Int,
+  IntArray,
+}
+
+export const TRANSFORMS: {
+  [paramType in ParameterType]?: (value: string) => unknown;
+} = {
+  [ParameterType.HexArray]: (value) => [value],
+  [ParameterType.IntArray]: (value) => [value],
+};
+
+export const PLACEHOLDERS: Record<ParameterType, string> = {
+  [ParameterType.Hex]:
+    "0x1111111111111111111111111111111111111111111111111111111111111111",
+  [ParameterType.HexArray]:
+    "0x1111111111111111111111111111111111111111111111111111111111111111",
+  [ParameterType.Int]: "60",
+  [ParameterType.IntArray]: "60",
+};
+
+export const getValidationError = <Name extends string>(
+  parameter: Parameter<Name>,
+  value: string,
+): string | undefined => {
+  const messages = VALIDATIONS[parameter.type]
+    .map((validation) => validation(value))
+    .filter((message) => message !== undefined);
+  return messages.length === 0 ? undefined : messages.join(", ");
+};
+
+export const isValid = <Name extends string>(
+  parameter: Parameter<Name>,
+  value: string,
+): boolean =>
+  VALIDATIONS[parameter.type].every(
+    (validation) => validation(value) === undefined,
+  );
+
+const validateHex = (value: string) =>
+  HEX_REGEX.test(value)
+    ? undefined
+    : 'Please enter a hexadecimal string prefixed with 0x, for example "0xa19f"';
+
+const validateInt = (value: string) =>
+  Number.parseInt(value, 10).toString() === value
+    ? undefined
+    : "Please enter a valid integer";
+
+const VALIDATIONS: Record<
+  ParameterType,
+  ((value: string) => string | undefined)[]
+> = {
+  [ParameterType.Hex]: [validateHex],
+  [ParameterType.HexArray]: [validateHex],
+  [ParameterType.Int]: [validateInt],
+  [ParameterType.IntArray]: [validateInt],
+};
+
+const HEX_REGEX = new RegExp("^(0|0x[0-9A-Fa-f]*)$");

+ 118 - 0
apps/api-reference/src/components/EvmApi/results-modal.tsx

@@ -0,0 +1,118 @@
+import { ContractFunctionExecutionError } from "viem";
+
+import { Code } from "../Code";
+import { Modal } from "../Modal";
+
+type Props<ParameterName extends string> = {
+  modalContents?: ModalContents<ParameterName> | undefined;
+  isShowingResults: boolean;
+  resetStatus: () => void;
+  clearModalContents: () => void;
+  functionName: string;
+};
+
+export type ModalContents<ParameterName extends string> = {
+  networkName: string;
+} & (
+  | { error: unknown; parameters?: Partial<Record<ParameterName, string>> }
+  | { result: unknown; parameters: Partial<Record<ParameterName, string>> }
+);
+
+export const ResultsModal = <ParameterName extends string>({
+  modalContents,
+  isShowingResults,
+  resetStatus,
+  clearModalContents,
+  functionName,
+}: Props<ParameterName>) => (
+  <Modal
+    show={modalContents !== undefined && isShowingResults}
+    onClose={resetStatus}
+    afterLeave={clearModalContents}
+    title={
+      typeof modalContents === "object" && "result" in modalContents
+        ? "Results"
+        : "Error"
+    }
+    description={`${functionName} - ${modalContents?.networkName ?? ""}`}
+  >
+    {modalContents !== undefined && (
+      <>
+        {"parameters" in modalContents &&
+          Object.keys(modalContents.parameters).length > 0 && (
+            <div className="mb-10 rounded-lg bg-neutral-100 p-8 dark:bg-neutral-800/50">
+              <h2 className="mb-2 border-b border-neutral-300 font-semibold text-neutral-700 dark:border-neutral-600 dark:text-neutral-300">
+                Arguments
+              </h2>
+              <ul className="overflow-hidden text-xs">
+                {Object.entries(modalContents.parameters).map(
+                  ([name, value]) => (
+                    <li className="overflow-hidden truncate" key={name}>
+                      <span className="mr-2 font-medium text-neutral-700 dark:text-neutral-300">
+                        {name}:
+                      </span>
+                      <span className="text-neutral-600 dark:text-neutral-400">
+                        {value as string}
+                      </span>
+                    </li>
+                  ),
+                )}
+              </ul>
+            </div>
+          )}
+        {"result" in modalContents ? (
+          <Code language="javascript">
+            {stringifyResponse(modalContents.result)}
+          </Code>
+        ) : (
+          <div className="mb-10 rounded-lg bg-neutral-100/25 p-8 dark:bg-neutral-800">
+            <p className="font-mono text-sm font-medium text-red-600 dark:text-red-400">
+              {showError(modalContents.error)}
+            </p>
+          </div>
+        )}
+      </>
+    )}
+  </Modal>
+);
+
+const showError = (error: unknown): string => {
+  if (typeof error === "string") {
+    return error;
+  } else if (error instanceof ContractFunctionExecutionError) {
+    return error.cause.metaMessages?.[0] ?? error.message;
+  } else if (error instanceof Error) {
+    return error.toString();
+  } else {
+    return "An unknown error occurred";
+  }
+};
+
+const stringifyResponse = (response: unknown): string => {
+  switch (typeof response) {
+    case "string": {
+      return `"${response}"`;
+    }
+    case "number":
+    case "boolean":
+    case "function": {
+      return response.toString();
+    }
+    case "bigint": {
+      return `${response.toString()}n`;
+    }
+    case "symbol": {
+      return `Symbol(${response.toString()})`;
+    }
+    case "undefined": {
+      return "undefined";
+    }
+    case "object": {
+      return response === null
+        ? "null"
+        : `{\n${Object.entries(response)
+            .map(([key, value]) => `    ${key}: ${stringifyResponse(value)}`)
+            .join(",\n")}\n}`;
+    }
+  }
+};

+ 242 - 0
apps/api-reference/src/components/EvmApi/run-button.tsx

@@ -0,0 +1,242 @@
+"use client";
+
+import { ArrowPathIcon } from "@heroicons/react/24/outline";
+import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
+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 { getContractAddress } from "./networks";
+import { type Parameter, TRANSFORMS } from "./parameter";
+import { type ModalContents, ResultsModal } from "./results-modal";
+import { useIsMounted } from "../../use-is-mounted";
+import { Button } from "../Button";
+import { InlineLink } from "../InlineLink";
+
+const abi = [...PythAbi, ...PythErrorsAbi] as const;
+
+type RunButtonProps<ParameterName extends string> = (
+  | Read
+  | Write<ParameterName>
+) & {
+  functionName: (typeof PythAbi)[number]["name"];
+  parameters: Parameter<ParameterName>[];
+  paramValues: Partial<Record<ParameterName, string>>;
+};
+
+type Read = {
+  type: EvmApiType.Read;
+  valueParam?: undefined;
+};
+
+type Write<ParameterName extends string> = {
+  type: EvmApiType.Write;
+  valueParam: ParameterName;
+};
+
+export enum EvmApiType {
+  Read,
+  Write,
+}
+
+export const RunButton = <ParameterName extends string>(
+  props: RunButtonProps<ParameterName>,
+) => {
+  const { isConnected } = useAccount();
+  const isMounted = useIsMounted();
+  const {
+    status,
+    modalContents,
+    resetStatus,
+    clearModalContents,
+    run,
+    disabled,
+  } = useRunButton(props);
+
+  return (
+    <>
+      {props.type === EvmApiType.Write && (
+        <ConnectKitButton.Custom>
+          {({ show, isConnected, address, truncatedAddress, ensName }) => (
+            <InlineLink
+              as="button"
+              onClick={show}
+              className="mb-2 flex flex-row items-center justify-center gap-2"
+            >
+              {isConnected ? (
+                <>
+                  <Avatar address={address} size={24} />
+                  <span>Wallet: {ensName ?? truncatedAddress}</span>
+                </>
+              ) : (
+                "Connect Wallet to Run"
+              )}
+            </InlineLink>
+          )}
+        </ConnectKitButton.Custom>
+      )}
+      {(props.type === EvmApiType.Read || (isMounted && isConnected)) && (
+        <Button
+          disabled={disabled}
+          loading={status === Status.Loading}
+          className="flex h-10 w-full flex-row items-center justify-center gap-2"
+          onClick={run}
+        >
+          {status === Status.Loading ? (
+            <ArrowPathIcon className="size-4 animate-spin" />
+          ) : (
+            "Run"
+          )}
+        </Button>
+      )}
+      <ResultsModal
+        modalContents={modalContents}
+        isShowingResults={status === Status.ShowingResults}
+        resetStatus={resetStatus}
+        clearModalContents={clearModalContents}
+        functionName={props.functionName}
+      />
+    </>
+  );
+};
+
+const useRunButton = <ParameterName extends string>({
+  functionName,
+  parameters,
+  paramValues,
+  ...props
+}: RunButtonProps<ParameterName>) => {
+  const config = useConfig();
+  const [status, setStatus] = useState<Status>(Status.None);
+  const [modalContents, setModalContents] = useState<
+    ModalContents<ParameterName> | undefined
+  >(undefined);
+
+  const resetStatus = useCallback(() => {
+    setStatus(Status.None);
+  }, [setStatus]);
+  const clearModalContents = useCallback(() => {
+    setModalContents(undefined);
+  }, [setModalContents]);
+
+  const args = useMemo(() => {
+    const allParams =
+      props.type === EvmApiType.Write
+        ? parameters.filter((parameter) => parameter.name !== props.valueParam)
+        : parameters;
+    const orderedParams = allParams.map(({ name, type }) => {
+      const transform = TRANSFORMS[type];
+      const value = paramValues[name];
+      return transform && value ? transform(value) : value;
+    });
+    return orderedParams.every((value) => value !== undefined)
+      ? orderedParams
+      : undefined;
+  }, [parameters, paramValues, props]);
+
+  const value = useMemo(() => {
+    if (props.type === EvmApiType.Write) {
+      const value = paramValues[props.valueParam];
+      return value ? BigInt(value) : undefined;
+    } else {
+      return;
+    }
+  }, [paramValues, props]);
+
+  const run = useCallback(() => {
+    setStatus(Status.Loading);
+    const networkName =
+      config.chains.find((chain) => chain.id === config.state.chainId)?.name ??
+      "";
+    if (args === undefined) {
+      setModalContents({
+        error: new Error("Invalid parameters!"),
+        networkName,
+      });
+      setStatus(Status.ShowingResults);
+    } else {
+      const address = getContractAddress(config.state.chainId);
+      if (!address) {
+        throw new Error(
+          `No contract for chain id: ${config.state.chainId.toString()}`,
+        );
+      }
+      switch (props.type) {
+        case EvmApiType.Read: {
+          readContract(config, { abi, address, functionName, args })
+            .then((result) => {
+              setModalContents({
+                result,
+                parameters: paramValues,
+                networkName,
+              });
+            })
+            .catch((error: unknown) => {
+              setModalContents({
+                error: error,
+                parameters: paramValues,
+                networkName,
+              });
+            })
+            .finally(() => {
+              setStatus(Status.ShowingResults);
+            });
+          return;
+        }
+        case EvmApiType.Write: {
+          if (value === undefined) {
+            setModalContents({
+              error: new Error("Missing value!"),
+              networkName,
+            });
+            setStatus(Status.ShowingResults);
+          } else {
+            writeContract(config, { abi, address, functionName, args, value })
+              .then((result) => {
+                setModalContents({
+                  result,
+                  parameters: paramValues,
+                  networkName,
+                });
+              })
+              .catch((error: unknown) => {
+                setModalContents({
+                  error: error,
+                  parameters: paramValues,
+                  networkName,
+                });
+              })
+              .finally(() => {
+                setStatus(Status.ShowingResults);
+              });
+          }
+          return;
+        }
+      }
+    }
+  }, [config, functionName, setStatus, args, paramValues, value, props.type]);
+
+  const { isConnected } = useAccount();
+
+  const disabled =
+    args === undefined ||
+    status !== Status.None ||
+    (props.type === EvmApiType.Write && (!isConnected || value === undefined));
+
+  return {
+    status,
+    modalContents,
+    resetStatus,
+    clearModalContents,
+    run,
+    disabled,
+  };
+};
+
+enum Status {
+  None,
+  Loading,
+  ShowingResults,
+}

+ 0 - 190
apps/api-reference/src/components/EvmCall/index.tsx

@@ -1,190 +0,0 @@
-"use client";
-
-import {
-  Tab,
-  TabGroup,
-  TabList,
-  TabPanel,
-  TabPanels,
-  Field,
-  Label,
-} from "@headlessui/react";
-import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
-import { mainnet } from "@wagmi/core/chains";
-import { type ReactNode, useState } from "react";
-
-import {
-  type Network,
-  NETWORKS,
-  NETWORK_TO_CONTRACT_ADDRESS,
-} from "./networks";
-import { type Parameter, ParameterInput } from "./parameter-input";
-import { RunButton } from "./run-button";
-import { type SupportedLanguage, Code } from "../Code";
-import { InlineLink } from "../InlineLink";
-import { Select } from "../Select";
-
-export { ParameterType } from "./parameter-input";
-
-type Props<
-  ParameterName extends string,
-  Parameters extends Record<ParameterName, string>,
-> = {
-  name: (typeof PythAbi)[number]["name"];
-  children: ReactNode;
-  parameters: Parameter<ParameterName>[];
-  examples: Example<ParameterName, Parameters>[];
-  code: CodeSample<ParameterName, Parameters>[];
-};
-
-type Example<
-  ParameterName extends string,
-  Parameters extends Record<ParameterName, string>,
-> = {
-  name: string;
-  parameters: Parameters;
-};
-
-export enum Language {
-  Solidity,
-  EthersJSV6,
-}
-
-type CodeSample<
-  ParameterName extends string,
-  Parameters extends Record<ParameterName, string>,
-> = {
-  language: Language;
-  code: (
-    network: {
-      name: string;
-      rpcUrl: string;
-      contractAddress: string;
-    },
-    params: Partial<Parameters>,
-  ) => string;
-};
-
-export const EvmCall = <
-  ParameterName extends string,
-  Parameters extends Record<ParameterName, string>,
->({
-  name,
-  children,
-  parameters,
-  code,
-  examples,
-}: Props<ParameterName, Parameters>) => {
-  const [paramValues, setParamValues] = useState<Partial<Parameters>>({});
-  const [network, setNetwork] = useState<Network>(mainnet);
-
-  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>
-      <section>
-        <h2 className="mb-4 border-b border-neutral-200 text-2xl/loose font-medium dark:border-neutral-800">
-          Description
-        </h2>
-        {children}
-      </section>
-      <section className="flex flex-col">
-        <h2 className="mb-4 border-b border-neutral-200 text-2xl/loose font-medium dark:border-neutral-800">
-          Arguments
-        </h2>
-        <div className="mb-8">
-          {parameters.length > 0 ? (
-            <ul className="flex flex-col gap-4">
-              {parameters.map((parameter) => (
-                <li key={name} className="contents">
-                  <ParameterInput
-                    spec={parameter}
-                    value={paramValues[parameter.name]}
-                    setParamValues={setParamValues}
-                  />
-                </li>
-              ))}
-            </ul>
-          ) : (
-            <div className="rounded-lg bg-neutral-200 p-8 text-center text-sm dark:bg-neutral-800">
-              This API takes no arguments
-            </div>
-          )}
-        </div>
-        <div className="grow" />
-        {examples.length > 0 && (
-          <div className="mb-8">
-            <h3 className="text-sm font-semibold">Examples</h3>
-            <ul className="ml-2 text-sm">
-              {examples.map(({ name, parameters: exampleParameters }) => (
-                <li key={name}>
-                  <InlineLink
-                    as="button"
-                    onClick={() => {
-                      setParamValues(exampleParameters);
-                    }}
-                  >
-                    {name}
-                  </InlineLink>
-                </li>
-              ))}
-            </ul>
-          </div>
-        )}
-        <Field className="mb-4 flex w-full flex-row items-center gap-2">
-          <Label className="text-sm font-bold">Network</Label>
-          <Select
-            value={network}
-            onChange={setNetwork}
-            renderOption={({ name }) => name}
-            options={NETWORKS}
-            buttonClassName="grow"
-          />
-        </Field>
-        <RunButton
-          network={network}
-          functionName={name}
-          parameters={parameters}
-          paramValues={paramValues}
-        />
-      </section>
-      <TabGroup className="col-span-2 mt-24">
-        <TabList className="mb-4 flex flex-row gap-2 border-b border-neutral-200 pb-px dark:border-neutral-800">
-          {code.map(({ language }) => (
-            <Tab
-              key={LANGUAGE_TO_DISPLAY_NAME[language]}
-              className="mb-[-2px] border-b-2 border-transparent px-2 text-sm font-medium leading-loose hover:text-pythpurple-600 data-[selected]:cursor-default data-[selected]:border-pythpurple-600 data-[selected]:text-pythpurple-600 dark:hover:text-pythpurple-400 dark:data-[selected]:border-pythpurple-400 dark:data-[selected]:text-pythpurple-400"
-            >
-              {LANGUAGE_TO_DISPLAY_NAME[language]}
-            </Tab>
-          ))}
-        </TabList>
-        <TabPanels>
-          {code.map(({ code: codeContents, language }) => (
-            <TabPanel key={LANGUAGE_TO_DISPLAY_NAME[language]}>
-              <Code language={LANUGAGE_TO_SHIKI_NAME[language]}>
-                {codeContents(
-                  {
-                    name: network.name,
-                    rpcUrl: network.rpcUrls.default.http[0],
-                    contractAddress: NETWORK_TO_CONTRACT_ADDRESS[network.id],
-                  },
-                  paramValues,
-                )}
-              </Code>
-            </TabPanel>
-          ))}
-        </TabPanels>
-      </TabGroup>
-    </div>
-  );
-};
-
-const LANGUAGE_TO_DISPLAY_NAME = {
-  [Language.Solidity]: "Solidity",
-  [Language.EthersJSV6]: "ethers.js v6",
-};
-
-const LANUGAGE_TO_SHIKI_NAME: Record<Language, SupportedLanguage> = {
-  [Language.Solidity]: "solidity",
-  [Language.EthersJSV6]: "javascript",
-};

+ 0 - 13
apps/api-reference/src/components/EvmCall/networks.ts

@@ -1,13 +0,0 @@
-import { arbitrum, avalanche, mainnet, sepolia } from "@wagmi/core/chains";
-
-export const NETWORKS = [mainnet, avalanche, arbitrum, sepolia] as const;
-
-export type Network = (typeof NETWORKS)[number];
-
-export const NETWORK_TO_CONTRACT_ADDRESS: Record<Network["id"], `0x${string}`> =
-  {
-    [mainnet.id]: "0x4305FB66699C3B2702D4d05CF36551390A4c69C6",
-    [avalanche.id]: "0x4305FB66699C3B2702D4d05CF36551390A4c69C6",
-    [arbitrum.id]: "0xff1a0f4744e8582DF1aE09D5611b887B6a12925C",
-    [sepolia.id]: "0xDd24F84d36BF92C65F92307595335bdFab5Bbd21",
-  };

+ 0 - 126
apps/api-reference/src/components/EvmCall/parameter-input.tsx

@@ -1,126 +0,0 @@
-import {
-  type ReactNode,
-  type ChangeEvent,
-  type Dispatch,
-  type SetStateAction,
-  useState,
-  useCallback,
-  useMemo,
-  useEffect,
-} from "react";
-
-import { Input } from "../Input";
-
-type ParameterProps<
-  ParameterName extends string,
-  Parameters extends Record<ParameterName, string>,
-> = {
-  spec: Parameter<ParameterName>;
-  value: string | undefined;
-  setParamValues: Dispatch<SetStateAction<Partial<Parameters>>>;
-};
-
-export type Parameter<Name extends string> = {
-  name: Name;
-  type: ParameterType;
-  description: ReactNode;
-};
-
-export enum ParameterType {
-  Hex,
-  Int,
-}
-
-export const ParameterInput = <
-  ParameterName extends string,
-  Parameters extends Record<ParameterName, string>,
->({
-  spec,
-  value,
-  setParamValues,
-}: ParameterProps<ParameterName, Parameters>) => {
-  const { validationError, internalValue, onChange } = useParameterInput(
-    spec,
-    value,
-    setParamValues,
-  );
-
-  return (
-    <Input
-      validationError={validationError}
-      label={spec.name}
-      description={spec.description}
-      placeholder={PLACEHOLDERS[spec.type]}
-      required={true}
-      value={internalValue}
-      onChange={onChange}
-    />
-  );
-};
-
-const useParameterInput = <
-  ParameterName extends string,
-  Parameters extends Record<ParameterName, string>,
->(
-  spec: Parameter<ParameterName>,
-  value: string | undefined,
-  setParamValues: Dispatch<SetStateAction<Partial<Parameters>>>,
-) => {
-  const [internalValue, setInternalValue] = useState(value ?? "");
-  const validationError = useMemo(
-    () =>
-      internalValue ? getValidationError(internalValue, spec.type) : undefined,
-    [internalValue, spec.type],
-  );
-  const onChange = useCallback(
-    (e: ChangeEvent<HTMLInputElement>) => {
-      const value = e.target.value;
-      setInternalValue(value);
-      setParamValues((paramValues) => ({
-        ...paramValues,
-        [spec.name]:
-          value === "" || !isValid(value, spec.type) ? undefined : value,
-      }));
-    },
-    [setParamValues, spec.name, spec.type],
-  );
-
-  useEffect(() => {
-    if (value) {
-      setInternalValue(value);
-    }
-  }, [value]);
-
-  return { internalValue, validationError, onChange };
-};
-
-const PLACEHOLDERS: Record<ParameterType, string> = {
-  [ParameterType.Hex]:
-    "0x1111111111111111111111111111111111111111111111111111111111111111",
-  [ParameterType.Int]: "60",
-};
-
-const VALIDATION_ERRORS: Record<ParameterType, string> = {
-  [ParameterType.Hex]:
-    'Please enter a hexadecimal string prefixed with 0x, for example "0xa19f"',
-  [ParameterType.Int]: "Please enter a valid integer",
-};
-
-const HEX_REGEX = new RegExp("^(0|0x[0-9A-Fa-f]*)$");
-
-const getValidationError = (
-  value: string,
-  format: ParameterType,
-): string | undefined =>
-  isValid(value, format) ? undefined : VALIDATION_ERRORS[format];
-
-const isValid = (value: string, format: ParameterType): boolean => {
-  switch (format) {
-    case ParameterType.Hex: {
-      return HEX_REGEX.test(value);
-    }
-    case ParameterType.Int: {
-      return Number.parseInt(value, 10).toString() === value;
-    }
-  }
-};

+ 0 - 241
apps/api-reference/src/components/EvmCall/run-button.tsx

@@ -1,241 +0,0 @@
-"use client";
-
-import { ArrowPathIcon } from "@heroicons/react/24/outline";
-import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
-import PythErrorsAbi from "@pythnetwork/pyth-sdk-solidity/abis/PythErrors.json";
-import { readContract, createConfig, http } from "@wagmi/core";
-import { useCallback, useMemo, useState } from "react";
-import { ContractFunctionExecutionError, type Transport } from "viem";
-
-import { type Network, NETWORK_TO_CONTRACT_ADDRESS } from "./networks";
-import { type Parameter } from "./parameter-input";
-import { Button } from "../Button";
-import { Code } from "../Code";
-import { Modal } from "../Modal";
-
-type RunButtonProps<
-  ParameterName extends string,
-  Parameters extends Record<ParameterName, string>,
-> = {
-  functionName: (typeof PythAbi)[number]["name"];
-  network: Network;
-  parameters: Parameter<ParameterName>[];
-  paramValues: Partial<Parameters>;
-};
-
-export const RunButton = <
-  ParameterName extends string,
-  Parameters extends Record<ParameterName, string>,
->({
-  network,
-  functionName,
-  parameters,
-  paramValues,
-}: RunButtonProps<ParameterName, Parameters>) => {
-  const {
-    status,
-    modalContents,
-    resetStatus,
-    clearModalContents,
-    runContract,
-    disabled,
-  } = useRunButton(functionName, network, parameters, paramValues);
-
-  return (
-    <>
-      <Button
-        disabled={disabled}
-        loading={status === Status.Loading}
-        className="flex h-10 w-full flex-row items-center justify-center gap-2"
-        onClick={runContract}
-      >
-        {status === Status.Loading ? (
-          <ArrowPathIcon className="size-4 animate-spin" />
-        ) : (
-          "Run"
-        )}
-      </Button>
-      <Modal
-        show={modalContents !== undefined && status === Status.ShowingResults}
-        onClose={resetStatus}
-        afterLeave={clearModalContents}
-        title={
-          typeof modalContents === "object" && "result" in modalContents
-            ? "Results"
-            : "Error"
-        }
-        description={`${functionName} - ${modalContents?.network.name ?? ""}`}
-      >
-        {modalContents !== undefined && (
-          <>
-            {"parameters" in modalContents &&
-              Object.keys(modalContents.parameters).length > 0 && (
-                <div className="mb-10 rounded-lg bg-neutral-100 p-8 dark:bg-neutral-800/50">
-                  <h2 className="mb-2 border-b border-neutral-300 font-semibold text-neutral-700 dark:border-neutral-600 dark:text-neutral-300">
-                    Arguments
-                  </h2>
-                  <ul className="overflow-hidden text-xs">
-                    {Object.entries(modalContents.parameters).map(
-                      ([name, value]) => (
-                        <li className="overflow-hidden truncate" key={name}>
-                          <span className="mr-2 font-medium text-neutral-700 dark:text-neutral-300">
-                            {name}:
-                          </span>
-                          <span className="text-neutral-600 dark:text-neutral-400">
-                            {value as string}
-                          </span>
-                        </li>
-                      ),
-                    )}
-                  </ul>
-                </div>
-              )}
-            {"result" in modalContents ? (
-              <Code language="javascript">
-                {stringifyResponse(modalContents.result)}
-              </Code>
-            ) : (
-              <div className="mb-10 rounded-lg bg-neutral-100/25 p-8 dark:bg-neutral-800">
-                <p className="font-mono text-sm font-medium text-red-600 dark:text-red-400">
-                  {showError(modalContents.error)}
-                </p>
-              </div>
-            )}
-          </>
-        )}
-      </Modal>
-    </>
-  );
-};
-
-const useRunButton = <
-  ParameterName extends string,
-  Parameters extends Record<ParameterName, string>,
->(
-  functionName: (typeof PythAbi)[number]["name"],
-  network: Network,
-  parameters: Parameter<ParameterName>[],
-  paramValues: Partial<Parameters>,
-) => {
-  const [status, setStatus] = useState<Status>(Status.None);
-  const [modalContents, setModalContents] = useState<
-    ModalContents<ParameterName, Parameters> | undefined
-  >(undefined);
-  const resetStatus = useCallback(() => {
-    setStatus(Status.None);
-  }, [setStatus]);
-  const clearModalContents = useCallback(() => {
-    setModalContents(undefined);
-  }, [setModalContents]);
-  const preparedParams = useMemo(() => {
-    const orderedParams = parameters.map(({ name }) => paramValues[name]);
-    return isComplete(orderedParams) ? orderedParams : undefined;
-  }, [parameters, paramValues]);
-  const runContract = useCallback(() => {
-    setStatus(Status.Loading);
-    if (preparedParams === undefined) {
-      setModalContents({ error: new Error("Invalid parameters!"), network });
-      setStatus(Status.ShowingResults);
-    } else {
-      runFunction(network, functionName, preparedParams)
-        .then((result) => {
-          setModalContents({ result, parameters: paramValues, network });
-        })
-        .catch((error: unknown) => {
-          setModalContents({ error: error, parameters: paramValues, network });
-        })
-        .finally(() => {
-          setStatus(Status.ShowingResults);
-        });
-    }
-  }, [network, functionName, setStatus, preparedParams, paramValues]);
-  const disabled = preparedParams === undefined || status !== Status.None;
-
-  return {
-    status,
-    modalContents,
-    resetStatus,
-    clearModalContents,
-    runContract,
-    disabled,
-  };
-};
-
-type ModalContents<
-  ParameterName extends string,
-  Parameters extends Record<ParameterName, string>,
-> = { network: Network } & (
-  | { error: unknown; parameters?: Partial<Parameters> }
-  | { result: unknown; parameters: Partial<Parameters> }
-);
-
-enum Status {
-  None,
-  Loading,
-  ShowingResults,
-}
-
-const runFunction = async (
-  network: Network,
-  functionName: (typeof PythAbi)[number]["name"],
-  args: string[],
-) =>
-  readContract(
-    createConfig({
-      chains: [network],
-      transports: { [network.id]: http() } as Record<
-        (typeof network)["id"],
-        Transport
-      >,
-    }),
-    {
-      abi: [...PythAbi, ...PythErrorsAbi] as const,
-      address: NETWORK_TO_CONTRACT_ADDRESS[network.id],
-      functionName,
-      args,
-    },
-  );
-
-const showError = (error: unknown): string => {
-  if (typeof error === "string") {
-    return error;
-  } else if (error instanceof ContractFunctionExecutionError) {
-    return error.cause.metaMessages?.[0] ?? error.message;
-  } else if (error instanceof Error) {
-    return error.toString();
-  } else {
-    return "An unknown error occurred";
-  }
-};
-
-const stringifyResponse = (response: unknown): string => {
-  switch (typeof response) {
-    case "string": {
-      return `"${response}"`;
-    }
-    case "number":
-    case "boolean":
-    case "function": {
-      return response.toString();
-    }
-    case "bigint": {
-      return `${response.toString()}n`;
-    }
-    case "symbol": {
-      return `Symbol(${response.toString()})`;
-    }
-    case "undefined": {
-      return "undefined";
-    }
-    case "object": {
-      return response === null
-        ? "null"
-        : `{\n${Object.entries(response)
-            .map(([key, value]) => `    ${key}: ${stringifyResponse(value)}`)
-            .join(",\n")}\n}`;
-    }
-  }
-};
-
-const isComplete = <T,>(arr: (T | undefined)[]): arr is NonNullable<T>[] =>
-  arr.every((value) => value !== undefined);

+ 60 - 0
apps/api-reference/src/components/EvmLayout/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";
+
+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>
+  );
+};

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

@@ -6,7 +6,7 @@ import Link from "next/link";
 import { useSelectedLayoutSegments } from "next/navigation";
 import { type HTMLAttributes, useState, type ComponentProps } from "react";
 
-import { apis } from "../../apis";
+import * as apis from "../../apis";
 import { Select } from "../Select";
 
 type Chain = keyof typeof apis;

+ 0 - 21
apps/api-reference/src/layouts.tsx

@@ -1,21 +0,0 @@
-import type { ComponentProps, ReactNode } from "react";
-
-import { EvmCall } from "./components/EvmCall";
-
-export {
-  Language as EvmLanguage,
-  ParameterType as EvmParameterType,
-} from "./components/EvmCall";
-
-type Props = {
-  children: ReactNode;
-};
-
-export const evmCall = (
-  props: Omit<ComponentProps<typeof EvmCall>, "children">,
-) => {
-  const EvmCallLayout = ({ children }: Props) => (
-    <EvmCall {...props}>{children}</EvmCall>
-  );
-  return EvmCallLayout;
-};

+ 52 - 0
apps/api-reference/src/metadata.ts

@@ -0,0 +1,52 @@
+import type { Metadata, Viewport } from "next";
+
+export const metadata = {
+  metadataBase: new URL("https://api-reference.pyth.network"),
+  title: {
+    default: "Pyth Network API Reference",
+    template: "%s | Pyth Network API Reference",
+  },
+  applicationName: "Pyth Network API Reference",
+  description:
+    "API reference, interactive explorer, and documentation for Pyth network products.",
+  referrer: "strict-origin-when-cross-origin",
+  openGraph: {
+    type: "website",
+  },
+  twitter: {
+    creator: "@PythNetwork",
+    card: "summary_large_image",
+  },
+  icons: {
+    icon: [
+      {
+        media: "(prefers-color-scheme: light)",
+        type: "image/x-icon",
+        url: "/favicon.ico",
+      },
+      {
+        media: "(prefers-color-scheme: dark)",
+        type: "image/x-icon",
+        url: "/favicon-light.ico",
+      },
+      {
+        type: "image/png",
+        sizes: "32x32",
+        url: "/favicon-32x32.png",
+      },
+      {
+        type: "image/png",
+        sizes: "16x16",
+        url: "/favicon-16x16.png",
+      },
+    ],
+    apple: {
+      url: "/apple-touch-icon.png",
+      sizes: "180x180",
+    },
+  },
+} satisfies Metadata;
+
+export const viewport = {
+  themeColor: "#242235",
+} satisfies Viewport;

+ 11 - 0
apps/api-reference/src/use-is-mounted.ts

@@ -0,0 +1,11 @@
+import { useState, useEffect } from "react";
+
+export const useIsMounted = () => {
+  const [mounted, setMounted] = useState(false);
+
+  useEffect(() => {
+    setMounted(true);
+  }, []);
+
+  return mounted;
+};

+ 19 - 0
apps/api-reference/src/zod-utils.ts

@@ -0,0 +1,19 @@
+import { type ZodSchema, type ZodTypeDef, z } from "zod";
+
+export const singletonArray = <Output, Def extends ZodTypeDef, Input>(
+  schema: ZodSchema<Output, Def, Input>,
+) =>
+  z
+    .array(schema)
+    .length(1)
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    .transform((value) => value[0]!);
+
+export const safeFetch = async <Output, Def extends ZodTypeDef, Input>(
+  schema: ZodSchema<Output, Def, Input>,
+  ...fetchArgs: Parameters<typeof fetch>
+) => {
+  const response = await fetch(...fetchArgs);
+  const json: unknown = await response.json();
+  return schema.parseAsync(json);
+};

ファイルの差分が大きいため隠しています
+ 412 - 55
pnpm-lock.yaml


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません