Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: feat(docs): ACT-1532 - Create Pages for the Linea JSON-RPC methods dynamically #1559

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Note: type annotations allow type checking and IDEs autocompletion

require("dotenv").config();
const { fetchAndGenerateSidebarItems } = require("./src/helpers/index.ts");
const capitalize = require("lodash.capitalize");
const { themes } = require("prism-react-renderer");
const codeTheme = themes.dracula;
const remarkCodesandbox = require("remark-codesandbox");
Expand Down Expand Up @@ -33,6 +35,8 @@ const config = {
customFields: {
LD_CLIENT_ID: process.env.LD_CLIENT_ID,
SENTRY_KEY: process.env.SENTRY_KEY,
sidebarData: {},
dynamicData: []
},

trailingSlash: true,
Expand Down Expand Up @@ -123,6 +127,20 @@ const config = {
editUrl: "https://github.com/MetaMask/metamask-docs/edit/main/",
sidebarPath: require.resolve("./services-sidebar.js"),
breadcrumbs: false,
sidebarItemsGenerator: async function ({ defaultSidebarItemsGenerator, ...args }) {
config.customFields.sidebarData = args;
let sidebarItems = await defaultSidebarItemsGenerator(args);
const networkName = "linea";
const dynamicSidebarItems = await fetchAndGenerateSidebarItems(networkName);
config.customFields.dynamicData = dynamicSidebarItems;
const updatedItems = sidebarItems.map(item => {
if (item?.label === capitalize(networkName) && item?.items) {
item.items = [...item.items, ...dynamicSidebarItems]
}
return item;
})
return [...updatedItems];
},
},
],
[
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"dotenv": "^16.4.5",
"js-cookie": "^3.0.5",
"launchdarkly-js-client-sdk": "^3.3.0",
"lodash.capitalize": "^4.2.1",
"lodash.debounce": "^4.0.8",
"lodash.isobject": "^3.0.2",
"node-polyfill-webpack-plugin": "^2.0.1",
Expand Down
37 changes: 32 additions & 5 deletions src/components/ParserOpenRPC/RequestBox/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useMemo } from "react";
import React, { useMemo, useState } from "react";
import clsx from "clsx";
import CodeBlock from "@theme/CodeBlock";
import { MethodParam } from "@site/src/components/ParserOpenRPC/interfaces";
import styles from "./styles.module.css";
import global from "../global.module.css";
import { Tooltip } from "@site/src/components/ParserOpenRPC/Tooltip";
import { useLocation } from "@docusaurus/router";
import { useColorMode } from "@docusaurus/theme-common";

