diff --git a/docs/Styling.md b/docs/Styling.md index 6eefe0aa..2f907eee 100644 --- a/docs/Styling.md +++ b/docs/Styling.md @@ -34,6 +34,29 @@ import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/ import { Button } from '@/components/common/Button'; ``` +### Global Modal Context + +Use `useGlobalModal` when you need a modal to persist independently of the component that triggers it: + +```tsx +import { useGlobalModal } from '@/contexts/GlobalModalContext'; + +// In your component (e.g., inside a tooltip) +const { toggleModal, closeModal } = useGlobalModal(); + + +``` + +**When to use:** +- Modals triggered from tooltips (modal stays open when tooltip closes) +- Modals triggered from ephemeral UI that may unmount + +**When NOT to use:** +- Standard page-level modals with local state +- Cases where parent component stays mounted + ### Standard Modal Pattern Use this pattern for primary workflows, settings, and management interfaces: diff --git a/package.json b/package.json index a57302d6..fe7e69eb 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "format": "prettier --log-level warn --write .", "format:check": "prettier --check .", "generate:chainlink": "tsx scripts/generate-chainlink-data.ts", + "generate:redstone": "tsx scripts/generate-redstone-data.ts", "lint": "next lint --fix", "lint:check": "next lint", "start": "next start", diff --git a/scripts/generate-redstone-data.ts b/scripts/generate-redstone-data.ts new file mode 100644 index 00000000..fa7dedff --- /dev/null +++ b/scripts/generate-redstone-data.ts @@ -0,0 +1,231 @@ +#!/usr/bin/env tsx + +import { writeFileSync } from 'fs'; +import { join } from 'path'; + +type PriceFeedConfig = { + priceFeedAddress: string; + updateTriggersOverrides?: { + deviationPercentage?: number; + timeSinceLastUpdateInMilliseconds?: number; + [key: string]: any; + }; +}; + +type RawRedstoneConfig = { + chain: { + name: string; + id: number; + }; + updateTriggers: { + deviationPercentage: number; + timeSinceLastUpdateInMilliseconds: number; + }; + adapterContract: string; + adapterContractType: string; + dataServiceId: string; + priceFeeds: Record; + [key: string]: any; +}; + +type CleanRedstoneEntry = { + path: string; + priceFeedAddress: string; + fundamental: boolean; + dataServiceId: string; + heartbeat: number; + threshold: number; +}; + +const BASE_URL = + 'https://raw.githubusercontent.com/redstone-finance/redstone-oracles-monorepo/main/packages/relayer-remote-config/main/relayer-manifests-multi-feed'; + + +const ENDPOINTS = { + mainnet: 'ethereumMultiFeed.json', + base: 'baseMultiFeed.json', + polygon: 'polygonMultiFeed.json', + arbitrum: 'arbitrumOneMultiFeed.json', + hyperevm: 'hyperevmMultiFeed.json', + monad: 'monadMultiFeed.json', +} as const; + +/** + * Mapping of derivative tokens to their underlying assets + * + * For FUNDAMENTAL feeds, Redstone tracks the on-chain exchange rate between + * an underlying asset and its derivative (e.g., wstETH/stETH from Lido). + * This mapping determines the correct quote asset for fundamental price feeds. + * + * Examples: + * - wstETH (derivative) -> ETH (underlying asset) + * - LBTC (derivative) -> BTC (underlying asset) + * - sUSDe (derivative) -> USDe (underlying asset) + */ +const FUNDAMENTAL_TO_UNDERLYING_MAPPING: Record = { + // BTC derivative tokens -> BTC underlying + lbtc: 'btc', + + // ETH derivative tokens -> ETH underlying + susde: 'usde', + + reth: 'eth', + weeth: 'eth', + ezeth: 'eth', + oseth: 'eth', + pufeth: 'eth', + wsteth: 'eth', + + // HYPE derivative tokens -> HYPE underlying + sthype: 'hype', + mhype: 'hype', + khype: 'hype', + hbhype: 'hype', + lsthype: 'hype', + + // Other derivative tokens + hbusdt: 'usdt', + hbbtc: 'btc', + + thbill: 'usd', +}; + +/** + * Generates the price feed path for a Redstone feed + * + * For FUNDAMENTAL feeds: Returns derivative/underlying pair (e.g., "wsteth/eth") + * For STANDARD feeds: Returns token/usd pair (e.g., "btc/usd") + * + * @param feedName - The raw feed name from Redstone config + * @param isFundamental - Whether this is a fundamental (contract rate) feed + * @returns The normalized path in format "base/quote" + */ +const generatePath = (feedName: string, isFundamental: boolean): string => { + // Check if the feed already contains a pair (e.g., "eBTC/WBTC" or "stHYPE/HYPE") + if (feedName.includes('/')) { + return feedName.toLowerCase(); + } + + // Strip _FUNDAMENTAL and other suffixes if present + const baseName = feedName + .replace(/_FUNDAMENTAL$/i, '') + .replace(/_DAILY_ACCRUAL$/i, '') + .replace(/_DAILY_INTEREST_ACCRUAL$/i, '') + .replace(/_ETHEREUM$/i, '') + .replace(/_ETH$/i, ''); + + const baseNameLower = baseName.toLowerCase(); + + if (isFundamental) { + // For FUNDAMENTAL feeds, find the underlying asset the derivative tracks + // e.g., wstETH (derivative) tracks its exchange rate to ETH (underlying) + const underlyingAsset = FUNDAMENTAL_TO_UNDERLYING_MAPPING[baseNameLower]; + if (underlyingAsset) { + return `${baseNameLower}/${underlyingAsset}`; + } + // If no mapping found, use "unknown" as underlying asset + return `${baseNameLower}/unknown`; + } + + // For STANDARD (market) feeds, default to USD pair + return `${baseNameLower}/usd`; +}; + +const isFundamental = (feedName: string): boolean => { + return feedName.endsWith('_FUNDAMENTAL'); +}; + +const cleanRedstoneEntry = ( + feedName: string, + feedData: { + priceFeedAddress: string; + updateTriggersOverrides?: { + deviationPercentage?: number; + timeSinceLastUpdateInMilliseconds?: number; + [key: string]: any; + }; + }, + config: RawRedstoneConfig, +): CleanRedstoneEntry => { + // Use override values if they exist, otherwise use global values + const heartbeatMs = + feedData.updateTriggersOverrides?.timeSinceLastUpdateInMilliseconds ?? + config.updateTriggers.timeSinceLastUpdateInMilliseconds; + const threshold = + feedData.updateTriggersOverrides?.deviationPercentage ?? + config.updateTriggers.deviationPercentage; + + const fundamental = isFundamental(feedName); + + return { + path: generatePath(feedName, fundamental), + priceFeedAddress: feedData.priceFeedAddress, + fundamental, + dataServiceId: config.dataServiceId, + heartbeat: Math.floor(heartbeatMs / 1000), + threshold, + }; +}; + +const fetchAndProcessData = async ( + network: keyof typeof ENDPOINTS, +): Promise => { + console.log(`Fetching ${network} Redstone oracle data...`); + + try { + const url = `${BASE_URL}/${ENDPOINTS[network]}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch ${network} data: ${response.statusText}`); + } + + const rawConfig: RawRedstoneConfig = await response.json(); + const priceFeeds = rawConfig.priceFeeds; + + console.log(`Found ${Object.keys(priceFeeds).length} price feeds for ${network}`); + + const cleanEntries = Object.entries(priceFeeds).map(([feedName, feedData]) => + cleanRedstoneEntry(feedName, feedData, rawConfig), + ); + + return cleanEntries; + } catch (error) { + console.error(`Error fetching ${network} data:`, error); + throw error; + } +}; + +const writeJsonFile = (filename: string, data: CleanRedstoneEntry[]): void => { + const outputPath = join( + process.cwd(), + 'src', + 'constants', + 'oracle', + 'redstone-data', + `${filename}.json`, + ); + writeFileSync(outputPath, JSON.stringify(data, null, 2)); + console.log(`Written ${data.length} entries to ${filename}.json`); +}; + +const main = async (): Promise => { + console.log('Starting Redstone oracle data generation...\n'); + + try { + const networks = Object.keys(ENDPOINTS) as (keyof typeof ENDPOINTS)[]; + + for (const network of networks) { + const cleanData = await fetchAndProcessData(network); + writeJsonFile(network, cleanData); + } + + console.log('\n✅ All Redstone oracle data files generated successfully!'); + } catch (error) { + console.error('\n❌ Error generating Redstone oracle data:', error); + process.exit(1); + } +}; + +if (require.main === module) { + main(); +} diff --git a/src/components/MarketOracle/FeedEntry.tsx b/src/components/MarketOracle/FeedEntry.tsx index d3c8b054..32fec7ed 100644 --- a/src/components/MarketOracle/FeedEntry.tsx +++ b/src/components/MarketOracle/FeedEntry.tsx @@ -14,6 +14,7 @@ import { OracleFeed } from '@/utils/types'; import { ChainlinkFeedTooltip } from './ChainlinkFeedTooltip'; import { CompoundFeedTooltip } from './CompoundFeedTooltip'; import { GeneralFeedTooltip } from './GeneralFeedTooltip'; +import { RedstoneFeedTooltip } from './RedstoneFeedTooltip'; import { UnknownFeedTooltip } from './UnknownFeedTooltip'; type FeedEntryProps = { @@ -44,6 +45,7 @@ export function FeedEntry({ feed, chainId }: FeedEntryProps): JSX.Element | null const vendorIcon = OracleVendorIcons[vendor]; const isChainlink = vendor === PriceFeedVendors.Chainlink; const isCompound = vendor === PriceFeedVendors.Compound; + const isRedstone = vendor === PriceFeedVendors.Redstone; // Type-safe SVR check using discriminated union const isSVR = vendor === PriceFeedVendors.Chainlink && data?.isSVR; @@ -57,6 +59,8 @@ export function FeedEntry({ feed, chainId }: FeedEntryProps): JSX.Element | null return ; case PriceFeedVendors.Redstone: + return ; + case PriceFeedVendors.PythNetwork: case PriceFeedVendors.Oval: case PriceFeedVendors.Lido: @@ -106,7 +110,7 @@ export function FeedEntry({ feed, chainId }: FeedEntryProps): JSX.Element | null )} - {(isChainlink || isCompound) && vendorIcon ? ( + {(isChainlink || isCompound || isRedstone) && vendorIcon ? ( Oracle ) : ( diff --git a/src/components/MarketOracle/RedstoneFeedTooltip.tsx b/src/components/MarketOracle/RedstoneFeedTooltip.tsx new file mode 100644 index 00000000..e9ae907c --- /dev/null +++ b/src/components/MarketOracle/RedstoneFeedTooltip.tsx @@ -0,0 +1,108 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { IoHelpCircleOutline } from 'react-icons/io5'; +import { Address } from 'viem'; +import { RedstoneOracleEntry } from '@/constants/oracle/redstone-data'; +import { useGlobalModal } from '@/contexts/GlobalModalContext'; +import etherscanLogo from '@/imgs/etherscan.png'; +import { getExplorerURL } from '@/utils/external'; +import { PriceFeedVendors, OracleVendorIcons } from '@/utils/oracle'; +import { OracleFeed } from '@/utils/types'; +import { RedstoneTypesModal } from './RedstoneTypesModal'; + +type RedstoneFeedTooltipProps = { + feed: OracleFeed; + redstoneData?: RedstoneOracleEntry; + chainId: number; +}; + +export function RedstoneFeedTooltip({ feed, redstoneData, chainId }: RedstoneFeedTooltipProps) { + const { toggleModal, closeModal } = useGlobalModal(); + const baseAsset = feed.pair?.[0] ?? redstoneData?.path.split('/')[0]?.toUpperCase() ?? 'Unknown'; + const quoteAsset = feed.pair?.[1] ?? redstoneData?.path.split('/')[1]?.toUpperCase() ?? 'Unknown'; + + const vendorIcon = OracleVendorIcons[PriceFeedVendors.Redstone]; + + return ( +
+
+ {/* Header with icon and title */} +
+ {vendorIcon && ( +
+ Redstone +
+ )} +
Redstone Feed Details
+
+ + {/* Feed pair name */} +
+
+ {baseAsset} / {quoteAsset} +
+
+ + {/* Redstone Specific Data */} + {redstoneData && ( +
+
+ Type: +
+ + {redstoneData.fundamental ? 'Fundamental' : 'Standard'} + + +
+
+
+ Heartbeat: + {redstoneData.heartbeat}s +
+
+ Deviation Threshold: + {redstoneData.threshold.toFixed(1)}% +
+
+ )} + + {/* External Links */} +
+
+ View on: +
+
+ + Etherscan + Etherscan + +
+
+
+
+ ); +} diff --git a/src/components/MarketOracle/RedstoneTypesModal.tsx b/src/components/MarketOracle/RedstoneTypesModal.tsx new file mode 100644 index 00000000..0e25ca9e --- /dev/null +++ b/src/components/MarketOracle/RedstoneTypesModal.tsx @@ -0,0 +1,67 @@ +import Image from 'next/image'; +import { MdWarning } from 'react-icons/md'; +import { Modal, ModalHeader, ModalBody } from '@/components/common/Modal'; +import { PriceFeedVendors, OracleVendorIcons } from '@/utils/oracle'; + +type RedstoneTypesModalProps = { + isOpen: boolean; + onClose: () => void; +}; + +export function RedstoneTypesModal({ isOpen, onClose }: RedstoneTypesModalProps) { + const redstoneIcon = OracleVendorIcons[PriceFeedVendors.Redstone]; + + return ( + !open && onClose()} + zIndex="base" + size="xl" + > + : undefined} + onClose={onClose} + /> + + +
+ {/* Market Feed (Standard) */} +
+

