From d81910160f2f2e790a7c26f0e579759d525db00b Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 7 Feb 2026 17:52:34 +0800 Subject: [PATCH 1/2] feat: pendle integration --- app/api/oracle-metadata/[chainId]/route.ts | 8 +- .../oracle/MarketOracle/FeedEntry.tsx | 13 ++- .../oracle/MarketOracle/PendleFeedTooltip.tsx | 93 ++++++++++++++++++ src/hooks/useOracleMetadata.ts | 4 + src/imgs/oracles/pendle.png | Bin 0 -> 6579 bytes src/utils/oracle.ts | 11 +++ 6 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx create mode 100644 src/imgs/oracles/pendle.png diff --git a/app/api/oracle-metadata/[chainId]/route.ts b/app/api/oracle-metadata/[chainId]/route.ts index 9aabc59d..61de9d40 100644 --- a/app/api/oracle-metadata/[chainId]/route.ts +++ b/app/api/oracle-metadata/[chainId]/route.ts @@ -17,7 +17,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ chai try { const url = `${ORACLE_GIST_BASE_URL}/oracles.${chainId}.json`; const response = await fetch(url, { - next: { revalidate: 1800 }, // Cache for 30 minutes + cache: 'no-store', }); if (!response.ok) { @@ -29,11 +29,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ chai const data = await response.json(); - return NextResponse.json(data, { - headers: { - 'Cache-Control': 'public, s-maxage=1800, stale-while-revalidate=3600', - }, - }); + return NextResponse.json(data); } catch (error) { console.error('Failed to fetch oracle metadata:', error); return NextResponse.json({ error: 'Failed to fetch oracle metadata' }, { status: 500 }); diff --git a/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx b/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx index a328e478..388aabab 100644 --- a/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx +++ b/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx @@ -10,6 +10,7 @@ import type { OracleFeed } from '@/utils/types'; import { ChainlinkFeedTooltip } from './ChainlinkFeedTooltip'; import { CompoundFeedTooltip } from './CompoundFeedTooltip'; import { GeneralFeedTooltip } from './GeneralFeedTooltip'; +import { PendleFeedTooltip } from './PendleFeedTooltip'; import { RedstoneFeedTooltip } from './RedstoneFeedTooltip'; import { UnknownFeedTooltip } from './UnknownFeedTooltip'; @@ -56,6 +57,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap }: F const isChainlink = vendor === PriceFeedVendors.Chainlink; const isCompound = vendor === PriceFeedVendors.Compound; const isRedstone = vendor === PriceFeedVendors.Redstone; + const isPendle = vendor === PriceFeedVendors.Pendle; const getTooltipContent = () => { switch (vendor) { @@ -86,6 +88,15 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap }: F /> ); + case PriceFeedVendors.Pendle: + return ( + + ); + case PriceFeedVendors.PythNetwork: case PriceFeedVendors.Oval: case PriceFeedVendors.Lido: @@ -143,7 +154,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap }: F )}
- {(isChainlink || isCompound || isRedstone) && vendorIcon ? ( + {(isChainlink || isCompound || isRedstone || isPendle) && vendorIcon ? ( Oracle + {/* Header with icon and title */} +
+ {vendorIcon && ( +
+ Pendle +
+ )} +
Pendle Feed Details
+
+ + {/* Feed pair name */} +
+
+ {baseAsset} / {quoteAsset} +
+
+ + {/* Pendle Specific Data */} + {(feedData?.ptSymbol != null || feedData?.baseDiscountPerYear != null) && ( +
+ {feedData?.ptSymbol != null && ( +
+ PT Token: + {feedData.ptSymbol} +
+ )} + {feedData?.baseDiscountPerYear != null && ( +
+ Base Discount / Year: + {formatDiscountPerYear(feedData.baseDiscountPerYear)} +
+ )} +
+ )} + + {/* External Links */} +
+
View on:
+
+ + Etherscan + Etherscan + +
+
+
+ ); +} diff --git a/src/hooks/useOracleMetadata.ts b/src/hooks/useOracleMetadata.ts index a95aef45..b3ec81bf 100644 --- a/src/hooks/useOracleMetadata.ts +++ b/src/hooks/useOracleMetadata.ts @@ -29,6 +29,10 @@ export type EnrichedFeed = { deviationThreshold?: number; ens?: string; // Chainlink ENS name for feed URL (e.g. "eth-usd") feedType?: string; // Redstone feed type: "market" or "fundamental" + baseDiscountPerYear?: string; // Pendle base discount per year (raw 18-decimal value) + innerOracle?: string; // Pendle inner oracle address + pt?: string; // Pendle PT token address + ptSymbol?: string; // Pendle PT token symbol }; export type OracleOutputData = { diff --git a/src/imgs/oracles/pendle.png b/src/imgs/oracles/pendle.png new file mode 100644 index 0000000000000000000000000000000000000000..27586919855b1dd156eb80e5b8e4b797db2f5866 GIT binary patch literal 6579 zcmXY0cT`i&)4qWKmmUP9V*&yS(xpmEq)LdW^eR;lq@(nb1QZZy(mO~M2ukl&ktR(6 z>4+djdKCnIdB5}hvFGeOvpaY1+}WAk=SJvaG-#>VsQ>_=)zVab2mlZiu^~ZnB0?fp zC;1?M*9dnm!VCacOsYu@bGfT?#1z8&~eo7Cw`eOn4(d z<}qnsh~b83uA!we-ENOfqKkM_YVD`4b2Fc-Sqs6^X(7EmHuZtm)jTIbA(iz?FCup#jfMCvx}g%HV}YTXoN_=j*c!E)b{i6 z_-JYA@8{Qdt>|G%|3Qy-!9#WBH*e*`hv}{ zCnd!PfofRq3bZc1LLL68gk$IdLa{ zeS@2;{g3oUR8tDNDFW`LtA9d!ub!07bXkYGrN7xF|DhKJa`jbd?TZ9hP{nZI>uHDV zO%0d{nu2?Uel({ zV=0sg`h86)ow>A4*CDZfP%okhYF?usTm&G>2;0ovr90Xl$-05C$yPxEqMI%{NP1c{smJEa|g){9bl*C5}Ivi3p# zbX3D~%gd-W3KGgxP^MD*~tHZg`QyySlXS;Zz`5aSCzcEq=YQ>c(=;q zD_6cO8eZ0+7rIeGvY1f1F2p)@JKF|Cj|vF81ZI0=Xg|y z02jph`^OmYaSrg08a~ouopFmPb3)j*ToY5P{dB7$$FM3Ad{F>;=$~AD8G+y$1r=P4 z)XiodP=V&?(@BGlVWGx-2NeCMS+1imB*MwF`B2PoO&<=X-!WIYw>+kyV(%o zP>>&#g612@3IQtx9F;nYux|1{aBSsAWnCkBXxjH{HGOQmDaS}7DJCmLn1hbpY^9~b zvNvoU>w23BGR#fe73fE9sqpI=OkGBko-$-|l`Y_!K%WJ9u0>kAEvshuRo2$9TtkgN zf<4RlUa{EX8#JGiZG7kS@DIR`isY-X>?3wR2E0CEbyJVJGHy%iTfq(YjC~~Qv$x}l zSUh}~%rCfT!)nzFW}uH{gA7Ymyv(gaYJw!s-Q{R9X;9-($ejlEa11#D0ZwXn4c7#Q zeSAe z^c@K`m(h0;;|o$TN|KROd+uTQ9;X|;c#!&@q}@AFta=Y?>x8RZXXBl@MSFr^oTanl za>9w7lJ$c?w(Ya#51;$s-=0ZyS|HoCN1~++Hh~W!U_Pbzv@H;mD%Px>EpjudQV{g+ z{)%S>aW{QxrzNy%`N90fSBcamz?IzKmGVhuYaN(+8=MUSyCM`WU;wr9;uaSIu=!n# zin0jvZ-?-2-pDsY5UVj9#24I1K?xVQYmEmyYVXC zUgvRa6B$G=>WJm}KQT2!tXLzuqvU-v#4B7R+56NZyaI|awFADzMI;T%7cL=-HCFNE z62SVfuvDf3c47qnEy;%%#+txm?h%IOH`QObXgiB%mV=N$^r}(R%m=vbJ(?Wj2T>U( zObxuqFK<~@-lK_v@vk>f1V<618nGKa9zl_7fUE8XrmQ4vJ>zi+-8eBnfDx$XeJ0=E zZSqSMr%Q1irA-4TpEBmJsS_jGR3U-86n3GnKZ-8{520G>09@%u@z`s99wh6mQc z9GL&KvLN^da2=6&uRQr`H7B0mx2jVz5;Dm*-6uRQOo6DoPg?mvsLrU43IE`yJrwwp z@a(z6=tEc~# za8yvGQ=t+n&pTtGA*5*knVsQxC2(hcD@acN!N1+dyxEg_u&13=fNZDjc%!-!VG(Y- z4F@QszH!TN;{R|#7xL7nmcntd)K`J};<%&wTsS8e*LT=2EEyn@R;TE41LQDwg903b z6WlWFcw-2lF=#k2$bm;kL40yt{1sebY4Pz0O5peRrq981@Y{UL-Xy@Ajm^#SR6B2+ zE@4JO^qdF?5&fW#H)|kk=anL?lycym(;*9tlKUxDU74bwBhgsCfxDq3*x(Nfd3pMK zO8pUmXMhSgQM>DAP-H3z>WB{3vH^4AKaw*421?;#50QXUnv?562J*`(9Egz`XTOiZ z5Z(Ci?VHG!o1Wx|xn895F5VW;Y}f6;bwySp-^XPizMLV zF!g!e7%5xzf*iPSC%8R8g*u`q0g>J#GD0BRTj2kDPegb~LSoP`d%^Ad)SV8yvANSyjZ)yE08un1Y8i z_W)@AM{_q5*hg=26;1St@c2Y(xi~W-Bo<8o8=N63A@=1A4{1rj=sAPTEGm?wIzI@( zuR6nyj*f`0*O`z?)oJ}fQw@a6`sKL@f^zyKaMi{VKS5BtV3Y?EvXeoJ!XyIVx9Z4P zI26YD2r6QI4H1&FCf@WGP?;>$$Fo5~fCmgDpm`cmFOP)*eMnhp=_CdKWFjtOfdLO- zHDDwlHxh*VK#4#iu983y6$M~71W`Z-FL5;$Dn$KkHGF^?fWC@^sjTvs;ws?)EF~)| zYw3H;{&f%nEi$@=(NkW?Bt9b#<*g=*0AN|D!zG1%5+GEo;NjnOismtPQgGx)B(YY; z|2L`%IXOL@&H{kbiss%aZPyhb zw@;(Vu8;y@>Ht8mTlzj3*FU$O3Q``hzN)M(oqz|!0$h)SO!^lQ&Y+e815{5=3k z9HCiK2u$T91w&IK(01mfpXnvUAPZHFWR~T>*?*G(3R0do6rRA+ygOS+fQz&w_Zn=h zHwsWnZ{xX5nvem6WK(^|U6CS=xZg)j$_NCdecvry57={o07C;(q$O;4&KlB%{B=gY zNArfi_dQ8~EOG7NhSd4w(*0iNGV+D=8MFD+(o0Vx0q6pKm6Gg}!tb=iSV&<6cDl_r zaLe0-;{-o{{}+_N2}0TP^0C@1{IyG&y&p}xucD%2gNd^AJiFCk7CXTCNV0#0w$nsW z3XtVD+@K)#9foM99f0~NEB zN30=T9wX5vQc_I#NTLw6;uNLdh14Xv@Ik|0rQAjMJ4Nz^9N%f{?`|-`DdI%Ocftlb zWQa|1py&2mV;!>G+MdI&8;}rTv;pnJw5oY|30&`!b=(tfQa5CmwyzFFu47Cc)m=v5 zdH;|%U$8I{NJ^{j0i5k6o@z^G&wIhtYbk+Ap@{*3|MF*VZ&*1{#z?ERH~25ZK&UhP z_&1>PQYuqNm?BsF7L5)tSx|z|`b6RwN2f!_DO)+H7oN@sUmD_082QlU@vFf`BIsLl z>O_5p!eQRbd19tVubQ^4-F(5vof<-1f3l?@z&`dT)^`237RTq$pGONGuGRaj;wcM8 za4-9FkE=dZ5oRk-p9$Ih*hu=a%+DzWH?(Yc2n@tqwg&iSSLc+&_4sLCq=|9Ddi1f| zxf;jGmdA=pg3@Dd-|B(_`B)+4n| z)PeSs6QSYZsm=`x>rxH4m@gyVDUDdVs=4re_(g8UaAWt7@cDCk1(S*sXTqsTL9S01 z#hls=9@dehRNB$JdhGCesqXaLS@4cS!`{Bszo#YmpR^Ng+JI#${Et%&`>o4>h=^$Y z#n$uuWvR9^G}t(G^jYntFnc72i-mR~NT@cYO==78?k+6VC3QQK*`lvFb<%4<-FBrh zUcfzn-?29l%j(@E*gRBr?s%vHNQc+5RYc{p-wNi6cu-f0qHQN!^q(*5e09jG9ju$H zPEx^p{wyZ%x74s6?=6kJDr;eam}}j=Iu2WeCP!zbK4e%bgJ#r?glu) zIQVUStIz+Uwj{R;`S_>EgD|rOztYpvE>j)v;zXWdoz0A!6@R7p5F>-PZAR7gi}e?{UIg)-3=Y@k$m7YZ z3FP|L$YM0Bi-eU=^6iKkm8LZVj&ZsXTK8wksfXTtR)a1@BT*RT!_+oA;b9Nw8f4L@ z8ecz=O|$Rw#o?~6zdu6Mxtuzoh9QmKT9=*c<90P5#v<)Yygi=7o`uhrv4k%b z-RF*Xs%S+eiL|P~su`+8e5$;R4JW|&YkrH~+m?YgF$a}xVt~BDh)t40IKrV(#3$dV zJuOmq^O7(VH-9>;@l`=F=CtU%dFWs38*EFBNsa z#piUEWLweuL~fj#)OMUqc{chSTVpiFZg{4(<&-Yr+I(FA#Ll6=_9nQg8ah2=jxsW2 zFj(EOUF_U4LBux9ye6*nT8hy8kGU@z}dgQ~kxFU!yCo+33sEc>T}bp5PSA{q$>DkK;MX z6@5Lr4!5uP6g)=lkel*%dwx&?6EX4g0pI=oFj;=$Lc}jS{fX&^C_mIy65(*z_>vJ> zoFyk4Z)2*%dho3`ge}8OJNhhd$4gLMAHkx_`O6t5NZaN4P|@kCd1b8F!CM2oUf7YR95gY(3ICl+ zBTM#t6?YtzAayr*%>-fS{iFX7riU@aGncWQ>Rco1Bdd5Wfs1=q@iJ$R2h``IF|-${ zS$QMb{d@X&i+PeSjn4H7y$>V)UYFbUi|vM89U+lf#7^Xv&^y8Emr+V6(ZX$mL zf|{L1Hs)q{72}zGkjKm-I+pEhw$!Veqx{gzdkR18V5KZsXKkD;QYUI%vFVPD_gv!| z%BN%I(AYX;@j+fVeb6c|V;Bpj;=8h{TlyS2|96b2@9Y(14|Cobv}I;;?K}ITS*v8g z@9{dzz)w%t)_SQkj(2#^$heAgqzvn78}kT3y0={sNW z4->d{IYP(Z=Q74oMrb&tLVD!&?!-cQQX(s-#$x909AwqC9gf{2yIGlkV6%|+16hW|!}f(YrR2uM@NZn~YO1R+Zag8u zdnaZ z%$~qqiqD^ATHW@Tl4ZSg+Y^>h2e;(}+oj3;&(K%0W-jw*1e7jzU9( zV8Gw8YaK0yAQoz$%NAThONr7x;*6e=sS`Kq$;*XIEN=aY|?jFDYQsimjW zmnX*Vu`c+z&Ik2dyr8gGe^2;nPYn00*RLNo8<`ps@Ya%E6PiN}CYD z4@w%?*!3Z6XfCf8?&!|k$AF#Hea4OKpcMyoF}0Y9HSs&tjwhSagf6|{=e!`-yYl@! zHNxCM!g>A#LnOh+p-!sjnP=hQtdq&SRc%W>4(>IR9lVw0e6e%VKJ=N6KrP4A#EHje zFM+6-e?KyhZI6Ck_%CFTwQOLrl@e%|BTGff7@nrzK}~PaV}~EfJN(VRf2s3)ZFHX^ z=(IampO^)XqKf31@Fs_f_NA@nE9VTIL!!>bM|uYo-i8N-9Vy}+lcI_Rp}a#}mQr$Z zKeiV~s%42}c+oavKoBoT%1~~;X3EDpmfd{5eafdlaFripUUV#=D?Z*h|P?@}+Gmq!UsM>k0GQ1>Wy5z9U z`0o9#H^W^_MDl)xN~0$5?CVAF`SGP@+xm>>>HdALugw=fqYR=IrLvT^n>{53Fd6qu z$F1*&nSU_K5Zm0;bai$0t?y7}sS&${G_8&tSXP%{e@i@KWLmscQr;icell?OM8yB% zFUx<$Q6elN;WTIW;4k%%aNV5pwaYLwB2$cRA>0W%FuI^kHZuyk?Cu>4`?R)CEnp$5 zU%!*57Pb_|$Vjk!ZnbE%s2cQ%Be_>Y>4f9&#YLj;ehFFWo$8;Lkm$FAW!(E+1me#a NKuZmyT7i0u{U19>4&wj- literal 0 HcmV?d00001 diff --git a/src/utils/oracle.ts b/src/utils/oracle.ts index bec1237a..5c5467de 100644 --- a/src/utils/oracle.ts +++ b/src/utils/oracle.ts @@ -48,6 +48,7 @@ export enum PriceFeedVendors { Oval = 'Oval', Compound = 'Compound', Lido = 'Lido', + Pendle = 'Pendle', Unknown = 'Unknown', } @@ -58,6 +59,7 @@ export const OracleVendorIcons: Record = { [PriceFeedVendors.Oval]: require('../imgs/oracles/uma.png') as string, [PriceFeedVendors.Compound]: require('../imgs/oracles/compound.webp') as string, [PriceFeedVendors.Lido]: require('../imgs/oracles/lido.png') as string, + [PriceFeedVendors.Pendle]: require('../imgs/oracles/pendle.png') as string, [PriceFeedVendors.Unknown]: '', }; @@ -74,6 +76,7 @@ export function mapProviderToVendor(provider: OracleFeedProvider): PriceFeedVend Lido: PriceFeedVendors.Lido, Oval: PriceFeedVendors.Oval, Pyth: PriceFeedVendors.PythNetwork, + Pendle: PriceFeedVendors.Pendle, }; return mapping[provider] ?? PriceFeedVendors.Unknown; @@ -110,6 +113,10 @@ export type FeedData = { deviationThreshold?: number; ens?: string; // Chainlink ENS name for feed URL (e.g. "eth-usd") feedType?: string; // Redstone feed type: "market" or "fundamental" + baseDiscountPerYear?: string; // Pendle base discount per year (raw 18-decimal value) + innerOracle?: string; // Pendle inner oracle address + pt?: string; // Pendle PT token address + ptSymbol?: string; // Pendle PT token symbol }; export type FeedVendorResult = { @@ -175,6 +182,10 @@ export function detectFeedVendorFromMetadata(feed: EnrichedFeed | null | undefin deviationThreshold: feed.deviationThreshold, ens: feed.ens, feedType: feed.feedType, + baseDiscountPerYear: feed.baseDiscountPerYear, + innerOracle: feed.innerOracle, + pt: feed.pt, + ptSymbol: feed.ptSymbol, }; return { From 53fafe84595079edfb15bc27bad8e1d68cf47973 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Thu, 12 Feb 2026 14:45:57 +0800 Subject: [PATCH 2/2] chore: review fixes --- .../markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx index 2f3f6d06..8e8141de 100644 --- a/src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx @@ -14,6 +14,7 @@ type PendleFeedTooltipProps = { }; function formatDiscountPerYear(raw: string): string { + if (!/^\d+$/.test(raw)) return "—"; const formatted = formatUnits(BigInt(raw), 18); const percent = Number(formatted) * 100; return `${percent.toFixed(2)}%`;