-
Notifications
You must be signed in to change notification settings - Fork 39
chore(sponsored-feeds): use JSON format for the sponsored feed data for EVM #746
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
Changes from 6 commits
dacbf0f
cf9d95f
bc00d71
d62f8b2
d50cbb7
c7cd51a
d814ff0
66eabcb
1c733c4
6dc21ee
49728da
00fb7c1
9647761
1e62312
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,77 +1,149 @@ | ||
import { useState } from "react"; | ||
import { useState, useEffect } from "react"; | ||
import CopyIcon from "./icons/CopyIcon"; | ||
import { mapValues } from "../utils/ObjectHelpers"; | ||
import { useCopyToClipboard } from "../utils/useCopyToClipboard"; | ||
|
||
interface UpdateParameters { | ||
heartbeatLength: number; | ||
heartbeatUnit: "second" | "minute" | "hour"; | ||
priceDeviation: number; | ||
} | ||
// Import the data for each network. The data is in the form of a yaml file. | ||
const networkImports = { | ||
ethereum_mainnet: () => | ||
import( | ||
"../pages/price-feeds/sponsored-feeds/data/evm/ethereum_mainnet.yaml" | ||
), | ||
|
||
base_mainnet: () => | ||
import("../pages/price-feeds/sponsored-feeds/data/evm/base_mainnet.yaml"), | ||
berachain_mainnet: () => | ||
import( | ||
"../pages/price-feeds/sponsored-feeds/data/evm/berachain_mainnet.yaml" | ||
), | ||
hyperevm_mainnet: () => | ||
import( | ||
"../pages/price-feeds/sponsored-feeds/data/evm/hyperevm_mainnet.yaml" | ||
), | ||
kraken_mainnet: () => | ||
import("../pages/price-feeds/sponsored-feeds/data/evm/kraken_mainnet.yaml"), | ||
unichain_mainnet: () => | ||
import( | ||
"../pages/price-feeds/sponsored-feeds/data/evm/unichain_mainnet.yaml" | ||
), | ||
sonic_mainnet: () => | ||
import("../pages/price-feeds/sponsored-feeds/data/evm/sonic_mainnet.yaml"), | ||
optimism_sepolia: () => | ||
import( | ||
"../pages/price-feeds/sponsored-feeds/data/evm/optimism_sepolia.yaml" | ||
), | ||
unichain_sepolia: () => | ||
import( | ||
"../pages/price-feeds/sponsored-feeds/data/evm/unichain_sepolia.yaml" | ||
), | ||
}; | ||
|
||
// SponsoredFeed interface has the same structure as defined in deployment yaml files | ||
interface SponsoredFeed { | ||
name: string; | ||
priceFeedId: string; | ||
updateParameters: UpdateParameters; | ||
alias: string; // name of the feed | ||
id: string; // price feed id | ||
time_difference: number; // in seconds | ||
price_deviation: number; | ||
confidence_ratio: number; | ||
} | ||
|
||
interface SponsoredFeedsTableProps { | ||
feeds: SponsoredFeed[]; | ||
networkKey: string; | ||
networkName: string; | ||
} | ||
|
||
/** | ||
* Helper functions | ||
*/ | ||
// Convert time_difference (seconds) to human readable format | ||
const formatTimeUnit = (seconds: number): { value: number; unit: string } => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat for this. It's a bit overkill for our needs as we don't plan to localize pretty much ever, certainly not any time soon, but in general it's probably wise to stick with standards whenever possible. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, using Intl.DurationFormat seems like a overkill, the code earlier was easy to read imo. Though I agree with your point of sticking around with standards. So added Intl.DurationFormat in Was using regex with |
||
if (seconds >= 3600) { | ||
return { value: seconds / 3600, unit: "hour" }; | ||
} else if (seconds >= 60) { | ||
return { value: seconds / 60, unit: "minute" }; | ||
} else { | ||
return { value: seconds, unit: "second" }; | ||
} | ||
}; | ||
|
||
// Format update parameters as a string for grouping | ||
const formatUpdateParams = (params: UpdateParameters): string => { | ||
return `${params.heartbeatLength} ${params.heartbeatUnit} heartbeat / ${params.priceDeviation}% price deviation`; | ||
const formatUpdateParams = (feed: SponsoredFeed): string => { | ||
const timeFormat = formatTimeUnit(feed.time_difference); | ||
const timeStr = `${timeFormat.value} ${timeFormat.unit}${ | ||
timeFormat.value !== 1 ? "s" : "" | ||
}`; | ||
return `${timeStr} heartbeat / ${feed.price_deviation}% price deviation`; | ||
}; | ||
|
||
// Render update parameters with proper styling | ||
const renderUpdateParams = (params: UpdateParameters, isDefault: boolean) => ( | ||
<div className="flex items-start gap-1.5"> | ||
<div | ||
className={`w-1.5 h-1.5 rounded-full mt-1 flex-shrink-0 ${ | ||
isDefault ? "bg-green-500" : "bg-orange-500" | ||
}`} | ||
></div> | ||
<span | ||
className={`text-xs leading-relaxed font-medium ${ | ||
isDefault | ||
? "text-gray-700 dark:text-gray-300" | ||
: "text-orange-600 dark:text-orange-400" | ||
}`} | ||
> | ||
<strong>{params.heartbeatLength}</strong> {params.heartbeatUnit} heartbeat | ||
<br /> | ||
<strong>{params.priceDeviation}%</strong> price deviation | ||
</span> | ||
</div> | ||
); | ||
const renderUpdateParams = (feed: SponsoredFeed, isDefault: boolean) => { | ||
|
||
const timeFormat = formatTimeUnit(feed.time_difference); | ||
const timeStr = | ||
timeFormat.value === 1 ? timeFormat.unit : `${timeFormat.unit}s`; | ||
|
||
return ( | ||
<div className="flex items-start gap-1.5"> | ||
<div | ||
className={`w-1.5 h-1.5 rounded-full mt-1 flex-shrink-0 ${ | ||
isDefault ? "bg-green-500" : "bg-orange-500" | ||
}`} | ||
></div> | ||
<span | ||
className={`text-xs leading-relaxed font-medium ${ | ||
isDefault | ||
? "text-gray-700 dark:text-gray-300" | ||
: "text-orange-600 dark:text-orange-400" | ||
}`} | ||
> | ||
<strong>{timeFormat.value}</strong> {timeStr} heartbeat | ||
<br /> | ||
<strong>{feed.price_deviation}%</strong> price deviation | ||
</span> | ||
</div> | ||
); | ||
}; | ||
|
||
export const SponsoredFeedsTable = ({ | ||
feeds, | ||
networkKey, | ||
networkName, | ||
}: SponsoredFeedsTableProps) => { | ||
const [copiedId, setCopiedId] = useState<string | null>(null); | ||
const [feeds, setFeeds] = useState<SponsoredFeed[]>([]); | ||
const { copiedText, copyToClipboard } = useCopyToClipboard(); | ||
|
||
useEffect(() => { | ||
const loadFeeds = async () => { | ||
const importFn = | ||
networkImports[networkKey as keyof typeof networkImports]; | ||
if (importFn) { | ||
const feedsModule = await importFn(); | ||
setFeeds(feedsModule.default || []); | ||
} | ||
}; | ||
|
||
loadFeeds(); | ||
}, [networkKey]); | ||
|
||
const copyToClipboard = (text: string) => { | ||
navigator.clipboard.writeText(text).then(() => { | ||
setCopiedId(text); | ||
setTimeout(() => setCopiedId(null), 2000); | ||
}); | ||
}; | ||
// Handle empty feeds | ||
if (feeds.length === 0) { | ||
return ( | ||
<div className="my-6"> | ||
<p className="mb-3"> | ||
No sponsored price feeds are currently available for{" "} | ||
<strong>{networkName}</strong>. | ||
</p> | ||
</div> | ||
); | ||
} | ||
|
||
// Calculate parameter statistics | ||
const paramCounts = mapValues( | ||
Object.groupBy(feeds, (feed) => formatUpdateParams(feed.updateParameters)), | ||
Object.groupBy(feeds, (feed) => formatUpdateParams(feed)), | ||
(feeds: SponsoredFeed[]) => feeds.length | ||
); | ||
|
||
const defaultParams = Object.entries(paramCounts).sort( | ||
const paramEntries = Object.entries(paramCounts).sort( | ||
([, a], [, b]) => b - a | ||
)[0][0]; | ||
); | ||
const defaultParams = paramEntries.length > 0 ? paramEntries[0][0] : ""; | ||
|
||
return ( | ||
<div className="my-6"> | ||
|
@@ -123,33 +195,31 @@ export const SponsoredFeedsTable = ({ | |
</tr> | ||
</thead> | ||
<tbody className="bg-white dark:bg-gray-900"> | ||
{feeds.map((feed, index) => { | ||
const formattedParams = formatUpdateParams( | ||
feed.updateParameters | ||
); | ||
{feeds.map((feed) => { | ||
const formattedParams = formatUpdateParams(feed); | ||
const isDefault = formattedParams === defaultParams; | ||
|
||
return ( | ||
<tr | ||
key={feed.priceFeedId} | ||
key={feed.id} | ||
className="border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/30" | ||
> | ||
<td className="px-3 py-2 align-top"> | ||
<span className="font-medium text-gray-900 dark:text-gray-100"> | ||
{feed.name} | ||
{feed.alias} | ||
</span> | ||
</td> | ||
<td className="px-3 py-2 align-top"> | ||
<div className="flex items-start gap-2"> | ||
<code className="text-xs font-mono text-gray-600 dark:text-gray-400 flex-1 break-all leading-relaxed"> | ||
{feed.priceFeedId} | ||
{feed.id} | ||
</code> | ||
<button | ||
onClick={() => copyToClipboard(feed.priceFeedId)} | ||
onClick={() => copyToClipboard(feed.id)} | ||
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-600 rounded flex-shrink-0 mt-0.5" | ||
title="Copy Price Feed ID" | ||
> | ||
{copiedId === feed.priceFeedId ? ( | ||
{copiedText === feed.id ? ( | ||
<span className="text-green-500 text-xs font-bold"> | ||
✓ | ||
</span> | ||
|
@@ -160,7 +230,7 @@ export const SponsoredFeedsTable = ({ | |
</div> | ||
</td> | ||
<td className="px-3 py-2 align-top"> | ||
{renderUpdateParams(feed.updateParameters, isDefault)} | ||
{renderUpdateParams(feed, isDefault)} | ||
</td> | ||
</tr> | ||
); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than importing all networks here, can't you make a component which will take the yaml on run?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing import of the json files in the mdx file directly, not using network key anymore.