Skip to content
Open
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
182 changes: 72 additions & 110 deletions src/adaptors/kintsu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,133 +3,95 @@ const axios = require('axios');

const SECONDS_PER_DAY = 86400;
const DAYS_PER_YEAR = 365;
const SCALE = BigInt(1e18);
const WMON = '0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A';

const vaults = {
monad: '0xA3227C5969757783154C60bF0bC1944180ed81B9',
}
const underlyingTokenPriceId = {
monad: 'coingecko:monad',
const chains = {
monad: {
vault: '0xA3227C5969757783154C60bF0bC1944180ed81B9',
priceId: 'coingecko:monad',
inception: { block: 36068335, timestamp: 1763335037 },
},
};

const chainApy = async (chain) => {
// Get current timestamp
const now = Math.floor(Date.now() / 1000);
const timestamp1DayAgo = now - SECONDS_PER_DAY;
const call = (chain, target, abi, block) =>
sdk.api.abi.call({ target, abi, chain, block }).then((r) => r.output);

// Fetch block numbers for current and 1 day ago
const [blockNow, block1DayAgo] = await Promise.all([
axios
.get(`https://coins.llama.fi/block/${chain}/${now}`)
.then((r) => r.data.height),
axios
.get(`https://coins.llama.fi/block/${chain}/${timestamp1DayAgo}`)
.then((r) => r.data.height),
]);
const getBlock = (chain, timestamp) =>
axios
.get(`https://coins.llama.fi/block/${chain}/${timestamp}`)
.then((r) => r.data.height);

if (!blockNow || !block1DayAgo) {
throw new Error('RPC issue: Failed to fetch block numbers');
}
const getUsdPrice = (priceId) =>
axios
.get(`https://coins.llama.fi/prices/current/${priceId}`)
.then((r) => r.data.coins[priceId].price);
Comment on lines +25 to +28
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against missing price in getUsdPrice response.

If the coin is unknown to coins.llama.fi (typo, new chain not yet indexed, transient lookup miss), r.data.coins[priceId] will be undefined and .price will throw a TypeError with a non-actionable message. Validate the response and surface a clear error so adaptor failures are easier to diagnose.

🛡️ Proposed fix
-const getUsdPrice = (priceId) =>
-  axios
-    .get(`https://coins.llama.fi/prices/current/${priceId}`)
-    .then((r) => r.data.coins[priceId].price);
+const getUsdPrice = (priceId) =>
+  axios
+    .get(`https://coins.llama.fi/prices/current/${priceId}`)
+    .then((r) => {
+      const coin = r.data?.coins?.[priceId];
+      if (!coin || typeof coin.price !== 'number') {
+        throw new Error(`Price not available for ${priceId}`);
+      }
+      return coin.price;
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adaptors/kintsu/index.js` around lines 25 - 28, The getUsdPrice function
currently assumes r.data.coins[priceId] exists and will throw a vague TypeError
when it's missing; update getUsdPrice to validate the response after the
axios.get call (inspect r.data and r.data.coins[priceId]) and throw or reject
with a clear, actionable Error (including the requested priceId and whether
coins or the specific entry was missing) so callers can log/handle an explicit
failure instead of a runtime TypeError.


// Fetch current totalPooled, totalSupply, and symbol
const [totalPooledNow, totalSupplyNow, symbol] = await Promise.all([
sdk.api.abi.call({
target: vaults[chain],
abi: 'function totalPooled() view returns (uint96)',
chain: chain,
block: blockNow,
}),
sdk.api.abi.call({
target: vaults[chain],
abi: 'erc20:totalSupply',
chain: chain,
block: blockNow,
}),
sdk.api.abi.call({
target: vaults[chain],
abi: 'erc20:symbol',
chain: chain,
block: blockNow,
}),
const getShareValue = async (chain, vault, block) => {
const [totalPooled, totalSupply] = await Promise.all([
call(chain, vault, 'function totalPooled() view returns (uint96)', block),
call(chain, vault, 'erc20:totalSupply', block),
]);

// Fetch totalPooled and totalSupply from 1 day ago
const [totalPooled1DayAgo, totalSupply1DayAgo] = await Promise.all([
sdk.api.abi.call({
target: vaults[chain],
abi: 'function totalPooled() view returns (uint96)',
chain: chain,
block: block1DayAgo,
}),
sdk.api.abi.call({
target: vaults[chain],
abi: 'erc20:totalSupply',
chain: chain,
block: block1DayAgo,
}),
]);

// Calculate share values (multiply by 1e18 to handle decimals)
const shareValueNow =
(BigInt(totalPooledNow.output) * BigInt(1e18)) /
BigInt(totalSupplyNow.output);
const shareValue1DayAgo =
(BigInt(totalPooled1DayAgo.output) * BigInt(1e18)) /
BigInt(totalSupply1DayAgo.output);

if (shareValue1DayAgo === 0n) {
throw new Error('RPC issue: Previous share value is zero');
}

// Calculate proportion: shareValueNow / shareValue1DayAgo
// Multiply by 1e18 to maintain precision
const proportion =
Number((shareValueNow * BigInt(1e18)) / shareValue1DayAgo) / 1e18;

if (proportion <= 0) {
throw new Error('RPC issue: Invalid proportion calculated');
if (BigInt(totalSupply) === 0n) {
throw new Error(`RPC issue: zero totalSupply at block ${block}`);
}
return (BigInt(totalPooled) * SCALE) / BigInt(totalSupply);
};

// Calculate APY using the formula:
// APY = ((1 + ((proportion - 1) / 365)) ** 365 - 1) * 100
// This is equivalent to: APY = (proportion ** 365 - 1) * 100
const apyBase = (Math.pow(proportion, DAYS_PER_YEAR) - 1) * 100;
const annualize = (svNow, svThen, periodDays) => {
if (svThen === 0n) throw new Error('RPC issue: previous share value is zero');
const ratio = Number((svNow * SCALE) / svThen) / 1e18;
if (ratio <= 0) throw new Error('RPC issue: invalid ratio');
return (Math.pow(ratio, DAYS_PER_YEAR / periodDays) - 1) * 100;
};

// Convert to number (assuming 18 decimals for underlying token)
const tvlUnderlyingToken = Number(totalPooledNow.output) / 1e18;
const chainApy = async (chain) => {
const { vault, priceId, inception } = chains[chain];
const now = Math.floor(Date.now() / 1000);
const inceptionDays = (now - inception.timestamp) / SECONDS_PER_DAY;

// Get native token price to convert to USD
const underlyingTokenPriceResponse = await axios.get(
`https://coins.llama.fi/prices/current/${underlyingTokenPriceId[chain]}`
);
const underlyingTokenPrice =
underlyingTokenPriceResponse.data.coins[underlyingTokenPriceId[chain]].price;
const tvlUsd = tvlUnderlyingToken * underlyingTokenPrice;
const [blockNow, block7dAgo] = await Promise.all([
getBlock(chain, now),
getBlock(chain, now - SECONDS_PER_DAY * 7),
]);
if (!blockNow || !block7dAgo) {
throw new Error('RPC issue: Failed to fetch block numbers');
}

return [
{
pool: vaults[chain].toLowerCase(),
chain: chain,
project: 'kintsu',
symbol: symbol.output,
tvlUsd: tvlUsd,
apyBase: apyBase,
...(Number(shareValueNow) / 1e18 > 0 && { pricePerShare: Number(shareValueNow) / 1e18 }),
underlyingTokens: [WMON],
searchTokenOverride: vaults[chain],
isIntrinsicSource: true,
},
];
const [totalPooledNow, symbol, svNow, sv7d, svInception, underlyingPrice] =
await Promise.all([
call(chain, vault, 'function totalPooled() view returns (uint96)', blockNow),
call(chain, vault, 'erc20:symbol', blockNow),
getShareValue(chain, vault, blockNow),
getShareValue(chain, vault, block7dAgo),
getShareValue(chain, vault, inception.block),
getUsdPrice(priceId),
]);

const apyBase = annualize(svNow, sv7d, 7);
const apyBaseInception = annualize(svNow, svInception, inceptionDays);
const pricePerShare = Number(svNow) / 1e18;
const tvlUsd = (Number(totalPooledNow) / 1e18) * underlyingPrice;

return {
pool: vault.toLowerCase(),
chain,
project: 'kintsu',
symbol,
tvlUsd,
apyBase,
apyBase7d: apyBase,
apyBaseInception,
...(pricePerShare > 0 && { pricePerShare }),
underlyingTokens: [WMON],
searchTokenOverride: vault,
isIntrinsicSource: true,
};
};

const apy = async () => {
const chains = Object.keys(vaults);
const apys = await Promise.all(chains.map(async (chain) => await chainApy(chain)));
return apys.flat();
};
const apy = async () => Promise.all(Object.keys(chains).map(chainApy));

module.exports = {
apy,
url: 'https://kintsu.xyz/staking',
};

Loading