Skip to content

Commit

Permalink
Add capability for feed notices (#1440)
Browse files Browse the repository at this point in the history
* Add feed notice capability

---------

Co-authored-by: Yacine Benichou <[email protected]>

* Fix typescript issues

* Edits

* More edits not included in previous commit

* Updates

* Fix typo in threshold variable name

---------

Co-authored-by: Yacine Benichou <[email protected]>
  • Loading branch information
dwightjl and Yacine Benichou authored Aug 7, 2023
1 parent 4558d92 commit f6cc730
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 3 deletions.
2 changes: 0 additions & 2 deletions src/features/feeds/components/FeedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useEffect, useState } from "preact/hooks"
import { MainnetTable, TestnetTable } from "./Tables"
import feedList from "./FeedList.module.css"
import { clsx } from "~/lib"
import button from "@chainlink/design-system/button.module.css"
import { updateTableOfContents } from "~/components/RightSidebar/TableOfContents/tocStore"
import { Chain, CHAINS, ALL_CHAINS } from "../data/chains"
import { useGetChainMetadata } from "./useGetChainMetadata"
Expand Down Expand Up @@ -39,7 +38,6 @@ export const FeedList = ({
const isPor = dataFeedType === "por"
const isNftFloor = dataFeedType === "nftFloor"
const isRates = dataFeedType === "rates"
const isDefault = !isPor && !isNftFloor && !isRates
const isDeprecating = ecosystem === "deprecating"
let netCount = 0

Expand Down
20 changes: 20 additions & 0 deletions src/features/feeds/components/FeedPage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,32 @@ export type Props = {
}
import { getServerSideChainMetadata } from "../api/backend"
import { CHAINS, ALL_CHAINS } from "../data/chains"
import { CheckHeartbeat } from "./pause-notice/CheckHeartbeat"
import { FeedDataItem, monitoredFeeds } from "../data"
const { initialNetwork, ecosystem, dataFeedType } = Astro.props
const initialCache = await getServerSideChainMetadata([...CHAINS, ...ALL_CHAINS])
const feedItems: FeedDataItem[] = monitoredFeeds.mainnet
---

{
dataFeedType === "por"
? feedItems.map((feedItem: FeedDataItem) => {
const [feedAddress] = Object.keys(feedItem)
return (
<CheckHeartbeat
client:idle
{feedAddress}
supportedChain="ETHEREUM_MAINNET"
feedName="TUSD Reserves"
currencyName={feedItem[feedAddress]}
/>
)
})
: ""
}

{
ecosystem === "deprecating" ? (
<>
Expand Down
20 changes: 19 additions & 1 deletion src/features/feeds/components/Tables.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/** @jsxImportSource preact */
import h from "preact"
import feedList from "./FeedList.module.css"
import { clsx } from "../../../lib"
import { ChainNetwork } from "../data/chains"
import tableStyles from "./Tables.module.css"
import { CheckHeartbeat } from "./pause-notice/CheckHeartbeat"
import { monitoredFeeds, FeedDataItem } from "../data"

const feedItems = monitoredFeeds.mainnet
const feedCategories = {
verified: (
<span className={clsx(feedList.hoverText, tableStyles.statusIcon, "feed-category")} title="Verified">
Expand Down Expand Up @@ -152,6 +155,21 @@ const ProofOfReserveTHead = ({
const ProofOfReserveTr = ({ network, proxy, showExtraDetails, isTestnet = false }) => (
<tr>
<td class={tableStyles.pairCol}>
{feedItems.map((feedItem: FeedDataItem) => {
const [feedAddress] = Object.keys(feedItem)
if (feedAddress === proxy.proxyAddress) {
return (
<CheckHeartbeat
feedAddress={proxy.proxyAddress}
supportedChain="ETHEREUM_MAINNET"
feedName="TUSD Reserves"
list
currencyName={feedItem[feedAddress]}
/>
)
}
return ""
})}
<div className={tableStyles.assetPair}>
{feedCategories[proxy.docs.feedCategory] || ""}
{proxy.name}
Expand Down
1 change: 1 addition & 0 deletions src/features/feeds/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./get-price/LatestPrice"
export * from "./get-price/HistoricalPrice"
export * from "./get-price/RegistryPrice"
export * from "./FeedList"
export * from "./pause-notice/CheckHeartbeat"
67 changes: 67 additions & 0 deletions src/features/feeds/components/pause-notice/CheckHeartbeat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/** @jsxImportSource preact */
import { useCallback, useEffect, useState } from "preact/hooks"
import { aggregatorV3InterfaceABI } from "@abi"
import { Contract } from "ethers"
import { ROUND_DATA_RESPONSE } from "@features/feeds"
import { PauseNotice } from "./PauseNotice"
import { SupportedChain } from "@config"
import { getWeb3Provider } from "~/features/utils"

export const CheckHeartbeat = ({
feedAddress,
supportedChain,
feedName,
list,
currencyName,
}: {
feedAddress: string
supportedChain: SupportedChain
feedName: string
list?: boolean
currencyName: string
}) => {
const [latestUpdateTimestamp, setLatestUpdateTimestamp] = useState<number | undefined>(undefined)
const getLatestTimestamp = useCallback(async () => {
const rpcProvider = getWeb3Provider(supportedChain)
if (!rpcProvider) {
console.error(`web3 provider not found for chain ${supportedChain}`)
return
}
const dataFeed = new Contract(feedAddress, aggregatorV3InterfaceABI, rpcProvider)
const roundData: ROUND_DATA_RESPONSE = await dataFeed.latestRoundData()
if (!roundData.updatedAt) {
return
}
const updatedTimestamp = roundData.updatedAt.toNumber()
setLatestUpdateTimestamp(updatedTimestamp)
}, [feedAddress])

useEffect(() => {
let timeout: NodeJS.Timeout
const fetchData = async () => {
await getLatestTimestamp()
if (!latestUpdateTimestamp) {
timeout = setInterval(getLatestTimestamp, 5000)
}
}
fetchData()
return () => {
clearTimeout(timeout)
}
}, [getLatestTimestamp, latestUpdateTimestamp])

return latestUpdateTimestamp ? (
<div>
<PauseNotice
value={latestUpdateTimestamp}
list={list}
type="alert"
feedName={feedName}
feedAddress={feedAddress}
heartbeat={86400}
buffer={900}
currencyName={currencyName}
/>
</div>
) : null
}
75 changes: 75 additions & 0 deletions src/features/feeds/components/pause-notice/PauseNotice.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
.notice {
color: black;
padding: 0.5em;
}

.banner {
display: inline-flex;
border-color: black;
border-style: solid;
border-width: 0.1em;
border-radius: 0.4em;
}

.alert {
background-color: var(--yellow-100);
}

.danger {
background-color: var(--red-200);
}

.alert:before {
background-color: var(--yellow-100);
}

.danger:before {
background-color: var(--red-200);
}

.icon {
min-width: 2em;
margin-left: 1em;
vertical-align: middle;
}

.iconSmall {
min-width: 1em;
margin-left: 0.5em;
vertical-align: middle;
}

.tooltip {
position: relative;
}

.tooltip:hover {
color: var(--black);
cursor: default;
}

.tooltip:before {
position: absolute;
z-index: 50;
bottom: 4em;
left: 0;
right: 0;
content: attr(tooltip-text);
visibility: hidden;
opacity: 0;
max-width: 50vw;
min-width: 24em;
color: black;
text-align: left;
border: solid;
border-color: black;
border-width: 0.1em;
border-radius: 0.4em;
padding: 0.4em;
transition: opacity 0.2s ease-in-out;
}

.tooltip:hover:before {
opacity: 1;
visibility: visible;
}
83 changes: 83 additions & 0 deletions src/features/feeds/components/pause-notice/PauseNotice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/** @jsxImportSource preact */
import styles from "./PauseNotice.module.css"
import dangerIcon from "../../../../components/Alert/Assets/danger-icon.svg"
import alertIcon from "../../../../components/Alert/Assets/alert-icon.svg"
import { useEffect, useState } from "preact/hooks"

// SVG icon paths
const icons = {
alert: alertIcon,
danger: dangerIcon,
}

export const PauseNotice = ({
list,
type = "alert",
feedName,
feedAddress,
value,
heartbeat,
buffer,
currencyName,
}: {
value: number
list?: boolean
type: "alert" | "danger"
feedName: string
feedAddress: string
heartbeat: number
buffer: number
currencyName: string
}) => {
const [ripCord, setRipCord] = useState<boolean>(false)
const date = Math.floor(new Date().getTime() / 1000)
const timeSinceUpdate = date - value
const threshold = heartbeat + buffer

useEffect(() => {
const fetchRipCord = async () => {
const res = await fetch(
`https://api.real-time-reserves.ledgerlens.io/v1/chainlink/proof-of-reserves/${currencyName}`,
{
method: "GET",
}
)
const fecthedProofOfReserveData = await res.json()
setRipCord(fecthedProofOfReserveData.ripCord ?? false)
}
fetchRipCord().catch((error) => {
console.error(error)
})
}, [ripCord])
// TODO: Add dynamic scanner URL paths from chain data.
if (timeSinceUpdate > threshold && ripCord) {
if (!list) {
return (
<>
<div class={styles.banner + " " + styles[type]}>
<img class={styles.icon} src={icons[type]}></img>
<p class={styles.notice}>
The <a href={`https://etherscan.io/address/${feedAddress}`}>{feedName} feed</a> is paused due to lack of
attestation data. Read the <a href="/data-feeds/proof-of-reserve">Proof of Reserves</a> page to learn more
about data attestation types.
</p>
</div>
</>
)
} else {
return (
<>
<span
class={styles.banner + " " + styles.tooltip + " " + styles[type]}
tooltip-text="This feed is paused due to lack of attestation data."
>
<img class={styles.iconSmall} src={icons[type]}></img>
<p class={styles.notice}>Paused</p>
</span>
</>
)
}
} else {
return null
}
}
12 changes: 12 additions & 0 deletions src/features/feeds/data/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export type FeedDataItem = {
[key: string]: string
}

export const priceFeedAddresses = {
btc: {
usd: {
Expand All @@ -22,3 +26,11 @@ export const registryAddresses = {
},
},
}

export const monitoredFeeds = {
mainnet: [
{
"0xBE456fd14720C3aCCc30A2013Bffd782c9Cb75D5": "TrueUSD",
},
],
}

1 comment on commit f6cc730

@vercel
Copy link

@vercel vercel bot commented on f6cc730 Aug 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.