diff --git a/README.md b/README.md index eb2d24b37..4d0a8eb09 100644 --- a/README.md +++ b/README.md @@ -48,3 +48,25 @@ This command generates static content into the `build` directory and can be serv Create a PR and once merged, Github actions automatically deploy it. The docs use Vercel for hosting, and deployment is done by Vercel on any merge into the master branch. + +## Refund Metrics Widget + +This site displays MEV and gas refund metrics in the navbar, fetched from the [Flashbots Refund Metrics API](https://github.com/flashbots/refund-metrics-dune-api). + +### Configuration + +To configure the widget, edit `docusaurus.config.js`: + +```js +customFields: { + refundMetricsApiUrl: 'https://refund-metrics-dune-api.vercel.app', + refundMetricsRedirectUrl: 'https://protect.flashbots.net/', +}, +``` + +- `refundMetricsApiUrl`: The API endpoint for fetching metrics +- `refundMetricsRedirectUrl`: Where to redirect when users click on MEV refunds + +The widget implementation is in `src/components/MevMetrics.tsx`. For Flashbots docs, it: +- Shows both MEV and gas refunds +- Clicking on MEV refunds redirects to the configured URL (default: [Flashbots Protect](https://protect.flashbots.net/)) diff --git a/docusaurus.config.js b/docusaurus.config.js index 5a7a40480..523e9f5da 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -69,6 +69,10 @@ module.exports = async function createConfigAsync() { sidebarId: 'api', position: 'left', }, + { + type: 'custom-mevMetrics', + position: 'right', + }, { href: 'https://github.com/flashbots/docs', label: 'GitHub', @@ -116,5 +120,9 @@ module.exports = async function createConfigAsync() { }, 'docusaurus-plugin-sass' ], + customFields: { + refundMetricsApiUrl: 'https://refund-metrics-dune-api.vercel.app', + refundMetricsRedirectUrl: 'https://protect.flashbots.net/', + }, } } diff --git a/src/components/MevMetrics.module.css b/src/components/MevMetrics.module.css new file mode 100644 index 000000000..7cee49a6c --- /dev/null +++ b/src/components/MevMetrics.module.css @@ -0,0 +1,52 @@ +.container { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.875rem; + color: var(--ifm-navbar-link-color); + margin-right: 0.75rem; +} + +.metric { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.label { + font-weight: 400; +} + +.value { + font-family: monospace; + font-weight: 600; + transition: opacity 0.3s ease; +} + +.loading { + opacity: 0.5; +} + +.separator { + color: var(--ifm-navbar-link-color); + opacity: 0.3; +} + +.clickable { + cursor: pointer; + transition: opacity 0.2s ease; +} + +.clickable:hover { + opacity: 0.8; +} + +.clickable:active { + opacity: 0.6; +} + +@media (max-width: 996px) { + .container { + display: none !important; + } +} \ No newline at end of file diff --git a/src/components/MevMetrics.tsx b/src/components/MevMetrics.tsx new file mode 100644 index 000000000..66852ed42 --- /dev/null +++ b/src/components/MevMetrics.tsx @@ -0,0 +1,82 @@ +import React, { useEffect, useState } from 'react'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import styles from './MevMetrics.module.css'; + +interface MetricsResponse { + totalMevRefund: number; + totalGasRefund: number; + fetchedAt: string; + stale: boolean; +} + +export default function MevMetrics(): JSX.Element { + const { siteConfig } = useDocusaurusContext(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchMetrics = async () => { + try { + const apiUrl = siteConfig.customFields?.refundMetricsApiUrl as string; + const response = await fetch(`${apiUrl}/api/metrics`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const metrics: MetricsResponse = await response.json(); + setData(metrics); + } catch (error) { + console.error('Error fetching MEV metrics:', error); + // Mock data as fallback + setData({ + totalMevRefund: 380.29, + totalGasRefund: 444.24, + fetchedAt: new Date().toISOString(), + stale: true + }); + } finally { + setLoading(false); + } + }; + + fetchMetrics(); + }, []); + + const formatValue = (value: number): string => { + return `${value.toFixed(2)} ETH`; + }; + + const handleMevClick = () => { + const redirectUrl = siteConfig.customFields?.refundMetricsRedirectUrl as string; + window.open(redirectUrl, '_blank', 'noopener,noreferrer'); + }; + + return ( +
+ Refund + | +
{ + if (e.key === 'Enter' || e.key === ' ') { + handleMevClick(); + } + }} + > + MEV: + + {loading ? '...' : data && formatValue(data.totalMevRefund)} + +
+ | +
+ Gas: + + {loading ? '...' : data && formatValue(data.totalGasRefund)} + +
+
+ ); +} \ No newline at end of file diff --git a/src/theme/NavbarItem/ComponentTypes.tsx b/src/theme/NavbarItem/ComponentTypes.tsx new file mode 100644 index 000000000..faff4de86 --- /dev/null +++ b/src/theme/NavbarItem/ComponentTypes.tsx @@ -0,0 +1,27 @@ +import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; +import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem'; +import LocaleDropdownNavbarItem from '@theme/NavbarItem/LocaleDropdownNavbarItem'; +import SearchNavbarItem from '@theme/NavbarItem/SearchNavbarItem'; +import HtmlNavbarItem from '@theme/NavbarItem/HtmlNavbarItem'; +import DocNavbarItem from '@theme/NavbarItem/DocNavbarItem'; +import DocSidebarNavbarItem from '@theme/NavbarItem/DocSidebarNavbarItem'; +import DocsVersionNavbarItem from '@theme/NavbarItem/DocsVersionNavbarItem'; +import DocsVersionDropdownNavbarItem from '@theme/NavbarItem/DocsVersionDropdownNavbarItem'; +import MevMetrics from '@site/src/components/MevMetrics'; + +import type {ComponentTypesObject} from '@theme/NavbarItem/ComponentTypes'; + +const ComponentTypes: ComponentTypesObject = { + default: DefaultNavbarItem, + localeDropdown: LocaleDropdownNavbarItem, + search: SearchNavbarItem, + dropdown: DropdownNavbarItem, + html: HtmlNavbarItem, + doc: DocNavbarItem, + docSidebar: DocSidebarNavbarItem, + docsVersion: DocsVersionNavbarItem, + docsVersionDropdown: DocsVersionDropdownNavbarItem, + 'custom-mevMetrics': MevMetrics, +}; + +export default ComponentTypes;