Pārlūkot izejas kodu

Merge remote-tracking branch 'origin/main' into bduran/dual-publish

benduran 2 nedēļas atpakaļ
vecāks
revīzija
82f140a97e

+ 45 - 21
Cargo.lock

@@ -3132,7 +3132,7 @@ dependencies = [
  "tracing",
  "tracing-subscriber",
  "url",
- "utoipa",
+ "utoipa 3.5.0",
  "utoipa-swagger-ui",
 ]
 
@@ -5674,8 +5674,8 @@ dependencies = [
  "hyper 1.6.0",
  "hyper-util",
  "protobuf",
- "pyth-lazer-protocol 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "pyth-lazer-publisher-sdk 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pyth-lazer-protocol 0.20.0",
+ "pyth-lazer-publisher-sdk 0.20.0",
  "reqwest 0.12.23",
  "serde",
  "serde_json",
@@ -5694,7 +5694,7 @@ dependencies = [
 
 [[package]]
 name = "pyth-lazer-client"
-version = "8.6.0"
+version = "8.6.1"
 dependencies = [
  "alloy-primitives 0.8.25",
  "anyhow",
@@ -5712,7 +5712,7 @@ dependencies = [
  "hex",
  "humantime-serde",
  "libsecp256k1 0.7.2",
- "pyth-lazer-protocol 0.20.0",
+ "pyth-lazer-protocol 0.20.1",
  "reqwest 0.12.23",
  "serde",
  "serde_json",
@@ -5727,22 +5727,17 @@ dependencies = [
 [[package]]
 name = "pyth-lazer-protocol"
 version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2efd998c309b88c9f9790addb962cb20cb2528f3c1fe305160f998403306ba7d"
 dependencies = [
- "alloy-primitives 0.8.25",
  "anyhow",
- "assert_float_eq",
- "bincode 1.3.3",
- "bs58",
  "byteorder",
  "chrono",
  "derive_more 1.0.0",
- "ed25519-dalek 2.1.1",
  "hex",
  "humantime",
  "humantime-serde",
  "itertools 0.13.0",
- "libsecp256k1 0.7.2",
- "mry",
  "protobuf",
  "rust_decimal",
  "serde",
@@ -5752,28 +5747,36 @@ dependencies = [
 
 [[package]]
 name = "pyth-lazer-protocol"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2efd998c309b88c9f9790addb962cb20cb2528f3c1fe305160f998403306ba7d"
+version = "0.20.1"
 dependencies = [
+ "alloy-primitives 0.8.25",
  "anyhow",
+ "assert_float_eq",
+ "bincode 1.3.3",
+ "bs58",
  "byteorder",
  "chrono",
  "derive_more 1.0.0",
+ "ed25519-dalek 2.1.1",
  "hex",
  "humantime",
  "humantime-serde",
  "itertools 0.13.0",
+ "libsecp256k1 0.7.2",
+ "mry",
  "protobuf",
  "rust_decimal",
  "serde",
  "serde_json",
  "thiserror 2.0.12",
+ "utoipa 5.4.0",
 ]
 
 [[package]]
 name = "pyth-lazer-publisher-sdk"
 version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "988c6a2d6bc8d065a492d49915e803912b263e57ad5e300dc45c5a5471d609c8"
 dependencies = [
  "anyhow",
  "fs-err",
@@ -5785,15 +5788,13 @@ dependencies = [
 
 [[package]]
 name = "pyth-lazer-publisher-sdk"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "988c6a2d6bc8d065a492d49915e803912b263e57ad5e300dc45c5a5471d609c8"
+version = "0.20.1"
 dependencies = [
  "anyhow",
  "fs-err",
  "protobuf",
  "protobuf-codegen",
- "pyth-lazer-protocol 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pyth-lazer-protocol 0.20.1",
  "serde_json",
 ]
 
@@ -10554,7 +10555,19 @@ dependencies = [
  "indexmap 2.10.0",
  "serde",
  "serde_json",
- "utoipa-gen",
+ "utoipa-gen 3.5.0",
+]
+
+[[package]]
+name = "utoipa"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
+dependencies = [
+ "indexmap 2.10.0",
+ "serde",
+ "serde_json",
+ "utoipa-gen 5.4.0",
 ]
 
 [[package]]
@@ -10570,6 +10583,17 @@ dependencies = [
  "syn 2.0.104",
 ]
 
+[[package]]
+name = "utoipa-gen"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
 [[package]]
 name = "utoipa-swagger-ui"
 version = "3.1.5"
@@ -10582,7 +10606,7 @@ dependencies = [
  "rust-embed",
  "serde",
  "serde_json",
- "utoipa",
+ "utoipa 3.5.0",
  "zip",
 ]
 

+ 1 - 1
apps/developer-hub/src/app/(docs)/[section]/[...slug]/page.tsx

@@ -1,7 +1,7 @@
-export { DocumentationPage as default } from "../../../../components/Pages/DocumentationPage";
 import type { Metadata } from "next";
 import { notFound } from "next/navigation";
 
+export { DocumentationPage as default } from "../../../../components/Pages/DocumentationPage";
 import { source } from "../../../../lib/source";
 
 export function generateStaticParams() {

+ 32 - 0
apps/developer-hub/src/app/llms.txt/route.ts

@@ -0,0 +1,32 @@
+import { NextResponse } from "next/server";
+
+import { getLLMText } from "../../lib/get-llm-text";
+import { source } from "../../lib/source";
+
+export async function GET() {
+  const pages = source.getPages();
+  const scan = pages.map((page) => getLLMText(page));
+  const scanned = await Promise.all(scan);
+
+  const content = [
+    "# Pyth Documentation",
+    "",
+    "This file contains the complete Pyth documentation for LLM consumption.",
+    `Generated on: ${new Date().toISOString()}`,
+    "",
+    "## About Pyth",
+    "Pyth is a decentralized price oracle network that provides real-time price feeds for a wide range of assets.",
+    "",
+    "---",
+    "",
+    ...scanned,
+  ].join("\n");
+
+  return new NextResponse(content, {
+    status: 200,
+    headers: {
+      "Content-Type": "text/plain; charset=utf-8",
+      "Cache-Control": "public, max-age=3600", // Cache for 1 hour
+    },
+  });
+}

+ 26 - 0
apps/developer-hub/src/app/mdx/[...slug]/route.ts

@@ -0,0 +1,26 @@
+import { NextRequest, NextResponse } from "next/server";
+
+import { getLLMText } from "../../../lib/get-llm-text";
+import { source } from "../../../lib/source";
+
+export async function GET(
+  _request: NextRequest,
+  { params }: { params: Promise<{ slug: string[] }> },
+) {
+  const { slug } = await params;
+  const page = source.getPage(slug);
+
+  if (!page) {
+    return new NextResponse("Page not found", { status: 404 });
+  }
+
+  const content = await getLLMText(page);
+
+  return new NextResponse(content, {
+    status: 200,
+    headers: {
+      "Content-Type": "text/plain; charset=utf-8",
+      "Cache-Control": "public, max-age=3600", // Cache for 1 hour
+    },
+  });
+}

+ 57 - 0
apps/developer-hub/src/components/PageActions/index.module.scss

@@ -0,0 +1,57 @@
+@use "@pythnetwork/component-library/theme";
+
+.wrapper {
+  display: flex;
+  flex-direction: column;
+  gap: 0;
+  margin-top: theme.spacing(0);
+}
+
+.container {
+  display: flex;
+  align-items: center;
+  gap: 0;
+}
+
+.buttonWrapper {
+  display: flex;
+  align-items: center;
+  gap: 0;
+}
+
+.button {
+  border-radius: 0;
+
+  &:first-of-type {
+    border-top-left-radius: theme.border-radius("md");
+    border-bottom-left-radius: theme.border-radius("md");
+  }
+
+  &:last-of-type {
+    border-top-right-radius: theme.border-radius("md");
+    border-bottom-right-radius: theme.border-radius("md");
+  }
+
+  &:not(:last-of-type) {
+    border-right: none;
+  }
+}
+
+.verticalDivider {
+  width: 1px;
+  height: 1.5rem;
+  background-color: var(--color-fd-border);
+  flex-shrink: 0;
+}
+
+.horizontalDivider {
+  width: 100%;
+  height: 1px;
+  background-color: var(--color-fd-border);
+  margin-top: theme.spacing(1);
+}
+
+.icon {
+  width: 0.75rem;
+  height: 0.75rem;
+}

+ 139 - 0
apps/developer-hub/src/components/PageActions/index.tsx

@@ -0,0 +1,139 @@
+"use client";
+
+import { Copy, Check, Eye, OpenAiLogo } from "@phosphor-icons/react/dist/ssr";
+import { Button } from "@pythnetwork/component-library/Button";
+import { useCopy } from "@pythnetwork/component-library/useCopy";
+import type { ReactNode } from "react";
+
+import styles from "./index.module.scss";
+import { ClaudeIcon } from "../../lib/icons";
+
+type PageActionOption = {
+  id: string;
+  label: string;
+  icon: ReactNode;
+  ariaLabel?: string;
+};
+
+type PageActionsProps = {
+  content: string;
+  title: string;
+  url: string;
+};
+
+const getPageActionOptions = (): PageActionOption[] => [
+  {
+    id: "copy-page",
+    label: "Copy Page",
+    icon: <Copy />,
+  },
+  {
+    id: "view-markdown",
+    label: "View Markdown",
+    icon: <Eye />,
+  },
+  {
+    id: "ask-chatgpt",
+    label: "Ask in ChatGPT",
+    icon: <OpenAiLogo />,
+  },
+  {
+    id: "ask-claude",
+    label: "Ask in Claude",
+    icon: <ClaudeIcon />,
+  },
+];
+
+export function PageActions({ content, title, url }: PageActionsProps) {
+  const { isCopied, copy } = useCopy(content);
+  const pageActionOptions = getPageActionOptions();
+
+  function handleViewMarkdown() {
+    const blob = new Blob([content], { type: "text/plain" });
+    const blobUrl = URL.createObjectURL(blob);
+    window.open(blobUrl, "_blank");
+    // Clean up the URL after a delay to ensure it opens
+    setTimeout(() => {
+      URL.revokeObjectURL(blobUrl);
+    }, 1000);
+  }
+
+  function handleShare(option: PageActionOption) {
+    const prompt = `Please read and analyze this documentation page:
+
+        Title: ${title}
+        URL: ${url}
+
+        Content:
+        ${content}
+
+        Please provide a summary and answer any questions I might have about this content.`;
+
+    const encodedInstruction = encodeURIComponent(prompt);
+
+    if (option.id === "ask-claude") {
+      const shareUrl = `https://claude.ai/new?q=${encodedInstruction}`;
+      window.open(shareUrl, "_blank");
+    } else if (option.id === "ask-chatgpt") {
+      const shareUrl = `https://chat.openai.com?q=${encodedInstruction}`;
+      window.open(shareUrl, "_blank");
+    }
+  }
+
+  function handleActionClick(option: PageActionOption) {
+    switch (option.id) {
+      case "copy-page": {
+        copy();
+        break;
+      }
+      case "view-markdown": {
+        handleViewMarkdown();
+        break;
+      }
+      case "ask-chatgpt":
+      case "ask-claude": {
+        handleShare(option);
+        break;
+      }
+    }
+  }
+
+  return (
+    <div className={styles.wrapper}>
+      <div className={styles.container}>
+        {pageActionOptions.map((option, index) => {
+          const isLast = index === pageActionOptions.length - 1;
+          const isCopyAction = option.id === "copy-page";
+          const showCheckIcon = isCopyAction && isCopied;
+
+          return (
+            <div key={option.id} className={styles.buttonWrapper}>
+              <Button
+                onPress={() => {
+                  handleActionClick(option);
+                }}
+                size="sm"
+                variant="ghost"
+                className={styles.button ?? ""}
+                aria-label={option.ariaLabel ?? option.label}
+                beforeIcon={
+                  showCheckIcon ? (
+                    <Check className={styles.icon ?? ""} />
+                  ) : (
+                    option.icon
+                  )
+                }
+              >
+                {option.label}
+              </Button>
+              {!isLast && (
+                <div className={styles.verticalDivider} aria-hidden="true" />
+              )}
+            </div>
+          );
+        })}
+      </div>
+      <div className={styles.horizontalDivider} aria-hidden="true" />
+    </div>
+  );
+}

+ 12 - 2
apps/developer-hub/src/components/Pages/BasePage/index.tsx

@@ -6,19 +6,29 @@ import {
 } from "fumadocs-ui/page";
 import { notFound } from "next/navigation";
 
+import { getLLMText } from "../../../lib/get-llm-text";
 import { source } from "../../../lib/source";
 import { getMDXComponents } from "../../../mdx-components";
+import { PageActions } from "../../PageActions";
 
-export function BasePage(props: { params: { slug: string[] } }) {
+export async function BasePage(props: { params: { slug: string[] } }) {
   const page = source.getPage(props.params.slug);
   if (!page) notFound();
 
   const MDX = page.data.body;
+  const content = await getLLMText(page);
+  const title = page.data.title;
+  const url = page.url;
 
   return (
-    <DocsPage toc={page.data.toc} full={page.data.full}>
+    <DocsPage
+      toc={page.data.toc}
+      tableOfContent={{ style: "clerk" }}
+      full={page.data.full}
+    >
       <DocsTitle>{page.data.title}</DocsTitle>
       <DocsDescription>{page.data.description}</DocsDescription>
+      <PageActions content={content} title={title} url={url} />
       <DocsBody>
         <MDX components={getMDXComponents()} />
       </DocsBody>

+ 21 - 9
apps/developer-hub/src/lib/get-llm-text.ts

@@ -1,23 +1,35 @@
-import fs from "node:fs/promises";
+import { existsSync } from "node:fs";
+import { readFile } from "node:fs/promises";
+import path from "node:path";
 
-import type { InferPageType } from "fumadocs-core/source";
+import type { Page } from "fumadocs-core/source";
 import { remarkInclude } from "fumadocs-mdx/config";
 import { remark } from "remark";
 import remarkGfm from "remark-gfm";
 import remarkMdx from "remark-mdx";
 
-import { source } from "./source";
-
 const processor = remark().use(remarkMdx).use(remarkInclude).use(remarkGfm);
 
-export async function getLLMText(page: InferPageType<typeof source>) {
+function resolveMdxPath(page: Page): string {
+  const appRelPath = [process.cwd(), "content", "docs", page.path] as [
+    string,
+    ...string[],
+  ];
+
+  if (existsSync(path.join(...appRelPath))) {
+    return path.join(...appRelPath);
+  } else throw new Error(`MDX file not found at ${path.join(...appRelPath)}`);
+}
+
+export async function getLLMText(page: Page) {
+  const mdxPath = resolveMdxPath(page);
+
   const processed = await processor.process({
-    path: page.path,
-    value: await fs.readFile(page.path, "utf8"),
+    path: mdxPath,
+    value: await readFile(mdxPath, "utf8"),
   });
 
-  // note: it doesn't escape frontmatter, it's up to you.
-  return `# ${page.data.title}
+  return `# ${page.data.title ?? "Untitled"}
 URL: ${page.url}
 
 ${String(processed.value)}`;

+ 31 - 0
apps/developer-hub/src/lib/icons.tsx

@@ -0,0 +1,31 @@
+import type { ComponentProps } from "react";
+import { createElement } from "react";
+
+export function ClaudeIcon({
+  size = 12,
+  color = "currentColor",
+  className,
+  style,
+  ...props
+}: ComponentProps<"svg"> & {
+  size?: number;
+  color?: string;
+}) {
+  return createElement(
+    "svg",
+    {
+      role: "img",
+      width: size,
+      height: size,
+      viewBox: "0 0 24 24",
+      xmlns: "http://www.w3.org/2000/svg",
+      fill: color,
+      className,
+      style,
+      ...props,
+    },
+    createElement("path", {
+      d: "m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z",
+    }),
+  );
+}

+ 4 - 10
lazer/contracts/evm/src/PythLazerLib.sol

@@ -294,16 +294,10 @@ library PythLazerLib {
                     property == PythLazerStructs.PriceFeedProperty.Exponent
                 ) {
                     (feed._exponent, pos) = parseFeedValueInt16(payload, pos);
-                    if (feed._exponent != 0)
-                        _setPresent(
-                            feed,
-                            uint8(PythLazerStructs.PriceFeedProperty.Exponent)
-                        );
-                    else
-                        _setApplicableButMissing(
-                            feed,
-                            uint8(PythLazerStructs.PriceFeedProperty.Exponent)
-                        );
+                    _setPresent(
+                        feed,
+                        uint8(PythLazerStructs.PriceFeedProperty.Exponent)
+                    );
 
                     // Confidence Property
                 } else if (

+ 2 - 2
lazer/publisher_sdk/rust/Cargo.toml

@@ -1,13 +1,13 @@
 [package]
 name = "pyth-lazer-publisher-sdk"
-version = "0.20.0"
+version = "0.20.1"
 edition = "2021"
 description = "Pyth Lazer Publisher SDK types."
 license = "Apache-2.0"
 repository = "https://github.com/pyth-network/pyth-crosschain"
 
 [dependencies]
-pyth-lazer-protocol = { version = "0.20.0", path = "../../sdk/rust/protocol" }
+pyth-lazer-protocol = { version = "0.20.1", path = "../../sdk/rust/protocol" }
 anyhow = "1.0.98"
 protobuf = "3.7.2"
 serde_json = "1.0.140"

+ 2 - 2
lazer/sdk/rust/client/Cargo.toml

@@ -1,12 +1,12 @@
 [package]
 name = "pyth-lazer-client"
-version = "8.6.0"
+version = "8.6.1"
 edition = "2021"
 description = "A Rust client for Pyth Lazer"
 license = "Apache-2.0"
 
 [dependencies]
-pyth-lazer-protocol = { path = "../protocol", version = "0.20.0" }
+pyth-lazer-protocol = { path = "../protocol", version = "0.20.1" }
 tokio = { version = "1", features = ["full"] }
 tokio-tungstenite = { version = "0.20", features = ["native-tls"] }
 futures-util = "0.3"

+ 2 - 1
lazer/sdk/rust/protocol/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "pyth-lazer-protocol"
-version = "0.20.0"
+version = "0.20.1"
 edition = "2021"
 description = "Pyth Lazer SDK - protocol types."
 license = "Apache-2.0"
@@ -21,6 +21,7 @@ chrono = "0.4.41"
 humantime = "2.2.0"
 hex = "0.4.3"
 thiserror = "2.0.12"
+utoipa = "5.3.1"
 
 [dev-dependencies]
 bincode = "1.3.3"

+ 43 - 27
lazer/sdk/rust/protocol/src/api.rs

@@ -7,6 +7,7 @@ use std::{
 use derive_more::derive::From;
 use itertools::Itertools as _;
 use serde::{de::Error, Deserialize, Serialize};
+use utoipa::ToSchema;
 
 use crate::{
     payload::AggregatedPriceFeedData,
@@ -14,11 +15,13 @@ use crate::{
     ChannelId, Price, PriceFeedId, PriceFeedProperty, Rate,
 };
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct LatestPriceRequestRepr {
     // Either price feed ids or symbols must be specified.
+    #[schema(example = json!([1]))]
     pub price_feed_ids: Option<Vec<PriceFeedId>>,
+    #[schema(example = schema_default_symbols)]
     pub symbols: Option<Vec<String>>,
     pub properties: Vec<PriceFeedProperty>,
     // "chains" was renamed to "formats". "chains" is still supported for compatibility.
@@ -33,7 +36,7 @@ pub struct LatestPriceRequestRepr {
     pub channel: Channel,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct LatestPriceRequest(LatestPriceRequestRepr);
 
@@ -79,12 +82,13 @@ impl DerefMut for LatestPriceRequest {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct PriceRequestRepr {
     pub timestamp: TimestampUs,
     // Either price feed ids or symbols must be specified.
     pub price_feed_ids: Option<Vec<PriceFeedId>>,
+    #[schema(default)]
     pub symbols: Option<Vec<String>>,
     pub properties: Vec<PriceFeedProperty>,
     pub formats: Vec<Format>,
@@ -97,7 +101,7 @@ pub struct PriceRequestRepr {
     pub channel: Channel,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct PriceRequest(PriceRequestRepr);
 
@@ -143,7 +147,7 @@ impl DerefMut for PriceRequest {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct ReducePriceRequest {
     pub payload: JsonUpdate,
@@ -158,7 +162,14 @@ pub fn default_parsed() -> bool {
     true
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
+pub fn schema_default_symbols() -> Option<Vec<String>> {
+    None
+}
+pub fn schema_default_price_feed_ids() -> Option<Vec<PriceFeedId>> {
+    Some(vec![PriceFeedId(1)])
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub enum DeliveryFormat {
     /// Deliver stream updates as JSON text messages.
@@ -168,7 +179,7 @@ pub enum DeliveryFormat {
     Binary,
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub enum Format {
     Evm,
@@ -177,7 +188,7 @@ pub enum Format {
     LeUnsigned,
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub enum JsonBinaryEncoding {
     #[default]
@@ -185,9 +196,11 @@ pub enum JsonBinaryEncoding {
     Hex,
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, From)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, From, ToSchema)]
+#[schema(example = "fixed_rate@200ms")]
 pub enum Channel {
     FixedRate(FixedRate),
+    #[schema(rename = "real_time")]
     RealTime,
 }
 
@@ -275,11 +288,12 @@ impl<'de> Deserialize<'de> for Channel {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct SubscriptionParamsRepr {
     // Either price feed ids or symbols must be specified.
     pub price_feed_ids: Option<Vec<PriceFeedId>>,
+    #[schema(default)]
     pub symbols: Option<Vec<String>>,
     pub properties: Vec<PriceFeedProperty>,
     // "chains" was renamed to "formats". "chains" is still supported for compatibility.
@@ -299,7 +313,7 @@ pub struct SubscriptionParamsRepr {
     pub ignore_invalid_feeds: bool,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct SubscriptionParams(SubscriptionParamsRepr);
 
@@ -345,14 +359,14 @@ impl DerefMut for SubscriptionParams {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct JsonBinaryData {
     pub encoding: JsonBinaryEncoding,
     pub data: String,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct JsonUpdate {
     /// Present unless `parsed = false` is specified in subscription params.
@@ -372,7 +386,7 @@ pub struct JsonUpdate {
     pub le_unsigned: Option<JsonBinaryData>,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct ParsedPayload {
     #[serde(with = "crate::serde_str::timestamp")]
@@ -380,7 +394,7 @@ pub struct ParsedPayload {
     pub price_feeds: Vec<ParsedFeedPayload>,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct ParsedFeedPayload {
     pub price_feed_id: PriceFeedId,
@@ -491,7 +505,7 @@ impl ParsedFeedPayload {
 }
 
 /// A request sent from the client to the server.
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(tag = "type")]
 #[serde(rename_all = "camelCase")]
 pub enum WsRequest {
@@ -499,10 +513,12 @@ pub enum WsRequest {
     Unsubscribe(UnsubscribeRequest),
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[derive(
+    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
+)]
 pub struct SubscriptionId(pub u64);
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct SubscribeRequest {
     pub subscription_id: SubscriptionId,
@@ -510,14 +526,14 @@ pub struct SubscribeRequest {
     pub params: SubscriptionParams,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct UnsubscribeRequest {
     pub subscription_id: SubscriptionId,
 }
 
 /// A JSON response sent from the server to the client.
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, From)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, From, ToSchema)]
 #[serde(tag = "type")]
 #[serde(rename_all = "camelCase")]
 pub enum WsResponse {
@@ -530,13 +546,13 @@ pub enum WsResponse {
 }
 
 /// Sent from the server after a successul subscription.
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct SubscribedResponse {
     pub subscription_id: SubscriptionId,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct InvalidFeedSubscriptionDetails {
     pub unknown_ids: Vec<PriceFeedId>,
@@ -545,7 +561,7 @@ pub struct InvalidFeedSubscriptionDetails {
     pub unstable: Vec<PriceFeedId>,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct SubscribedWithInvalidFeedIdsIgnoredResponse {
     pub subscription_id: SubscriptionId,
@@ -553,7 +569,7 @@ pub struct SubscribedWithInvalidFeedIdsIgnoredResponse {
     pub ignored_invalid_feed_ids: InvalidFeedSubscriptionDetails,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct UnsubscribedResponse {
     pub subscription_id: SubscriptionId,
@@ -561,7 +577,7 @@ pub struct UnsubscribedResponse {
 
 /// Sent from the server if the requested subscription or unsubscription request
 /// could not be fulfilled.
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct SubscriptionErrorResponse {
     pub subscription_id: SubscriptionId,
@@ -570,7 +586,7 @@ pub struct SubscriptionErrorResponse {
 
 /// Sent from the server if an internal error occured while serving data for an existing subscription,
 /// or a client request sent a bad request.
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct ErrorResponse {
     pub error: String,
@@ -578,7 +594,7 @@ pub struct ErrorResponse {
 
 /// Sent from the server when new data is available for an existing subscription
 /// (only if `delivery_format == Json`).
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct StreamUpdatedResponse {
     pub subscription_id: SubscriptionId,

+ 16 - 2
lazer/sdk/rust/protocol/src/lib.rs

@@ -24,6 +24,7 @@ pub mod time;
 
 use derive_more::derive::{From, Into};
 use serde::{Deserialize, Serialize};
+use utoipa::ToSchema;
 
 pub use crate::{
     dynamic_value::DynamicValue,
@@ -39,8 +40,21 @@ pub use crate::{
 pub struct PublisherId(pub u16);
 
 #[derive(
-    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into,
+    Debug,
+    Clone,
+    Copy,
+    PartialEq,
+    Eq,
+    Hash,
+    PartialOrd,
+    Ord,
+    Serialize,
+    Deserialize,
+    From,
+    Into,
+    ToSchema,
 )]
+#[schema(value_type = u32)]
 pub struct PriceFeedId(pub u32);
 
 #[derive(
@@ -55,7 +69,7 @@ impl ChannelId {
     pub const FIXED_RATE_1000: ChannelId = ChannelId(4);
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
 #[serde(rename_all = "camelCase")]
 pub enum PriceFeedProperty {
     Price,

+ 5 - 1
lazer/sdk/rust/protocol/src/price.rs

@@ -7,6 +7,7 @@ use {
     serde::{Deserialize, Serialize},
     std::num::NonZeroI64,
     thiserror::Error,
+    utoipa::ToSchema,
 };
 
 #[derive(Debug, Error)]
@@ -21,8 +22,11 @@ pub enum PriceError {
     Overflow,
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[derive(
+    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
+)]
 #[repr(transparent)]
+#[schema(value_type = i64)]
 pub struct Price(NonZeroI64);
 
 impl Price {

+ 5 - 1
lazer/sdk/rust/protocol/src/rate.rs

@@ -6,6 +6,7 @@ use {
     rust_decimal::{prelude::FromPrimitive, Decimal},
     serde::{Deserialize, Serialize},
     thiserror::Error,
+    utoipa::ToSchema,
 };
 
 #[derive(Debug, Error)]
@@ -18,8 +19,11 @@ pub enum RateError {
     Overflow,
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[derive(
+    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
+)]
 #[repr(transparent)]
+#[schema(value_type = i64)]
 pub struct Rate(i64);
 
 impl Rate {

+ 11 - 3
lazer/sdk/rust/protocol/src/time.rs

@@ -11,11 +11,15 @@ use {
     },
     serde::{Deserialize, Serialize},
     std::time::{Duration, SystemTime},
+    utoipa::ToSchema,
 };
 
 /// Unix timestamp with microsecond resolution.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[derive(
+    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
+)]
 #[repr(transparent)]
+#[schema(value_type = u64)]
 pub struct TimestampUs(u64);
 
 #[cfg_attr(feature = "mry", mry::mry)]
@@ -279,7 +283,10 @@ impl TryFrom<TimestampUs> for chrono::DateTime<chrono::Utc> {
 }
 
 /// Non-negative duration with microsecond resolution.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[derive(
+    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
+)]
+#[schema(value_type = u64)]
 pub struct DurationUs(u64);
 
 impl DurationUs {
@@ -487,7 +494,8 @@ pub mod duration_us_serde_humantime {
     }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, ToSchema)]
+#[schema(as = String, example = "fixed_rate@200ms")]
 pub struct FixedRate {
     rate: DurationUs,
 }

+ 4 - 0
packages/component-library/package.json

@@ -296,6 +296,10 @@
       "types": "./dist/useLogger/index.d.ts",
       "default": "./dist/useLogger/index.mjs"
     },
+    "./useCopy": {
+      "types": "./dist/esm/useCopy/index.d.ts",
+      "default": "./dist/esm/useCopy/index.js"
+    },
     "./useQueryParamsPagination": {
       "types": "./dist/useQueryParamsPagination/index.d.ts",
       "default": "./dist/useQueryParamsPagination/index.mjs"

+ 3 - 36
packages/component-library/src/CopyButton/index.tsx

@@ -4,13 +4,10 @@ import { Check } from "@phosphor-icons/react/dist/ssr/Check";
 import { Copy } from "@phosphor-icons/react/dist/ssr/Copy";
 import clsx from "clsx";
 import type { ComponentProps } from "react";
-import { useCallback, useEffect, useState } from "react";
 
-import styles from "./index.module.scss";
 import { Button } from "../unstyled/Button/index.jsx";
-import { useLogger } from "../useLogger/index.jsx";
-
-const COPY_INDICATOR_TIME = 1000;
+import { useCopy } from "../useCopy";
+import styles from "./index.module.scss";
 
 type OwnProps = {
   text: string;
@@ -30,37 +27,7 @@ export const CopyButton = ({
   className,
   ...props
 }: Props) => {
-  const [isCopied, setIsCopied] = useState(false);
-  const logger = useLogger();
-  const copy = useCallback(() => {
-    navigator.clipboard
-      .writeText(text)
-      .then(() => {
-        setIsCopied(true);
-      })
-      .catch((error: unknown) => {
-        /* TODO do something here? */
-        logger.error(error);
-      });
-  }, [text, logger]);
-
-  useEffect(() => {
-    setIsCopied(false);
-  }, [text]);
-
-  useEffect(() => {
-    if (isCopied) {
-      const timeout = setTimeout(() => {
-        setIsCopied(false);
-      }, COPY_INDICATOR_TIME);
-      return () => {
-        clearTimeout(timeout);
-      };
-    } else {
-      return;
-    }
-  }, [isCopied]);
-
+  const { isCopied, copy } = useCopy(text);
   return (
     <Button
       onPress={copy}

+ 39 - 0
packages/component-library/src/useCopy/index.ts

@@ -0,0 +1,39 @@
+"use client";
+
+import { useCallback, useEffect, useState } from "react";
+
+import { useLogger } from "../useLogger";
+
+export const useCopy = (text: string, copyIndicatorTime = 1000) => {
+  const [isCopied, setIsCopied] = useState(false);
+  const logger = useLogger();
+  const copy = useCallback(() => {
+    navigator.clipboard
+      .writeText(text)
+      .then(() => {
+        setIsCopied(true);
+      })
+      .catch((error: unknown) => {
+        logger.error(error);
+      });
+  }, [text, logger]);
+
+  useEffect(() => {
+    setIsCopied(false);
+  }, [text]);
+
+  useEffect(() => {
+    if (isCopied) {
+      const timeout = setTimeout(() => {
+        setIsCopied(false);
+      }, copyIndicatorTime);
+      return () => {
+        clearTimeout(timeout);
+      };
+    } else {
+      return;
+    }
+  }, [isCopied, copyIndicatorTime]);
+
+  return { isCopied, copy };
+};