Market Feed (Standard)

+

+ Tracks market trading prices denominated in USD or major currencies (e.g., BTC/USD, + ETH/BTC). Reflects real-time market movements with simple aggregation across + exchanges. +

+
+ + {/* Fundamental Feed */} +
+

Fundamental Feed (Contract Rate)

+

+ Tracks the on-chain exchange rate between an underlying asset and its derivative from + protocol smart contracts, offering less volatile pricing but introducing additional + trust assumptions about the protocol's structure and reserve liquidity. +

+

+ Examples: wstETH/stETH from Lido, weETH/eETH from Ether.fi +

+ + {/* Warning Box */} +
+ +
+

Trust Assumptions

+

+ Relies on protocol smart contract structure and underlying reserve liquidity. +

+
+
+
+
+
+
+ ); +} diff --git a/src/components/providers/ClientProviders.tsx b/src/components/providers/ClientProviders.tsx index 920f5596..08a9c00b 100644 --- a/src/components/providers/ClientProviders.tsx +++ b/src/components/providers/ClientProviders.tsx @@ -1,6 +1,7 @@ 'use client'; import { ReactNode } from 'react'; +import { GlobalModalProvider } from '@/contexts/GlobalModalContext'; import { LiquidationsProvider } from '@/contexts/LiquidationsContext'; import { MarketsProvider } from '@/contexts/MarketsContext'; import { MerklCampaignsProvider } from '@/contexts/MerklCampaignsContext'; @@ -13,14 +14,16 @@ type ClientProvidersProps = { export function ClientProviders({ children }: ClientProvidersProps) { return ( - - - - - {children} - - - - + + + + + + {children} + + + + + ); } diff --git a/src/constants/oracle/redstone-data/README b/src/constants/oracle/redstone-data/README new file mode 100644 index 00000000..33f09c00 --- /dev/null +++ b/src/constants/oracle/redstone-data/README @@ -0,0 +1,20 @@ +This directory contains Redstone oracle feed data fetched from their GitHub repository. + +The data is generated using the `generate:redstone` script which fetches configuration +files from the redstone-oracles-monorepo and transforms them into a clean format. + +Source: https://github.com/redstone-finance/redstone-oracles-monorepo/tree/main/packages/relayer-remote-config/main/relayer-manifests-multi-feed + +Data structure: +- Each network has its own JSON file (e.g., hyperevm.json) +- Each entry contains: + - path: The price feed pair (e.g., "btc/usd", "ebtc/wbtc") + - priceFeedAddress: The contract address for the price feed + - fundamental: Boolean flag for _FUNDAMENTAL feeds + - adapterContract: The adapter contract address + - dataServiceId: The data service identifier + - heartbeat: Update frequency in seconds + - threshold: Deviation percentage trigger + +To update the data: + pnpm generate:redstone diff --git a/src/constants/oracle/redstone-data/arbitrum.json b/src/constants/oracle/redstone-data/arbitrum.json new file mode 100644 index 00000000..634565ed --- /dev/null +++ b/src/constants/oracle/redstone-data/arbitrum.json @@ -0,0 +1,138 @@ +[ + { + "path": "eusd/usd", + "priceFeedAddress": "0xa41107f9259bB835275eaCaAd8048307B80D7c00", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "eth+/eth", + "priceFeedAddress": "0xCfd39de761508A7aCb8C931b959127a1D9d0B3D4", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "usdx/usd", + "priceFeedAddress": "0xb81131B6368b3F0a83af09dB4E39Ac23DA96C2Db", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "susdx_arb/unknown", + "priceFeedAddress": "0x24c8964338Deb5204B096039147B8e8C3AEa42Cc", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "susde_rate_provider/usd", + "priceFeedAddress": "0x3A236F67Fce401D87D7215695235e201966576E4", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "xvs/usd", + "priceFeedAddress": "0xd9a66Ff1D660aD943F48e9c606D09eA672f312E8", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "buidl_arbitrum/unknown", + "priceFeedAddress": "0xa8a94Da411425634e3Ed6C331a32ab4fd774aa43", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "buidl_arbitrum/usd", + "priceFeedAddress": "0x3587a73AA02519335A8a6053a97657BECe0bC2Cc", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "usdc/usd", + "priceFeedAddress": "0x4BAD96DD1C7D541270a0C92e1D4e5f12EEEA7a57", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "usdt/usd", + "priceFeedAddress": "0x3fd49f2146FE0e10c4AE7E3fE04b3d5126385Ac4", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "eth/usd", + "priceFeedAddress": "0x197225B3B017eb9b72Ac356D6B3c267d0c04c57c", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "btc/usd", + "priceFeedAddress": "0xbbF121624c3b85C929Ac83872bf6c86b0976A55e", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "arb/usd", + "priceFeedAddress": "0x09639692CE6ff12A06CA3AE9A24b3Aae4CD80DC8", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "weeth/eth", + "priceFeedAddress": "0x83c6f7F61A55Fc7A1337AbD45733AD9c1c68076D", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "wsteth/steth", + "priceFeedAddress": "0x3401DAF2b1f150Ef0c709Cc0283b5F2e55c3DF29", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "thbill/usd", + "priceFeedAddress": "0x6A1c87d11dDe3D1d52c24f8EC59B91019f14170D", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "susde/usde", + "priceFeedAddress": "0x67F6838e58859d612E4ddF04dA396d6DABB66Dc4", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + } +] \ No newline at end of file diff --git a/src/constants/oracle/redstone-data/base.json b/src/constants/oracle/redstone-data/base.json new file mode 100644 index 00000000..e32fa81e --- /dev/null +++ b/src/constants/oracle/redstone-data/base.json @@ -0,0 +1,146 @@ +[ + { + "path": "susdz/unknown", + "priceFeedAddress": "0x24c8964338Deb5204B096039147B8e8C3AEa42Cc", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "puffer/usd", + "priceFeedAddress": "0x85C4F855Bc0609D2584405819EdAEa3aDAbfE97D", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "lbtc/btc", + "priceFeedAddress": "0x5C4c8d6f6Bf79B718F3e8399AaBdFEd01cB7e48f", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "fbomb/usd", + "priceFeedAddress": "0xFB1267A29C0aa19daae4a483ea895862A69e4AA5", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "xvs/usd", + "priceFeedAddress": "0x5ED849a45B4608952161f45483F4B95BCEa7f8f0", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "aero/usd", + "priceFeedAddress": "0xBf3bA2b090188B40eF83145Be0e9F30C6ca63689", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 43200, + "threshold": 0.25 + }, + { + "path": "usd+/usd", + "priceFeedAddress": "0xd9a66Ff1D660aD943F48e9c606D09eA672f312E8", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.2 + }, + { + "path": "bio/usd", + "priceFeedAddress": "0x13433B1949d9141Be52Ae13Ad7e7E4911228414e", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "usr/usd", + "priceFeedAddress": "0x4BAD96DD1C7D541270a0C92e1D4e5f12EEEA7a57", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.2 + }, + { + "path": "kaito/usd", + "priceFeedAddress": "0x3fd49f2146FE0e10c4AE7E3fE04b3d5126385Ac4", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "prime_echelon/usd", + "priceFeedAddress": "0x197225B3B017eb9b72Ac356D6B3c267d0c04c57c", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "prompt/usd", + "priceFeedAddress": "0xbbF121624c3b85C929Ac83872bf6c86b0976A55e", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "susdx_bnb/unknown", + "priceFeedAddress": "0x09639692CE6ff12A06CA3AE9A24b3Aae4CD80DC8", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "thbill/usd", + "priceFeedAddress": "0x83c6f7F61A55Fc7A1337AbD45733AD9c1c68076D", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "uty/unknown", + "priceFeedAddress": "0x3401DAF2b1f150Ef0c709Cc0283b5F2e55c3DF29", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "yusd/unknown", + "priceFeedAddress": "0x6A1c87d11dDe3D1d52c24f8EC59B91019f14170D", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 43200, + "threshold": 0.25 + }, + { + "path": "syusd/unknown", + "priceFeedAddress": "0x4154f0e4dc70DFA4219309fBea34322225E17b68", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 43200, + "threshold": 0.25 + }, + { + "path": "uty_pegged/unknown", + "priceFeedAddress": "0x56b13Aa2f3b9fD2a136Ab46fc570E3c549191d71", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + } +] \ No newline at end of file diff --git a/src/constants/oracle/redstone-data/hyperevm.json b/src/constants/oracle/redstone-data/hyperevm.json new file mode 100644 index 00000000..80f4c941 --- /dev/null +++ b/src/constants/oracle/redstone-data/hyperevm.json @@ -0,0 +1,338 @@ +[ + { + "path": "hype/usd", + "priceFeedAddress": "0xa8a94Da411425634e3Ed6C331a32ab4fd774aa43", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "btc/usd", + "priceFeedAddress": "0x3587a73AA02519335A8a6053a97657BECe0bC2Cc", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "eth/usd", + "priceFeedAddress": "0x4BAD96DD1C7D541270a0C92e1D4e5f12EEEA7a57", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "sol/usd", + "priceFeedAddress": "0x3fd49f2146FE0e10c4AE7E3fE04b3d5126385Ac4", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "wsteth/eth", + "priceFeedAddress": "0x197225B3B017eb9b72Ac356D6B3c267d0c04c57c", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "weeth/eth", + "priceFeedAddress": "0xbbF121624c3b85C929Ac83872bf6c86b0976A55e", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "sweth/unknown", + "priceFeedAddress": "0x09639692CE6ff12A06CA3AE9A24b3Aae4CD80DC8", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "stone/usd", + "priceFeedAddress": "0x83c6f7F61A55Fc7A1337AbD45733AD9c1c68076D", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "rsweth/unknown", + "priceFeedAddress": "0x3401DAF2b1f150Ef0c709Cc0283b5F2e55c3DF29", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "ezeth/eth", + "priceFeedAddress": "0x6A1c87d11dDe3D1d52c24f8EC59B91019f14170D", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "rseth/unknown", + "priceFeedAddress": "0x84AD474c33c9cCefB1a2D8b77Bdd88bDc592f96b", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "pufeth/eth", + "priceFeedAddress": "0xe7f71d6a24EBc391f5ee57B867ED429EB7Bd74f4", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "lbtc/btc", + "priceFeedAddress": "0x65eD6a4ac085620eE943c0B15525C4428D23e4Db", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "solvbtc/usd", + "priceFeedAddress": "0x24eDD61cdA334bFf871A80DEB135073a7d7a9187", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "wbtc/usd", + "priceFeedAddress": "0xddE3b77040cee3387C0cA661d1b619C3acA203b0", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "ebtc/wbtc", + "priceFeedAddress": "0x8B4736f5eaD8ed579Ecf65a13F9c1E8B44dEdF20", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "usdc/usd", + "priceFeedAddress": "0x4C89968338b75551243C99B452c84a01888282fD", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "usdt/usd", + "priceFeedAddress": "0x5e21f6530f656A38caE4F55500944753F662D184", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "usde/usd", + "priceFeedAddress": "0xcA727511c9d542AAb9eF406d24E5bbbE4567c22d", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "susde/usde", + "priceFeedAddress": "0x29D295409d5A20b2C851df18054D32A442791346", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "usr/usd", + "priceFeedAddress": "0x29d2fEC890B037B2d34f061F9a50f76F85ddBcAE", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "cbbtc/usd", + "priceFeedAddress": "0xC9e11c60e24BEF478cC999fA9fA2d89cC098A86e", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "cmeth/unknown", + "priceFeedAddress": "0x5C4c8d6f6Bf79B718F3e8399AaBdFEd01cB7e48f", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "susde/usd", + "priceFeedAddress": "0x243507C8C114618d7C8AD94b51118dB7b4e32ECe", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "ubtc/usd", + "priceFeedAddress": "0x7d05cd5159F38694A7D4dBf58957146a63c8Ad5A", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "mhype/hype", + "priceFeedAddress": "0xa42a6568f1df29ef95DDDF440c41e48D4cfB310E", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "sthype/hype", + "priceFeedAddress": "0xEC34D1Cf550dda751ff20cD4eCC7FF9219551B04", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "hwhlp/unknown", + "priceFeedAddress": "0xC328CDf06CBc77134B84e1f6ed452774947146b6", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "usr/unknown", + "priceFeedAddress": "0xa275809f06944c00E308FE764b0559ED84481042", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "xau/usd", + "priceFeedAddress": "0xB5d303b9F984e42EB5E3C00Bdf733A309c654630", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "xaut/usd", + "priceFeedAddress": "0xA3A75Fd9f19bd334605f59527552DBC6c7f6fD88", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "sthype/usd", + "priceFeedAddress": "0x4cEC96A68cb9A979621b104F3C94884be1a66da0", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "hbusdt/usdt", + "priceFeedAddress": "0x96572d32d699cE463Fdf36610273CC76B7d83f9b", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "hbhype/hype", + "priceFeedAddress": "0xDb924A25BfF353f98B066F692c38C3cFacb3a601", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "hbbtc/btc", + "priceFeedAddress": "0x9ED559c2Ad1562aE8e919691A84A3320f547B248", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "khype/hype", + "priceFeedAddress": "0xFfe5F5e9e18b88FBdD7e28d4A583a111C874fB47", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "khype_fundamental/usd", + "priceFeedAddress": "0x82721e2C5Ef2DF1796B09728376361892b155594", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "rlp/unknown", + "priceFeedAddress": "0xd6156F8177aA1a6E0c5278CE437A9BDB32F203ef", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "lsthype/hype", + "priceFeedAddress": "0xA569E68B5D110F2A255482c2997DFDBe1b2ab912", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "khype/usd", + "priceFeedAddress": "0xF2448DC04B1d3f1767D6f7C03da8a3933bdDD697", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "khype/hype", + "priceFeedAddress": "0x3519B2f175D22a4dFA0595c291fEfe0945F0656d", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "behype/unknown", + "priceFeedAddress": "0xBE4d4D2FDdE7408bD00B9912705De7bDC3F9bDeb", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + } +] \ No newline at end of file diff --git a/src/constants/oracle/redstone-data/index.ts b/src/constants/oracle/redstone-data/index.ts new file mode 100644 index 00000000..e2fe6f00 --- /dev/null +++ b/src/constants/oracle/redstone-data/index.ts @@ -0,0 +1,51 @@ +import { isSupportedChain, SupportedNetworks } from '@/utils/networks'; +import arbitrumRawData from './arbitrum.json'; +import baseRawData from './base.json'; +import hyperevmRawData from './hyperevm.json'; +import mainnetRawData from './mainnet.json'; +import monadRawData from './monad.json'; +import polygonRawData from './polygon.json'; +import { RedstoneOracleEntry } from './types'; + +export const REDSTONE_ORACLES = { + [SupportedNetworks.Mainnet]: mainnetRawData as RedstoneOracleEntry[], + [SupportedNetworks.Base]: baseRawData as RedstoneOracleEntry[], + [SupportedNetworks.Polygon]: polygonRawData as RedstoneOracleEntry[], + [SupportedNetworks.Arbitrum]: arbitrumRawData as RedstoneOracleEntry[], + [SupportedNetworks.Unichain]: [] as RedstoneOracleEntry[], + [SupportedNetworks.HyperEVM]: hyperevmRawData as RedstoneOracleEntry[], + [SupportedNetworks.Monad]: monadRawData as RedstoneOracleEntry[], +} as const; + +export const getAllRedstoneOracles = (): Record< + SupportedNetworks, + RedstoneOracleEntry[] +> => REDSTONE_ORACLES; + +export const getRedstoneOracleByPath = ( + chain: keyof typeof REDSTONE_ORACLES, + path: string, +): RedstoneOracleEntry | undefined => { + return REDSTONE_ORACLES[chain].find((oracle) => oracle.path === path); +}; + +export const isRedstoneOracle = (chainId: number, address: string): boolean => { + if (!isSupportedChain(chainId) || !address) return false; + const network = chainId as SupportedNetworks; + return REDSTONE_ORACLES[network].some( + (oracle) => oracle.priceFeedAddress.toLowerCase() === address.toLowerCase(), + ); +}; + +export const getRedstoneOracle = ( + chainId: number, + address: string, +): RedstoneOracleEntry | undefined => { + if (!isSupportedChain(chainId) || !address) return undefined; + const network = chainId as SupportedNetworks; + return REDSTONE_ORACLES[network].find( + (oracle) => oracle.priceFeedAddress.toLowerCase() === address.toLowerCase(), + ); +}; + +export * from './types'; diff --git a/src/constants/oracle/redstone-data/mainnet.json b/src/constants/oracle/redstone-data/mainnet.json new file mode 100644 index 00000000..74dd2e39 --- /dev/null +++ b/src/constants/oracle/redstone-data/mainnet.json @@ -0,0 +1,546 @@ +[ + { + "path": "lbtc/btc", + "priceFeedAddress": "0xb415eAA355D8440ac7eCB602D3fb67ccC1f0bc81", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "pzeth/eth", + "priceFeedAddress": "0x0B6c5C37215B27C944497D2d7011cBD366b0870A", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "pzeth/unknown", + "priceFeedAddress": "0x0d7697a15bce933cE8671Ba3D60ab062dA216C60", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "usd3/usd", + "priceFeedAddress": "0xB39339B82DdCF89d12d987d1D4Db33aFdd40B6AA", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "deusd/usd", + "priceFeedAddress": "0x89F48f6671Ec1B1C4f6abE964EBdd21F4eb7076f", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 43200, + "threshold": 0.1 + }, + { + "path": "ebtc/wbtc", + "priceFeedAddress": "0xe5867B1d421f0b52697F16e2ac437e87d66D5fbF", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "cesr/usd", + "priceFeedAddress": "0x2C787Db499BD7BF1f592D8DDd79ead9a72420763", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.000001 + }, + { + "path": "solvbtc/usd", + "priceFeedAddress": "0x24c8964338Deb5204B096039147B8e8C3AEa42Cc", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "pumpbtc/btc", + "priceFeedAddress": "0x13433B1949d9141Be52Ae13Ad7e7E4911228414e", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "pumpbtc/unknown", + "priceFeedAddress": "0x85C4F855Bc0609D2584405819EdAEa3aDAbfE97D", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "fxusd/usd", + "priceFeedAddress": "0x4BAD96DD1C7D541270a0C92e1D4e5f12EEEA7a57", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.2 + }, + { + "path": "solvbtc/btc", + "priceFeedAddress": "0x3fd49f2146FE0e10c4AE7E3fE04b3d5126385Ac4", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "puffer/usd", + "priceFeedAddress": "0x197225B3B017eb9b72Ac356D6B3c267d0c04c57c", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "c3m/usd", + "priceFeedAddress": "0x6E27A25999B3C665E44D903B2139F5a4Be2B6C26", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "ethx/eth", + "priceFeedAddress": "0xc799194cAa24E2874Efa89b4Bf5c92a530B047FF", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "eusd/usd", + "priceFeedAddress": "0xb347d2e3524D0F9e2321D84A2E9b2e60CbC4A836", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "oseth/eth", + "priceFeedAddress": "0x66ac817f997Efd114EDFcccdce99F3268557B32C", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "sfrxeth/eth", + "priceFeedAddress": "0xdd60c54115C19e0c6360AD4762B88BB8076D50a8", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "susde/usd", + "priceFeedAddress": "0xb99D174ED06c83588Af997c8859F93E83dD4733f", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.2 + }, + { + "path": "usde/usd", + "priceFeedAddress": "0xbC5FBcf58CeAEa19D523aBc76515b9AEFb5cfd58", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.2 + }, + { + "path": "usdx/usd", + "priceFeedAddress": "0x6205DCc41329fb5dE3FB01e272eD49F8771cC715", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "susdx/unknown", + "priceFeedAddress": "0x5708d924Fc996EDee46962CdB6815f90639974e1", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "berastone/unknown", + "priceFeedAddress": "0x4f67Fd74cff274Ef2942223c0f3166b856410AdD", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "swell/eth", + "priceFeedAddress": "0xf2d31a29c7BCc992a62D297489dBBfff5F39BE15", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "ethx/usd", + "priceFeedAddress": "0xFaBEb1474C2Ab34838081BFdDcE4132f640E7D2d", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "xvs/usd", + "priceFeedAddress": "0xa2a8507DEb233ceE4F5594044C259DD0582339CC", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "eth/usd", + "priceFeedAddress": "0x67F6838e58859d612E4ddF04dA396d6DABB66Dc4", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "btc/usd", + "priceFeedAddress": "0xAB7f623fb2F6fea6601D4350FA0E2290663C28Fc", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "bio/usd", + "priceFeedAddress": "0xeC7C6AdcC867E1C22713D14797339750E36538E4", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "buidl/unknown", + "priceFeedAddress": "0xb9BD795BB71012c0F3cd1D9c9A4c686F2d3524A4", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "deusd/unknown", + "priceFeedAddress": "0xcA727511c9d542AAb9eF406d24E5bbbE4567c22d", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.2 + }, + { + "path": "egeth/unknown", + "priceFeedAddress": "0x4914B59AB7f030d605c336CDc3f7470a2e16A53b", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "usr/usd", + "priceFeedAddress": "0x107Dd3391A6357248f2093698014e7c6130779Ee", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.2 + }, + { + "path": "bfbtc/unknown", + "priceFeedAddress": "0xe0d290eBA865A6CA14c1C9bFD9CD839FA8d54543", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "hlscope/unknown", + "priceFeedAddress": "0x1f14a50bA904A28CF6088e71B6a15561074398d7", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "acred/unknown", + "priceFeedAddress": "0xD6BcbbC87bFb6c8964dDc73DC3EaE6d08865d51C", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "usual/usd", + "priceFeedAddress": "0x2240AE461B34CC56D654ba5FA5830A243Ca54840", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "usdtb/unknown", + "priceFeedAddress": "0x692F045EFaC629a4d3C54DE4d8dDb1D225b5A976", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "prime_echelon/usd", + "priceFeedAddress": "0xC328CDf06CBc77134B84e1f6ed452774947146b6", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "prompt/usd", + "priceFeedAddress": "0x25a23C034fbC1eea341F6c92C6D8e6baFCF5DA40", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "eigen/usd", + "priceFeedAddress": "0x2ee5Ce6556599E16c226579BA14F94926d8Cb86d", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "taceth/unknown", + "priceFeedAddress": "0x9D0aB80d6B68144a6BBb5c7447Eb84EdaaC18519", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "tacusd/unknown", + "priceFeedAddress": "0xa90E0136cB598E1cEf28dc54932C17033B54538C", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "tacbtc/unknown", + "priceFeedAddress": "0x0A6FD5820f797e517297fEFf90AaEcD8e2Dc77b3", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "sweth/unknown", + "priceFeedAddress": "0x061bB36F8b67bB922937C102092498dcF4619F86", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "buidl/usd", + "priceFeedAddress": "0x9c303927dC4939692CC60641D6ceABa9185d687a", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "buidl_i/unknown", + "priceFeedAddress": "0xf2db7b3455077Fb177215d45D62d441DF3C17bf3", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "buidl_i/usd", + "priceFeedAddress": "0xd6156F8177aA1a6E0c5278CE437A9BDB32F203ef", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "vbill/unknown", + "priceFeedAddress": "0xA569E68B5D110F2A255482c2997DFDBe1b2ab912", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "spk/usd", + "priceFeedAddress": "0xF2448DC04B1d3f1767D6f7C03da8a3933bdDD697", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "hwhlp/unknown", + "priceFeedAddress": "0x03138081aeD44E2E0Eb10361Ee41D84EDD22a05f", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "usdc_v2/usd", + "priceFeedAddress": "0xeeF31c7d9F2E82e8A497b140cc60cc082Be4b94e", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.05 + }, + { + "path": "usdt_v2/usd", + "priceFeedAddress": "0x02E1F8d15762047b7a87BA0E5d94B9a0c5b54Ed2", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.05 + }, + { + "path": "pyusd/usd", + "priceFeedAddress": "0xcE18A2Bf89Fa3c56aF5Bde8A41efF967a6d63d26", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.05 + }, + { + "path": "cusd/unknown", + "priceFeedAddress": "0x9A5a3c3Ed0361505cC1D4e824B3854De5724434A", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.05 + }, + { + "path": "thbill/usd", + "priceFeedAddress": "0x80B0735616b27b647233D3Ab67078C95BA5A2c93", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "vbill/usd", + "priceFeedAddress": "0x5cC480aeCAd8F52ebd25b9B427737e401E47e8B0", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "wlfi/usd", + "priceFeedAddress": "0x131141E6c88a3389A4Ab2368D3bbC65df1BF4a7E", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "wsteth/usd", + "priceFeedAddress": "0xe4aE88743c3834d0c492eAbC47384c84BcADC6a6", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "wsteth/steth", + "priceFeedAddress": "0xa7B0247d2dA6B11FF2740491cB433a1520d5DA98", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 21600, + "threshold": 0.5 + }, + { + "path": "yusd/unknown", + "priceFeedAddress": "0x2e65b1a44FDC30976d7e2397D16c425A8A7Aa4F6", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 43200, + "threshold": 0.25 + }, + { + "path": "rsweth/unknown", + "priceFeedAddress": "0x3A236F67Fce401D87D7215695235e201966576E4", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 1 + }, + { + "path": "syusd/unknown", + "priceFeedAddress": "0xab6982a14e811DF558F49C79923723281D0DD16A", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 43200, + "threshold": 0.25 + }, + { + "path": "stac/unknown", + "priceFeedAddress": "0xEdC6287D3D41b322AF600317628D7E226DD3add4", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "ibenji/unknown", + "priceFeedAddress": "0x009119Cd7eB8912863c30362CfdCe0B2F8a52D6C", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "sierra/unknown", + "priceFeedAddress": "0x9269127F104C040AB526575573c23F3e67401aD9", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "apr/usd", + "priceFeedAddress": "0xE5c1A89887C572c3C345EFE2BBBe797AC5B22461", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "pt_srusde_15jan2026_eth-twap/usd", + "priceFeedAddress": "0x1102D8C7A6021e45cCddEC4912dc998Bc5ebD8e5", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + } +] \ No newline at end of file diff --git a/src/constants/oracle/redstone-data/monad.json b/src/constants/oracle/redstone-data/monad.json new file mode 100644 index 00000000..7133a1ba --- /dev/null +++ b/src/constants/oracle/redstone-data/monad.json @@ -0,0 +1,194 @@ +[ + { + "path": "eth/usd", + "priceFeedAddress": "0xc44be6D00307c3565FDf753e852Fc003036cBc13", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 0, + "threshold": 0.5 + }, + { + "path": "btc/usd", + "priceFeedAddress": "0xED2B1ca5D7E246f615c2291De309643D41FeC97e", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 0, + "threshold": 0.5 + }, + { + "path": "mon/usd", + "priceFeedAddress": "0x1C9582E87eD6E99bc23EC0e6Eb52eE9d7C0D6bcd", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 0, + "threshold": 0.5 + }, + { + "path": "usdc/usd", + "priceFeedAddress": "0x7A9b672fc20b5C89D6774514052b3e0899E5E263", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 3600, + "threshold": 0.05 + }, + { + "path": "usdt/usd", + "priceFeedAddress": "0x90196F6D52fce394C79D1614265d36D3F0033Ccf", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 3600, + "threshold": 0.05 + }, + { + "path": "dai/usd", + "priceFeedAddress": "0x1b0FDa12D125B864756Bbf191ad20eaB10915a6F", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 3600, + "threshold": 0.05 + }, + { + "path": "sol/usd", + "priceFeedAddress": "0x0bE6929FD4ad87347e97A525DB6ac8E884FCDCeC", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 0, + "threshold": 0.5 + }, + { + "path": "wbtc/usd", + "priceFeedAddress": "0x98ECE0D516f891a35278E3186772fb1545b274eB", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "cbbtc/usd", + "priceFeedAddress": "0x7A532B1B204409472cB92cd07c8B0BB09DD2F520", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "steth/usd", + "priceFeedAddress": "0xD3A0C347b07Fd45F41270D557089B389Fa735C3d", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "wsteth/eth", + "priceFeedAddress": "0xC0FC9416b5c3737E13507f4bAD58cFc016779C84", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "weeth/eth", + "priceFeedAddress": "0xB3C5BE567817F127412d1758048b376D4D35ec51", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "ezeth/eth", + "priceFeedAddress": "0x6e7407fcd5021e3FE5f75959575b20C85231562d", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "ausd/usd", + "priceFeedAddress": "0xFFD1339908E0deBE2416E03df0843B896b8944Fe", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "shmon/unknown", + "priceFeedAddress": "0xAd1A270a3F7FF685B90445d9da3EE7Eb22F8A1Ec", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 43200, + "threshold": 0.05 + }, + { + "path": "apr/usd", + "priceFeedAddress": "0xB144836c17C5d613Aa471f0BB9B79B55288c2C03", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "aprmon/unknown", + "priceFeedAddress": "0x096073133355F874A7D0a857Ffac314dda4e0551", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 43200, + "threshold": 0.5 + }, + { + "path": "yzusd/unknown", + "priceFeedAddress": "0x5577797e21FBbB7458f500fa80770D80AAE39bca", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.05 + }, + { + "path": "syzusd_fundamental/usd", + "priceFeedAddress": "0xa79F7363FCa5118B4141eCeff6dd2A83d5047344", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.05 + }, + { + "path": "smon/unknown", + "priceFeedAddress": "0xE77456457619ad1948336FBaBC3883cB965b50D1", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 43200, + "threshold": 0.05 + }, + { + "path": "mubond/unknown", + "priceFeedAddress": "0x61669df14B681010A5cfaefc4688Cf9e079ddE5D", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "aznd/unknown", + "priceFeedAddress": "0x63Bb491346fCC8695244D811F0c3501E6C2e8d25", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "earnausd/unknown", + "priceFeedAddress": "0xD1136a8C1E5a8Ff204d6875b51158b5Ef1858f60", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "gmon/unknown", + "priceFeedAddress": "0x8C9f39f0D08EE284a4Fe0198524fE7C28630CEAb", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 43200, + "threshold": 0.5 + } +] \ No newline at end of file diff --git a/src/constants/oracle/redstone-data/polygon.json b/src/constants/oracle/redstone-data/polygon.json new file mode 100644 index 00000000..4a519d0f --- /dev/null +++ b/src/constants/oracle/redstone-data/polygon.json @@ -0,0 +1,34 @@ +[ + { + "path": "hlscope/unknown", + "priceFeedAddress": "0x780fe28dBac08eBa781de833A5E860C86D524251", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.5 + }, + { + "path": "acred/unknown", + "priceFeedAddress": "0xBF4Fa84B5433660a4194B5FbaF41c3d11B47ed77", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "buidl_polygon/unknown", + "priceFeedAddress": "0xb81131B6368b3F0a83af09dB4E39Ac23DA96C2Db", + "fundamental": true, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + }, + { + "path": "buidl_polygon/usd", + "priceFeedAddress": "0x24c8964338Deb5204B096039147B8e8C3AEa42Cc", + "fundamental": false, + "dataServiceId": "redstone-primary-prod", + "heartbeat": 86400, + "threshold": 0.1 + } +] \ No newline at end of file diff --git a/src/constants/oracle/redstone-data/types.ts b/src/constants/oracle/redstone-data/types.ts new file mode 100644 index 00000000..dd7ebc6a --- /dev/null +++ b/src/constants/oracle/redstone-data/types.ts @@ -0,0 +1,8 @@ +export type RedstoneOracleEntry = { + path: string; + priceFeedAddress: string; + fundamental: boolean; + dataServiceId: string; + heartbeat: number; + threshold: number; +}; diff --git a/src/contexts/GlobalModalContext.tsx b/src/contexts/GlobalModalContext.tsx new file mode 100644 index 00000000..8d1bc5c8 --- /dev/null +++ b/src/contexts/GlobalModalContext.tsx @@ -0,0 +1,52 @@ +import { createContext, useContext, useState, useMemo, useCallback, ReactNode } from 'react'; + +type ModalContent = Exclude>; + +type ModalContextType = { + openModal: (content: ModalContent) => void; + closeModal: () => void; + toggleModal: (content: ModalContent) => void; + isOpen: boolean; +}; + +const GlobalModalContext = createContext(undefined); + +export function GlobalModalProvider({ children }: { children: ReactNode }) { + const [modalContent, setModalContent] = useState(null); + + const openModal = useCallback((content: ModalContent) => { + setModalContent(content); + }, []); + + const closeModal = useCallback(() => { + setModalContent(null); + }, []); + + const toggleModal = useCallback((content: ModalContent) => { + // If any modal is currently open, close it + // Otherwise, open the new content + setModalContent((current) => (current ? null : content)); + }, []); + + const value = useMemo( + () => ({ openModal, closeModal, toggleModal, isOpen: !!modalContent }), + [openModal, closeModal, toggleModal, modalContent] + ); + + return ( + + {children} + + {/* Render whatever modal content was passed */} + {modalContent} + + ); +} + +export function useGlobalModal() { + const context = useContext(GlobalModalContext); + if (!context) { + throw new Error('useGlobalModal must be used within GlobalModalProvider'); + } + return context; +} diff --git a/src/utils/oracle.ts b/src/utils/oracle.ts index d177b43a..83abe476 100644 --- a/src/utils/oracle.ts +++ b/src/utils/oracle.ts @@ -6,6 +6,11 @@ import { } from '@/constants/oracle/chainlink-data'; import { getCompoundFeed, CompoundFeedEntry, isCompoundFeed } from '@/constants/oracle/compound'; import { getGeneralFeed, isGeneralFeed, GeneralPriceFeed } from '@/constants/oracle/general-feeds'; +import { + getRedstoneOracle, + RedstoneOracleEntry, + isRedstoneOracle, +} from '@/constants/oracle/redstone-data'; import { isSupportedChain } from './networks'; import { MorphoChainlinkOracleData, OracleFeed } from './types'; @@ -69,12 +74,17 @@ export type CompoundFeedResult = { }; }; +export type RedstoneFeedResult = { + vendor: PriceFeedVendors.Redstone; + data: RedstoneOracleEntry; + assetPair: { + baseAsset: string; + quoteAsset: string; + }; +}; + export type GeneralFeedResult = { - vendor: - | PriceFeedVendors.Redstone - | PriceFeedVendors.PythNetwork - | PriceFeedVendors.Oval - | PriceFeedVendors.Lido; + vendor: PriceFeedVendors.PythNetwork | PriceFeedVendors.Oval | PriceFeedVendors.Lido; data: GeneralPriceFeed; assetPair: { baseAsset: string; @@ -95,6 +105,7 @@ export type UnknownFeedResult = { export type FeedVendorResult = | ChainlinkFeedResult | CompoundFeedResult + | RedstoneFeedResult | GeneralFeedResult | UnknownFeedResult; @@ -137,6 +148,23 @@ export function detectFeedVendor(feedAddress: Address | string, chainId: number) } } + // Check if it's a Redstone feed + if (isRedstoneOracle(chainId, address)) { + const redstoneData = getRedstoneOracle(chainId, address); + if (redstoneData) { + // Parse path to get base and quote assets (e.g., "btc/usd" -> ["btc", "usd"]) + const [baseAsset, quoteAsset] = redstoneData.path.split('/').map((s) => s.toUpperCase()); + return { + vendor: PriceFeedVendors.Redstone, + data: redstoneData, + assetPair: { + baseAsset: baseAsset ?? 'Unknown', + quoteAsset: quoteAsset ?? 'Unknown', + }, + } satisfies RedstoneFeedResult; + } + } + // Check if it's a general price feed (from various vendors via Morpho's API data) if (isGeneralFeed(address, chainId)) { const generalFeedData = getGeneralFeed(address, chainId); @@ -145,16 +173,7 @@ export function detectFeedVendor(feedAddress: Address | string, chainId: number) const vendorName = generalFeedData.vendor.toLowerCase(); // Return proper discriminated union based on vendor - if (vendorName === 'redstone') { - return { - vendor: PriceFeedVendors.Redstone, - data: generalFeedData, - assetPair: { - baseAsset: generalFeedData.pair[0], - quoteAsset: generalFeedData.pair[1], - }, - } satisfies GeneralFeedResult; - } + // Note: Redstone is now handled separately above with our own data if (vendorName === 'pyth network' || vendorName === 'pyth') { return {