Skip to content

Commit c2934af

Browse files
fabiaz84cursoragent
andcommitted
Price Harbor TVL per market using each minter and collateral feed.
Resolve pegged token USD inside the market loop with caching so multi-collateral haUSD pools use matching minter and Chainlink feed pairs. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent aaedf4a commit c2934af

1 file changed

Lines changed: 71 additions & 80 deletions

File tree

src/adaptors/harbor/index.js

Lines changed: 71 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,67 @@ async function calculateAPRFromRewards(poolAddress, poolTVLUsd, chain) {
238238
}
239239
}
240240

241+
/** USD price of 1 pegged token using this market's minter + collateral feed. */
242+
async function resolveMarketPeggedPriceUsd(
243+
chain,
244+
market,
245+
peggedTokenSymbol,
246+
defaultFeedKey,
247+
cache = {}
248+
) {
249+
const feedKey = market.collateralPriceFeed || defaultFeedKey;
250+
const cacheKey = `${market.minterAddress || 'none'}:${feedKey}`;
251+
if (cache[cacheKey] !== undefined) {
252+
return cache[cacheKey];
253+
}
254+
255+
let peggedTokenPriceInUnderlying = 0;
256+
if (market.minterAddress) {
257+
try {
258+
const minterPriceResult = await sdk.api.abi.call({
259+
target: market.minterAddress,
260+
abi: MINTER_ABI.find((m) => m.name === 'peggedTokenPrice'),
261+
chain,
262+
});
263+
if (minterPriceResult?.output) {
264+
peggedTokenPriceInUnderlying = Number(minterPriceResult.output) / 1e18;
265+
}
266+
} catch (error) {
267+
// fall through to default peg ratio
268+
}
269+
}
270+
271+
if (peggedTokenPriceInUnderlying === 0) {
272+
peggedTokenPriceInUnderlying = 1;
273+
} else if (market.marketLabel) {
274+
console.log(
275+
` [${chain}] ${market.marketLabel} peggedTokenPrice: ${peggedTokenPriceInUnderlying.toFixed(6)} ${UNDERLYING_ASSET_DISPLAY[peggedTokenSymbol] || 'collateral units'}`
276+
);
277+
}
278+
279+
let underlyingAssetPriceUSD = 0;
280+
try {
281+
underlyingAssetPriceUSD = await getCollateralUsdPrice(chain, feedKey);
282+
if (underlyingAssetPriceUSD > 0 && market.marketLabel) {
283+
console.log(
284+
` [${chain}] ${market.marketLabel} ${feedKey}: $${underlyingAssetPriceUSD.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
285+
);
286+
}
287+
} catch (error) {
288+
console.log(` [${chain}] Failed to get ${feedKey} price:`, error.message);
289+
}
290+
291+
const peggedTokenPriceUSD = peggedTokenPriceInUnderlying * underlyingAssetPriceUSD;
292+
if (peggedTokenPriceUSD > 0 && market.marketLabel) {
293+
console.log(
294+
` [${chain}] ${market.marketLabel} ${peggedTokenSymbol} price: $${peggedTokenPriceUSD.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
295+
);
296+
}
297+
298+
cache[cacheKey] = peggedTokenPriceUSD;
299+
return peggedTokenPriceUSD;
300+
}
301+
241302
async function fetchPoolsFromChain() {
242303
const marketsByChainToken = {};
243304
for (const market of activeMarkets) {
@@ -258,8 +319,6 @@ async function fetchPoolsFromChain() {
258319
let totalTVL = 0;
259320

260321
try {
261-
let peggedTokenPriceInUnderlying = 0;
262-
let underlyingAssetPriceUSD = 0;
263322
let decimals = 18;
264323

265324
const defaultFeedKey = TOKEN_CHAINLINK_FEED_MAP[peggedTokenSymbol];
@@ -269,84 +328,6 @@ async function fetchPoolsFromChain() {
269328
);
270329
}
271330

272-
let pricingMarket = tokenMarkets.find(
273-
(m) => m.minterAddress && m.collateralPriceFeed
274-
);
275-
276-
if (pricingMarket) {
277-
try {
278-
const minterPriceResult = await sdk.api.abi.call({
279-
target: pricingMarket.minterAddress,
280-
abi: MINTER_ABI.find((m) => m.name === 'peggedTokenPrice'),
281-
chain,
282-
});
283-
if (minterPriceResult?.output) {
284-
peggedTokenPriceInUnderlying = Number(minterPriceResult.output) / 1e18;
285-
console.log(
286-
` [${chain}] peggedTokenPrice from ${pricingMarket.marketLabel || 'market'} minter: ${peggedTokenPriceInUnderlying.toFixed(6)} ${UNDERLYING_ASSET_DISPLAY[peggedTokenSymbol] || 'collateral units'}`
287-
);
288-
}
289-
} catch (error) {
290-
pricingMarket = null;
291-
}
292-
}
293-
294-
if (peggedTokenPriceInUnderlying === 0) {
295-
const hasAnyFeedMarket = tokenMarkets.some((m) => m.collateralPriceFeed);
296-
for (const market of tokenMarkets) {
297-
if (!market.minterAddress || (hasAnyFeedMarket && !market.collateralPriceFeed)) {
298-
continue;
299-
}
300-
try {
301-
const minterPriceResult = await sdk.api.abi.call({
302-
target: market.minterAddress,
303-
abi: MINTER_ABI.find((m) => m.name === 'peggedTokenPrice'),
304-
chain,
305-
});
306-
if (minterPriceResult?.output) {
307-
peggedTokenPriceInUnderlying = Number(minterPriceResult.output) / 1e18;
308-
pricingMarket = market;
309-
console.log(
310-
` [${chain}] peggedTokenPrice from ${market.marketLabel || 'market'} minter: ${peggedTokenPriceInUnderlying.toFixed(6)} ${UNDERLYING_ASSET_DISPLAY[peggedTokenSymbol] || 'collateral units'}`
311-
);
312-
break;
313-
}
314-
} catch (error) {
315-
continue;
316-
}
317-
}
318-
}
319-
320-
if (peggedTokenPriceInUnderlying === 0) {
321-
peggedTokenPriceInUnderlying = 1;
322-
console.log(
323-
` [${chain}] Using default peg ratio: 1.0 ${UNDERLYING_ASSET_DISPLAY[peggedTokenSymbol] || 'UNDERLYING'}`
324-
);
325-
}
326-
327-
const priceFeedKey =
328-
pricingMarket?.collateralPriceFeed ||
329-
tokenMarkets.find((m) => m.collateralPriceFeed)?.collateralPriceFeed ||
330-
defaultFeedKey;
331-
332-
try {
333-
underlyingAssetPriceUSD = await getCollateralUsdPrice(chain, priceFeedKey);
334-
if (underlyingAssetPriceUSD > 0) {
335-
console.log(
336-
` [${chain}] ${priceFeedKey} price in USD: $${underlyingAssetPriceUSD.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
337-
);
338-
}
339-
} catch (error) {
340-
console.log(` [${chain}] Failed to get ${priceFeedKey} price:`, error.message);
341-
}
342-
343-
const peggedTokenPriceUSD = peggedTokenPriceInUnderlying * underlyingAssetPriceUSD;
344-
if (peggedTokenPriceUSD > 0) {
345-
console.log(
346-
` [${chain}] Final ${peggedTokenSymbol} price: $${peggedTokenPriceUSD.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
347-
);
348-
}
349-
350331
try {
351332
const decimalsResult = await sdk.api.abi.call({
352333
target: peggedTokenAddress,
@@ -358,6 +339,8 @@ async function fetchPoolsFromChain() {
358339
console.log(` [${chain}] Decimals lookup failed, using default 18`);
359340
}
360341

342+
const priceCache = {};
343+
361344
for (const market of tokenMarkets) {
362345
const { collateralPoolAddress, sailPoolAddress, marketLabel } = market;
363346

@@ -366,6 +349,14 @@ async function fetchPoolsFromChain() {
366349
` [${chain}] ${peggedTokenSymbol} ${marketLabel || ''} - Collateral: ${collateralPoolAddress}, Sail: ${sailPoolAddress}`
367350
);
368351

352+
const peggedTokenPriceUSD = await resolveMarketPeggedPriceUsd(
353+
chain,
354+
market,
355+
peggedTokenSymbol,
356+
defaultFeedKey,
357+
priceCache
358+
);
359+
369360
const [collateralTVLResult, sailTVLResult] = await Promise.all([
370361
sdk.api.abi.call({
371362
target: collateralPoolAddress,

0 commit comments

Comments
 (0)