interface RequestBoxProps {
isMetamaskInstalled: boolean;
Expand All @@ -15,6 +17,8 @@ interface RequestBoxProps {
openModal: () => void;
submitRequest: () => void;
isMetamaskNetwork?: boolean;
customAPIKey?: string;
setCustomAPIKey?: (key: string) => void;
}

export default function RequestBox({
Expand All @@ -26,25 +30,31 @@ export default function RequestBox({
openModal,
submitRequest,
isMetamaskNetwork = false,
customAPIKey,
setCustomAPIKey,
}: RequestBoxProps) {

const location = useLocation();
const { colorMode } = useColorMode();
const isWalletReferencePage = location.pathname.includes("/wallet/reference");
const isLineaReferencePage = location.pathname.includes("/services/reference/linea");
const exampleRequest = useMemo(() => {
const preparedParams = JSON.stringify(paramsData, null, 2);
const preparedShellParams = JSON.stringify(paramsData);
const NETWORK_URL = "https://linea-mainnet.infura.io";
const API_KEY = "<YOUR-API-KEY>";
const lineaAPIKey = customAPIKey ? customAPIKey : API_KEY;
if (isMetamaskNetwork) {
return `await window.ethereum.request({\n "method": "${method}",\n "params": ${preparedParams.replace(/"([^"]+)":/g, '$1:')},\n});`;
}
return `curl ${NETWORK_URL}/v3/${API_KEY} \\\n -X POST \\\n -H "Content-Type: application/json" \\\n -d '{\n "jsonrpc": "2.0",\n "method": "${method}",\n "params": ${preparedShellParams},\n "id": 1\n }'`;
}, [method, paramsData]);
return `curl ${NETWORK_URL}/v3/${isLineaReferencePage ? lineaAPIKey : API_KEY} \\\n -X POST \\\n -H "Content-Type: application/json" \\\n -d '{\n "jsonrpc": "2.0",\n "method": "${method}",\n "params": ${preparedShellParams},\n "id": 1\n }'`;
}, [customAPIKey, method, paramsData]);

const exampleResponse = useMemo(() => {
return JSON.stringify(response, null, 2);
}, [response]);

const methodsWithRequiredWalletConnection = ["eth_accounts", "eth_sendTransaction", "personal_sign", "eth_signTypedData_v4"];
const isRunAndCustomizeRequestDisabled = methodsWithRequiredWalletConnection.includes(method) ?
const isRunAndCustomizeRequestDisabled = isWalletReferencePage && methodsWithRequiredWalletConnection.includes(method) ?
!isMetamaskInstalled :
false;

Expand All @@ -61,6 +71,23 @@ export default function RequestBox({

return (
<>
{!isWalletReferencePage ?
<div style={{ marginBottom: "20px" }}>
<label htmlFor="custom_key">Your API Key:</label>
<input
name="custom_key"
value={customAPIKey}
onChange={(e) => setCustomAPIKey(e.target.value)}
style={{
marginLeft: "10px",
padding: "8px",
border: `1px solid ${colorMode === "dark" ? "#fff" : "#848c96"}`,
borderRadius: "8px",
width: "360px"
}} />
</div> :
null
}
<div className={styles.cardWrapper}>
<div className={styles.cardHeader}>
<strong className={styles.cardHeading}>Request</strong>
Expand Down
67 changes: 50 additions & 17 deletions src/components/ParserOpenRPC/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { createContext, useMemo, useState, useContext } from "react";
import { usePluginData } from "@docusaurus/useGlobalData";
import { ResponseItem, NETWORK_NAMES } from "@site/src/plugins/plugin-json-rpc";
import { ResponseItem } from "@site/src/plugins/plugin-json-rpc";
import { NETWORK_NAMES } from "@site/src/lib/constants";
import DetailsBox from "@site/src/components/ParserOpenRPC/DetailsBox";
import InteractiveBox from "@site/src/components/ParserOpenRPC/InteractiveBox";
import { AuthBox } from "@site/src/components/ParserOpenRPC/AuthBox";
Expand All @@ -13,6 +14,7 @@ import clsx from "clsx";
import { useColorMode } from "@docusaurus/theme-common";
import { trackClickForSegment, trackInputChangeForSegment } from "@site/src/lib/segmentAnalytics";
import { MetamaskProviderContext } from "@site/src/theme/Root";
import { useLocation } from "@docusaurus/router";

interface ParserProps {
network: NETWORK_NAMES;
Expand All @@ -39,7 +41,18 @@ export default function ParserOpenRPC({ network, method, extraContent }: ParserP
const [isDrawerContentFixed, setIsDrawerContentFixed] = useState(false);
const [drawerLabel, setDrawerLabel] = useState(null);
const [isComplexTypeView, setIsComplexTypeView] = useState(false);
const [customAPIKey, setCustomAPIKey] = useState("");
const { colorMode } = useColorMode();
const location = useLocation();
const isWalletReferencePage = location.pathname.includes("/wallet/reference");
const trackAnalyticsForRequest = (response) => {
trackClickForSegment({
eventName: "Request Sent",
clickType: "Request Sent",
userExperience: "B",
...(response?.code && { responseStatus: response.code }),
});
}
const openModal = () => {
setModalOpen(true);
trackClickForSegment({
Expand Down Expand Up @@ -132,21 +145,39 @@ export default function ParserOpenRPC({ network, method, extraContent }: ParserP
};

const onSubmitRequestHandle = async () => {
if (!metaMaskProvider) return
try {
const response = await metaMaskProvider?.request({
method: method,
params: paramsData
})
setReqResult(response);
trackClickForSegment({
eventName: "Request Sent",
clickType: "Request Sent",
userExperience: "B",
...(response?.code && { responseStatus: response.code }),
});
} catch (e) {
setReqResult(e);
if (isMetamaskNetwork) {
if (!metaMaskProvider) return
try {
const response = await metaMaskProvider?.request({
method: method,
params: paramsData
})
setReqResult(response);
trackAnalyticsForRequest(response);
} catch (e) {
setReqResult(e);
}
} else {
const NETWORK_URL = "https://linea-mainnet.infura.io";
const URL = `${NETWORK_URL}/v3/${customAPIKey}`;
let params = {
method: "POST",
"Content-Type": "application/json",
body: JSON.stringify({
jsonrpc: "2.0",
method,
params: paramsData,
id: 1,
}),
};
const res = await fetch(URL, params);
if (res.ok) {
const response = await res.json();
setReqResult(response.result);
trackAnalyticsForRequest(response.result);
} else {
console.error("error");
}
}
};

Expand Down Expand Up @@ -228,7 +259,7 @@ export default function ParserOpenRPC({ network, method, extraContent }: ParserP
</div>
<div className={global.colRight}>
<div className={global.stickyCol}>
{!metaMaskAccount && <AuthBox handleConnect={metaMaskConnectHandler} />}
{isWalletReferencePage && !metaMaskAccount && <AuthBox handleConnect={metaMaskConnectHandler} />}
<RequestBox
isMetamaskInstalled={!!metaMaskAccount}
method={method}
Expand All @@ -238,6 +269,8 @@ export default function ParserOpenRPC({ network, method, extraContent }: ParserP
openModal={openModal}
submitRequest={onSubmitRequestHandle}
isMetamaskNetwork={isMetamaskNetwork}
customAPIKey={customAPIKey}
setCustomAPIKey={setCustomAPIKey}
/>
</div>
</div>
Expand Down
25 changes: 25 additions & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { RPC_NETWORK_URL } from "../lib/constants";

export const fetchAndGenerateSidebarItems = async (networkName) => {
try {
const response = await fetch(`${RPC_NETWORK_URL}/${networkName}`);
const data = await response.json();
const dynamicItems = data.methods.map((method) => ({
type: "link",
label: method.name,
href: `/services/reference/linea/json-rpc-methods-new/${method.name}`,
})).sort((a, b) => a.label.localeCompare(b.label));
return [
{
type: "category",
label: "JSON-RPC Methods NEW",
collapsed: true,
collapsible: true,
items: dynamicItems,
},
];
} catch (error) {
console.error("Error fetching methods:", error);
return [];
}
}
7 changes: 7 additions & 0 deletions src/lib/constants.js → src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,3 +449,10 @@ export const GET_OPTIONS = {
cache: "no-cache",
headers: NO_CACHE,
};

export const RPC_NETWORK_URL = "https://sot-network-methods.vercel.app/specs";

export enum NETWORK_NAMES {
linea = "linea",
metamask = "metamask",
}
121 changes: 121 additions & 0 deletions src/pages/CustomPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import Layout from "@theme/Layout";
import { NETWORK_NAMES } from "@site/src/plugins/plugin-json-rpc";
import ParserOpenRPC from "@site/src/components/ParserOpenRPC";
import React, { useEffect, useState } from "react";
import DocSidebar from '@theme/DocSidebar';
import styles from "@site/src/theme/Layout/styles.module.css"
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import * as capitalize from "lodash.capitalize"

function generateSidebarItems(docs) {
const categories = {};

docs.forEach((doc) => {
if (doc.id === 'index') {
categories['Introduction'] = {
type: 'link',
href: '/services',
label: capitalize(doc.frontMatter?.sidebar_label || doc.title),
};
return;
}

const pathParts = doc.sourceDirName.split('/');
let currentCategory = categories;
let isIndexPage = doc.id.endsWith('/index');

pathParts.forEach((part, index) => {
if (!currentCategory[part]) {
if (isIndexPage && index === pathParts.length - 2) {
currentCategory[part] = {
type: 'category',
label: capitalize(doc.frontMatter?.sidebar_label || doc.frontMatter?.title || part),
collapsed: true,
collapsible: true,
link: {
type: 'generated-index',
slug: pathParts.slice(0, index + 1).join('/')
},
items: []
};
} else {
currentCategory[part] = {
type: 'category',
label: capitalize(part),
collapsed: true,
collapsible: true,
items: []
};
}
}

if (index === pathParts.length - 1 && !isIndexPage) {
currentCategory[part].items.push({
type: 'link',
label: capitalize(doc.frontMatter?.title || doc.title),
href: `/services/${doc.id.replace(/\/index$/, '')}`,
sidebar_position: doc.frontMatter?.sidebar_position || Number.MAX_SAFE_INTEGER
});
}
currentCategory = currentCategory[part].items;
});
});

const convertToArray = (categoryObj) => {
return Object.values(categoryObj).map((category) => {
if (category.items && typeof category.items === 'object') {
category.items = convertToArray(category.items);
if (category.items.every(item => item.sidebar_position !== undefined)) {
category.items.sort((a, b) => (a.sidebar_position || Number.MAX_SAFE_INTEGER) - (b.sidebar_position || Number.MAX_SAFE_INTEGER));
}
}
return category;
});
};
return convertToArray(categories);
}

const sidebar_wrapper_classes = "theme-doc-sidebar-container docSidebarContainer_node_modules-@docusaurus-theme-classic-lib-theme-DocRoot-Layout-Sidebar-styles-module"

const CustomPage = (props) => {
const customData = props.route.customData;
const { siteConfig } = useDocusaurusContext();
const [formattedData, setFormattedData] = useState([]);

useEffect(() => {
setFormattedData(generateSidebarItems(siteConfig.customFields.sidebarData.docs).map(item => {
if (item?.label === "Reference" && item?.items) {
return {
...item,
items: item.items.map(referenceItem => {
if (referenceItem?.label === capitalize(NETWORK_NAMES.linea) && referenceItem?.items) {
return { ...referenceItem, items: [...referenceItem.items, ...siteConfig.customFields.dynamicData] };
}
return referenceItem;
})
}
}
return item;
}));
}, []);

return formattedData ? (
<Layout>
<div className={styles.pageWrapper}>
<aside className={sidebar_wrapper_classes}>
<DocSidebar sidebar={formattedData} path="" onCollapse={() => {}} isHidden={false} />
</aside>
<div className={styles.mainContainer}>
<div className={styles.contentWrapper}>
<ParserOpenRPC
network={NETWORK_NAMES.linea}
method={customData.name}
/>
</div>
</div>
</div>
</Layout>
) : null;
};

export default CustomPage;
Loading
Loading