Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,4 @@ MONARCH_API_KEY=
# ==================== Oracle Metadata ====================
# Base URL for oracle metadata Gist (without trailing slash)
# Example: https://gist.githubusercontent.com/username/gist-id/raw
ORACLE_GIST_BASE_URL=
NEXT_PUBLIC_ORACLE_GIST_BASE_URL=
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ Always consult these docs for detailed information:
4. Do not claim repo facts without evidence (no invented counts).
5. Prevent double-capture, noisy heuristics, or duplicate logic.

## Post-Implementation Consolidation (Mandatory)

Before closing any non-trivial change:

1. Run one consolidation pass and remove duplicated logic across files (especially repeated UI blocks).
2. Prefer one chokepoint fix for layout constraints (container-level width/spacing) over per-component ad hoc truncation.
3. Re-check first principles against the domain model so behavior applies consistently to all valid entities (not vendor-specific shortcuts).
4. Remove transitional code that was useful during debugging but adds long-term complexity.

---

## 🛠️ Skills System
Expand Down
46 changes: 0 additions & 46 deletions app/api/oracle-metadata/[chainId]/route.ts

This file was deleted.

24 changes: 24 additions & 0 deletions src/abis/chainlink-aggregator-v3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Abi } from 'viem';

export const chainlinkAggregatorV3Abi = [
{
inputs: [],
name: 'latestRoundData',
outputs: [
{ internalType: 'uint80', name: 'roundId', type: 'uint80' },
{ internalType: 'int256', name: 'answer', type: 'int256' },
{ internalType: 'uint256', name: 'startedAt', type: 'uint256' },
{ internalType: 'uint256', name: 'updatedAt', type: 'uint256' },
{ internalType: 'uint80', name: 'answeredInRound', type: 'uint80' },
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'decimals',
outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
stateMutability: 'view',
type: 'function',
},
] as const satisfies Abi;
5 changes: 2 additions & 3 deletions src/components/providers/QueryProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type QueryProviderProps = {
const ACTIONABLE_QUERY_ROOT_KEYS = new Set<string>([
'all-position-snapshots',
'enhanced-positions',
'feed-snapshot',
'fresh-markets-state',
'historicalSupplierPositions',
'marketData',
Expand All @@ -37,9 +38,7 @@ const ACTIONABLE_QUERY_ROOT_KEYS = new Set<string>([
'vault-allocations',
]);

const TRANSACTION_MUTATION_ROOT_KEYS = new Set<string>([
'sendTransaction',
]);
const TRANSACTION_MUTATION_ROOT_KEYS = new Set<string>(['sendTransaction']);

const getQueryRootKey = (queryKey: QueryKey): string => {
const root = queryKey[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import { Badge } from '@/components/ui/badge';
import { useGlobalModal } from '@/contexts/GlobalModalContext';
import etherscanLogo from '@/imgs/etherscan.png';
import { getExplorerURL } from '@/utils/external';
import { PriceFeedVendors, OracleVendorIcons, getChainlinkFeedUrl, type FeedData } from '@/utils/oracle';
import { getChainlinkFeedUrl, OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle';
import type { OracleFeed } from '@/utils/types';
import { ChainlinkRiskTiersModal } from './ChainlinkRiskTiersModal';
import { FeedFreshnessSection } from './FeedFreshnessSection';

type ChainlinkFeedTooltipProps = {
feed: OracleFeed;
feedData?: FeedData | null;
chainId: number;
feedFreshness?: FeedFreshnessStatus;
};

function getRiskTierBadge(category: string) {
Expand All @@ -36,7 +38,7 @@ function getRiskTierBadge(category: string) {
);
}

export function ChainlinkFeedTooltip({ feed, feedData, chainId }: ChainlinkFeedTooltipProps) {
export function ChainlinkFeedTooltip({ feed, feedData, chainId, feedFreshness }: ChainlinkFeedTooltipProps) {
const { toggleModal, closeModal } = useGlobalModal();
const baseAsset = feed.pair?.[0] ?? feedData?.pair[0] ?? 'Unknown';
const quoteAsset = feed.pair?.[1] ?? feedData?.pair[1] ?? 'Unknown';
Expand All @@ -48,7 +50,7 @@ export function ChainlinkFeedTooltip({ feed, feedData, chainId }: ChainlinkFeedT
const hasDetails = feedData?.heartbeat != null || feedData?.tier != null || feedData?.deviationThreshold != null;

return (
<div className="flex max-w-xs flex-col gap-3">
<div className="flex w-fit max-w-[22rem] flex-col gap-3">
{/* Header with icon and title */}
<div className="flex items-center gap-2">
{vendorIcon && (
Expand Down Expand Up @@ -113,6 +115,7 @@ export function ChainlinkFeedTooltip({ feed, feedData, chainId }: ChainlinkFeedT
)}
</div>
)}
<FeedFreshnessSection feedFreshness={feedFreshness} />

{/* External Links */}
<div className="border-t border-gray-200/30 pt-3 dark:border-gray-600/20">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@ import Link from 'next/link';
import type { Address } from 'viem';
import etherscanLogo from '@/imgs/etherscan.png';
import { getExplorerURL } from '@/utils/external';
import { PriceFeedVendors, OracleVendorIcons, type FeedData } from '@/utils/oracle';
import { OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle';
import type { OracleFeed } from '@/utils/types';
import { FeedFreshnessSection } from './FeedFreshnessSection';

type CompoundFeedTooltipProps = {
feed: OracleFeed;
feedData?: FeedData | null;
chainId: number;
feedFreshness?: FeedFreshnessStatus;
};

export function CompoundFeedTooltip({ feed, feedData, chainId }: CompoundFeedTooltipProps) {
export function CompoundFeedTooltip({ feed, feedData, chainId, feedFreshness }: CompoundFeedTooltipProps) {
const baseAsset = feed.pair?.[0] ?? feedData?.pair[0] ?? 'Unknown';
const quoteAsset = feed.pair?.[1] ?? feedData?.pair[1] ?? 'Unknown';

const compoundLogo = OracleVendorIcons[PriceFeedVendors.Compound];

return (
<div className="flex max-w-xs flex-col gap-3">
<div className="flex w-fit max-w-[22rem] flex-col gap-3">
{/* Header with icon and title */}
<div className="flex items-center gap-2">
{compoundLogo && (
Expand Down Expand Up @@ -49,6 +51,8 @@ export function CompoundFeedTooltip({ feed, feedData, chainId }: CompoundFeedToo
</div>
)}

<FeedFreshnessSection feedFreshness={feedFreshness} />

{/* External Links */}
<div className="border-t border-gray-200/30 pt-3 dark:border-gray-600/20">
<div className="mb-2 font-zen text-sm font-medium text-gray-700 dark:text-gray-300">View on:</div>
Expand Down
44 changes: 36 additions & 8 deletions src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@ import Image from 'next/image';
import { IoIosSwap } from 'react-icons/io';
import { IoHelpCircleOutline } from 'react-icons/io5';
import type { Address } from 'viem';
import type { FeedSnapshotByAddress } from '@/hooks/useFeedLastUpdatedByChain';
import {
getFeedFromOracleData,
getOracleFromMetadata,
isMetaOracleData,
type EnrichedFeed,
type OracleMetadataRecord,
} from '@/hooks/useOracleMetadata';
import { detectFeedVendor, detectFeedVendorFromMetadata, getTruncatedAssetName, PriceFeedVendors, OracleVendorIcons } from '@/utils/oracle';
import {
detectFeedVendor,
detectFeedVendorFromMetadata,
getFeedFreshnessStatus,
getTruncatedAssetName,
OracleVendorIcons,
PriceFeedVendors,
} from '@/utils/oracle';
import type { OracleFeed } from '@/utils/types';
import { ChainlinkFeedTooltip } from './ChainlinkFeedTooltip';
import { CompoundFeedTooltip } from './CompoundFeedTooltip';
Expand All @@ -26,9 +34,17 @@ type FeedEntryProps = {
oracleAddress?: string;
oracleMetadataMap?: OracleMetadataRecord;
enrichedFeed?: EnrichedFeed;
feedSnapshotsByAddress?: FeedSnapshotByAddress;
};

export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enrichedFeed }: FeedEntryProps): JSX.Element | null {
export function FeedEntry({
feed,
chainId,
oracleAddress,
oracleMetadataMap,
enrichedFeed,
feedSnapshotsByAddress,
}: FeedEntryProps): JSX.Element | null {
// Use metadata-based detection when available, fallback to legacy
const feedVendorResult = useMemo(() => {
if (!feed?.address) return null;
Expand Down Expand Up @@ -66,10 +82,13 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr
const showAssetPair = !(assetPair.baseAsset === 'Unknown' && assetPair.quoteAsset === 'Unknown');

const vendorIcon = OracleVendorIcons[vendor];
const isChainlink = vendor === PriceFeedVendors.Chainlink;
const isCompound = vendor === PriceFeedVendors.Compound;
const isRedstone = vendor === PriceFeedVendors.Redstone;
const isPendle = vendor === PriceFeedVendors.Pendle;
const hasKnownVendorIcon = vendor !== PriceFeedVendors.Unknown && Boolean(vendorIcon);
const feedAddressKey = feed.address.toLowerCase();
const snapshot = feedSnapshotsByAddress?.[feedAddressKey];
const freshness = getFeedFreshnessStatus(snapshot?.updatedAt ?? null, data?.heartbeat, {
updateKind: snapshot?.updateKind,
normalizedPrice: snapshot?.normalizedPrice,
});

const getTooltipContent = () => {
switch (vendor) {
Expand All @@ -79,6 +98,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr
feed={feed}
feedData={data}
chainId={chainId}
feedFreshness={freshness}
/>
);

Expand All @@ -88,6 +108,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr
feed={feed}
feedData={data}
chainId={chainId}
feedFreshness={freshness}
/>
);

Expand All @@ -97,6 +118,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr
feed={feed}
feedData={data}
chainId={chainId}
feedFreshness={freshness}
/>
);

Expand All @@ -106,6 +128,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr
feed={feed}
feedData={data}
chainId={chainId}
feedFreshness={freshness}
/>
);

Expand All @@ -117,6 +140,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr
feed={feed}
feedData={data}
chainId={chainId}
feedFreshness={freshness}
/>
);

Expand All @@ -127,6 +151,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr
feed={feed}
feedData={data}
chainId={chainId}
feedFreshness={freshness}
/>
);
}
Expand All @@ -148,7 +173,10 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr
};

return (
<Tooltip content={getTooltipContent()}>
<Tooltip
content={getTooltipContent()}
className="w-fit max-w-[calc(100vw-2rem)]"
>
<div className="bg-hovered flex w-full cursor-pointer items-center justify-between rounded-sm px-2 py-1 hover:bg-opacity-80 gap-1">
{showAssetPair ? (
<div className="flex min-w-0 flex-1 items-center gap-1">
Expand All @@ -166,7 +194,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr
)}

<div className="flex flex-shrink-0 items-center gap-1">
{(isChainlink || isCompound || isRedstone || isPendle) && vendorIcon ? (
{hasKnownVendorIcon ? (
<Image
src={vendorIcon}
alt="Oracle"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Badge } from '@/components/ui/badge';
import { cn } from '@/utils/components';
import { formatOracleDuration, formatOracleTimestamp, type FeedFreshnessStatus } from '@/utils/oracle';

type FeedFreshnessSectionProps = {
feedFreshness?: FeedFreshnessStatus;
className?: string;
};

export function FeedFreshnessSection({ feedFreshness, className }: FeedFreshnessSectionProps) {
if (!feedFreshness) return null;

const updatedAt = feedFreshness.updatedAt;
const normalizedPrice = feedFreshness.normalizedPrice;
const hasTimestamp = updatedAt != null;
const hasPrice = normalizedPrice != null;
const isDerived = feedFreshness.updateKind === 'derived';
if (!hasTimestamp && !hasPrice && !isDerived) return null;

return (
<div className={cn('space-y-1 border-t border-gray-200/30 pt-2 dark:border-gray-600/20', className)}>
{normalizedPrice != null && (
<div className="flex items-center justify-between gap-1">
<span className="font-zen text-xs text-gray-600 dark:text-gray-400">Price:</span>
<span className="font-zen text-xs font-medium">{normalizedPrice}</span>
</div>
)}

{updatedAt != null && (
<div className="flex items-center justify-between gap-1">
<span className="font-zen text-xs text-gray-600 dark:text-gray-400">Last Updated:</span>
<span className="whitespace-nowrap text-right font-zen text-xs font-medium">{formatOracleTimestamp(updatedAt)}</span>
</div>
)}

{feedFreshness.ageSeconds != null && (
<div className="flex items-center justify-between gap-1">
<span className="font-zen text-xs text-gray-600 dark:text-gray-400">Age:</span>
<span className="text-right font-zen text-xs font-medium">{formatOracleDuration(feedFreshness.ageSeconds)} ago</span>
</div>
)}

{isDerived && (
<div className="flex items-center justify-between gap-1">
<span className="font-zen text-xs text-gray-600 dark:text-gray-400">Mode:</span>
<Badge
variant="primary"
size="sm"
className="font-zen"
>
DERIVED
</Badge>
</div>
)}

{feedFreshness.isStale && (
<div className="flex items-center justify-between gap-1">
<span className="font-zen text-xs text-gray-600 dark:text-gray-400">Status:</span>
<span className="text-right font-zen text-xs font-medium text-yellow-700 dark:text-yellow-300">Stale</span>
</div>
)}
Comment on lines +56 to +61
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

"Stale" status has no ARIA indicator.

Screen readers see the yellow text as plain text with no hint of severity. A role="status" or aria-label would help. As per coding guidelines, use semantic HTML and ARIA attributes for accessibility.

Proposed fix
-          <span className="text-right font-zen text-xs font-medium text-yellow-700 dark:text-yellow-300">Stale</span>
+          <span role="status" aria-label="Feed is stale" className="text-right font-zen text-xs font-medium text-yellow-700 dark:text-yellow-300">Stale</span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{feedFreshness.isStale && (
<div className="flex items-center justify-between gap-1">
<span className="font-zen text-xs text-gray-600 dark:text-gray-400">Status:</span>
<span className="text-right font-zen text-xs font-medium text-yellow-700 dark:text-yellow-300">Stale</span>
</div>
)}
{feedFreshness.isStale && (
<div className="flex items-center justify-between gap-1">
<span className="font-zen text-xs text-gray-600 dark:text-gray-400">Status:</span>
<span role="status" aria-label="Feed is stale" className="text-right font-zen text-xs font-medium text-yellow-700 dark:text-yellow-300">Stale</span>
</div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/markets/components/oracle/MarketOracle/FeedFreshnessSection.tsx`
around lines 56 - 61, The "Stale" status rendered when feedFreshness.isStale in
FeedFreshnessSection lacks ARIA — update the element that outputs "Stale" to
provide an accessibility cue (e.g., add role="status" and aria-live="polite"
and/or an aria-label like "Feed data is stale") so screen readers announce
severity; keep the visible text unchanged and apply the attributes to the
existing span that renders the status.

</div>
);
}
Loading