feat: add stke sol by sol strategies#2341
Conversation
📝 WalkthroughWalkthroughAdds a Solana STKESOL adapter computing APY from on‑chain stake pool snapshots and SOL price, new Solana RPC utilities for decoding stake pool account data and APY helpers, and a dependency on Changes
Sequence DiagramsequenceDiagram
participant Client as Caller
participant Adapter as STKESOL Adapter
participant Solana as Solana RPC
participant Price as Price Oracle
Client->>Adapter: apy()
par Parallel fetch
Adapter->>Solana: getStakePoolInfo(stakePoolAddress)
Solana-->>Adapter: stake pool account data
Adapter->>Adapter: decode stake pool (totalLamports, poolTokenSupply, snapshots)
Adapter->>Price: getCurrentPrice(SOL)
Price-->>Adapter: currentSolPrice
end
Adapter->>Adapter: calcSolanaLstApy(stakePoolInfo)
alt SOL price missing
Adapter-->>Client: throw "Unable to fetch SOL price"
else
Adapter->>Adapter: build pool metadata (tvlUsd, apyBase, poolMeta)
Adapter-->>Client: [ pool metadata ]
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
The stkesol-by-sol-strategies adapter exports pools: Test Suites: 1 passed, 1 total |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/adaptors/stkesol-by-sol-strategies/index.js`:
- Around line 44-47: The calculation can divide by zero when stkePrice or
solPrice is 0 and can produce extreme/Infinity APY when days is near 0; in the
function that computes APY (use symbols historicalRatio, stkePrice, solPrice,
ratioChange, days) add guards: if stkePrice or solPrice are falsy/zero return a
safe sentinel (null or 0) or throw, ensure historicalRatio and ratioChange are
finite and >0 before taking powers, enforce a minimum days threshold (e.g.
minDays = 1) or clamp 365/days to a reasonable max exponent, and after computing
APY verify Number.isFinite(result) and clamp to a sane max/min before returning.
In `@src/adaptors/utils.js`:
- Around line 569-573: The current exchangeRate calculation can divide by zero
when poolTokenSupply is zero; update the code around totalLamports,
poolTokenSupply and exchangeRate (values read via buffer.readBigUInt64LE) to
check poolTokenSupply first (use a BigInt comparison like poolTokenSupply ===
0n) and handle that case explicitly (e.g., set exchangeRate to 0 or null, or
early-return/log an error) instead of performing Number(totalLamports) /
Number(poolTokenSupply); only perform the division when poolTokenSupply is
non-zero.
- Around line 566-570: The code reads totalLamports and poolTokenSupply from
hardcoded offsets in src/adaptors/utils.js (buffer.readBigUInt64LE(258) and
buffer.readBigUInt64LE(266)), which is fragile; replace this by using the
official `@solana/spl-stake-pool` decoder to parse the stake pool account data
instead of manual offsets: add/import the appropriate decoder from
`@solana/spl-stake-pool`, call its decode/deserialize method on buffer (or the
StakePool layout/type) to obtain the parsed object, then read
parsed.totalLamports and parsed.poolTokenSupply (or the library field names)
instead of buffer.readBigUInt64LE; ensure the package is added to dependencies
and update any variable names (buffer -> stakePoolAccountData) accordingly.
🧹 Nitpick comments (3)
src/adaptors/utils.js (1)
538-559: Add timeout to Solana RPC request.The axios request has no timeout configured, which could cause the adapter to hang indefinitely if the RPC endpoint is unresponsive.
⏱️ Proposed fix to add timeout
const getSolanaAccountInfo = async (address, rpcUrl = 'https://api.mainnet-beta.solana.com') => { const response = await axios.post(rpcUrl, { jsonrpc: '2.0', id: 1, method: 'getAccountInfo', params: [address, { encoding: 'base64' }], }, { headers: { 'Content-Type': 'application/json' }, + timeout: 30000, });src/adaptors/stkesol-by-sol-strategies/index.js (2)
17-19: Add timeout to axios requests.Multiple axios calls lack timeout configuration. If the DefiLlama API is slow or unresponsive, the adapter could hang indefinitely.
⏱️ Proposed fix to add timeout
const historicalRes = await axios.get( - `https://coins.llama.fi/prices/historical/${thirtyDaysAgo}/${stkesolKey},${solKey}` + `https://coins.llama.fi/prices/historical/${thirtyDaysAgo}/${stkesolKey},${solKey}`, + { timeout: 30000 } );Apply similar timeout configuration to other axios.get calls at lines 27, 33, and 53.
Also applies to: 27-28, 33-34, 53-53
50-54: Consider adding error handling for stake pool fetch failure.If
getStakePoolInfothrows (e.g., network error, account not found), the entireapy()function will fail. Depending on yield-server's behavior for failed adapters, you may want to catch and log the error, returning an empty array instead.🛡️ Optional: wrap in try-catch for graceful degradation
const apy = async () => { + try { const [stakePool, priceRes] = await Promise.all([ getStakePoolInfo(STAKE_POOL), axios.get(`https://coins.llama.fi/prices/current/${solKey}`), ]); const solPrice = priceRes.data.coins[solKey]?.price; if (!solPrice) throw new Error('Unable to fetch SOL price'); return [ { pool: STKESOL_MINT, chain: 'Solana', project: 'stkesol-by-sol-strategies', symbol: 'STKESOL', tvlUsd: stakePool.tvlSol * solPrice, apyBase: await calculateApy(stakePool.exchangeRate), underlyingTokens: [SOL], poolMeta: '5% epoch fee, 0.1% withdrawal fee', }, ]; + } catch (err) { + console.error(`stkesol-by-sol-strategies error: ${err.message}`); + return []; + } };
|
The stkesol-by-sol-strategies adapter exports pools: Test Suites: 1 passed, 1 total |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/adaptors/stkesol-by-sol-strategies/index.js`:
- Around line 49-53: The APY calculation uses currentExchangeRate and
historicalRatio directly, which can produce ratioChange=0 and yield a misleading
-100% APY; update the logic in the function around the variables
historicalRatio, ratioChange and apy to first validate currentExchangeRate > 0
and historicalRatio > 0 (and that days > 0), and if any are invalid return null
(or skip publishing) instead of computing Math.pow; ensure the early-return
happens before computing ratioChange or apy so getStakePoolInfo
returns/propagates a null result for empty/invalid pools.
In `@src/adaptors/utils.js`:
- Around line 567-575: StakePoolLayout.decode returns BN instances for
totalLamports and poolTokenSupply, so the current zero-check poolTokenSupply ===
0n will always fail; convert those BN values to bigint (or Number safely when
within JS number range) before checking for zero and computing exchangeRate:
read stakePool.totalLamports and stakePool.poolTokenSupply, convert to bigint
(e.g., BigInt(stakePool.poolTokenSupply.toString())) then check == 0n and
compute exchangeRate using converted values (use Number(...) only after ensuring
safe range) so division-by-zero is correctly guarded in the exchangeRate
calculation.
| const historicalRatio = stkePrice / solPrice; | ||
| const ratioChange = currentExchangeRate / historicalRatio; | ||
|
|
||
| const apy = (Math.pow(ratioChange, 365 / days) - 1) * 100; | ||
|
|
There was a problem hiding this comment.
Guard against zero/invalid currentExchangeRate to avoid -100% APY.
If getStakePoolInfo returns exchangeRate = 0 (e.g., empty pool or decode issue), Line 50 yields ratioChange = 0, producing an APY of -100%. Returning null is safer than publishing a misleading negative APY.
🛡️ Suggested guard
if (!stkePrice || !solPrice) {
return null;
}
+ if (!currentExchangeRate || currentExchangeRate <= 0) return null;
+
const historicalRatio = stkePrice / solPrice;
const ratioChange = currentExchangeRate / historicalRatio;📝 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.
| const historicalRatio = stkePrice / solPrice; | |
| const ratioChange = currentExchangeRate / historicalRatio; | |
| const apy = (Math.pow(ratioChange, 365 / days) - 1) * 100; | |
| if (!stkePrice || !solPrice) { | |
| return null; | |
| } | |
| if (!currentExchangeRate || currentExchangeRate <= 0) return null; | |
| const historicalRatio = stkePrice / solPrice; | |
| const ratioChange = currentExchangeRate / historicalRatio; | |
| const apy = (Math.pow(ratioChange, 365 / days) - 1) * 100; |
🤖 Prompt for AI Agents
In `@src/adaptors/stkesol-by-sol-strategies/index.js` around lines 49 - 53, The
APY calculation uses currentExchangeRate and historicalRatio directly, which can
produce ratioChange=0 and yield a misleading -100% APY; update the logic in the
function around the variables historicalRatio, ratioChange and apy to first
validate currentExchangeRate > 0 and historicalRatio > 0 (and that days > 0),
and if any are invalid return null (or skip publishing) instead of computing
Math.pow; ensure the early-return happens before computing ratioChange or apy so
getStakePoolInfo returns/propagates a null result for empty/invalid pools.
9a97a4e to
efdb2bc
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/adaptors/utils.js (1)
569-569:requirecall placed mid-file.The
require('@solana/spl-stake-pool')at Line 569 is sandwiched between function definitions rather than at the top of the module with other imports. Move it to the top of the file alongside the existingrequirecalls.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/adaptors/utils.js` at line 569, The require call for '@solana/spl-stake-pool' (used as StakePoolLayout) is currently placed mid-file; move the const { StakePoolLayout } = require('@solana/spl-stake-pool') statement up into the top of the module with the other require/import statements so all dependencies are declared together and available before functions that reference StakePoolLayout; update any linter ordering if needed and ensure there are no duplicate imports for StakePoolLayout after moving it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/adaptors/stkesol-by-sol-strategies/index.js`:
- Around line 22-24: getStakePoolInfo currently doesn't populate a withdrawal
fee field, so poolMeta only shows epochFee; update getStakePoolInfo to extract
the withdrawal fee (use stakePool.stakeWithdrawalFee or
stakePool.solWithdrawalFee as present) calculate it as a percentage string
similar to epochFee (e.g., ((numerator/denominator)*100).toFixed(1) + '%
withdrawal fee'), add that value to the returned object (e.g., withdrawalFee or
solWithdrawalFee), and then include that field in poolMeta so the output becomes
"X% epoch fee, Y% withdrawal fee" alongside existing epoch fee formatting.
- Line 34: The field underlyingTokens currently uses STKESOL_MINT (the LST) but
must reference the deposited asset SOL; update the config in the object that
defines underlyingTokens (replace STKESOL_MINT with SOL) so underlyingTokens:
[SOL] is used; ensure the SOL constant (already defined) is referenced and
remove the incorrect STKESOL_MINT usage in that underlyingTokens assignment.
---
Duplicate comments:
In `@src/adaptors/utils.js`:
- Around line 577-584: The division-by-zero guard uses BigInt literal (0n) but
stakePool.u64 fields (totalLamports, poolTokenSupply, lastEpochTotalLamports,
lastEpochPoolTokenSupply) are BN instances from StakePoolLayout.decode, so
poolTokenSupply === 0n never triggers; fix by normalizing/checking BNs: either
convert poolTokenSupply to a Number first (e.g., const poolTokenSupplyNum =
Number(poolTokenSupply)) and use poolTokenSupplyNum === 0 to guard, or call
poolTokenSupply.isZero() on the BN; then compute exchangeRate using the
normalized numeric values and remove redundant Number(...) wrappings so
exchangeRate = poolTokenSupplyNum === 0 ? 0 : totalLamportsNum /
poolTokenSupplyNum (apply same normalization to totalLamports and lastEpoch*
fields where used).
---
Nitpick comments:
In `@src/adaptors/utils.js`:
- Line 569: The require call for '@solana/spl-stake-pool' (used as
StakePoolLayout) is currently placed mid-file; move the const { StakePoolLayout
} = require('@solana/spl-stake-pool') statement up into the top of the module
with the other require/import statements so all dependencies are declared
together and available before functions that reference StakePoolLayout; update
any linter ordering if needed and ensure there are no duplicate imports for
StakePoolLayout after moving it.
| const feePct = stakePool.epochFee | ||
| ? `${((stakePool.epochFee.numerator / stakePool.epochFee.denominator) * 100).toFixed(0)}% epoch fee` | ||
| : null; |
There was a problem hiding this comment.
poolMeta is missing the withdrawal fee.
The PR's automated test output shows "5% epoch fee, 0.1% withdrawal fee", but the current code only generates the epoch fee string. The withdrawal fee (stakeWithdrawalFee / solWithdrawalFee) is not extracted in getStakePoolInfo and therefore unavailable here. Consider extracting it in getStakePoolInfo and including it in poolMeta to match the expected output.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adaptors/stkesol-by-sol-strategies/index.js` around lines 22 - 24,
getStakePoolInfo currently doesn't populate a withdrawal fee field, so poolMeta
only shows epochFee; update getStakePoolInfo to extract the withdrawal fee (use
stakePool.stakeWithdrawalFee or stakePool.solWithdrawalFee as present) calculate
it as a percentage string similar to epochFee (e.g.,
((numerator/denominator)*100).toFixed(1) + '% withdrawal fee'), add that value
to the returned object (e.g., withdrawalFee or solWithdrawalFee), and then
include that field in poolMeta so the output becomes "X% epoch fee, Y%
withdrawal fee" alongside existing epoch fee formatting.
| symbol: 'STKESOL', | ||
| tvlUsd: stakePool.tvlSol * solPrice, | ||
| apyBase, | ||
| underlyingTokens: [STKESOL_MINT], |
There was a problem hiding this comment.
underlyingTokens is set to the LST mint instead of SOL.
STKESOL_MINT is the liquid staking token itself, not the deposited asset. For an SPL stake pool, users deposit SOL in exchange for the pool token, so underlyingTokens should be [SOL]. The SOL constant is already defined at Line 6 but is unused here. The PR's own automated test output also confirms the expected value is 'So11111111111111111111111111111111111111112'.
🐛 Proposed fix
- underlyingTokens: [STKESOL_MINT],
+ underlyingTokens: [SOL],📝 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.
| underlyingTokens: [STKESOL_MINT], | |
| underlyingTokens: [SOL], |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adaptors/stkesol-by-sol-strategies/index.js` at line 34, The field
underlyingTokens currently uses STKESOL_MINT (the LST) but must reference the
deposited asset SOL; update the config in the object that defines
underlyingTokens (replace STKESOL_MINT with SOL) so underlyingTokens: [SOL] is
used; ensure the SOL constant (already defined) is referenced and remove the
incorrect STKESOL_MINT usage in that underlyingTokens assignment.
There was a problem hiding this comment.
@0xkr3p, thank you for the clarification! I understand now that underlyingTokens is intentionally set to STKESOL_MINT (the LST itself) rather than SOL. This follows a similar pattern to other adapters where the token's own address is used for search and filtering purposes. I'll note this for future reference.
✏️ Learnings added
Learnt from: 0xkr3p
Repo: DefiLlama/yield-server PR: 2341
File: src/adaptors/stkesol-by-sol-strategies/index.js:34-34
Timestamp: 2026-02-18T11:33:46.083Z
Learning: In src/adaptors/stkesol-by-sol-strategies/index.js, the `underlyingTokens` field is intentionally set to `STKESOL_MINT` (the LST's own address) to help with search and filtering. It's expected to be the same as the pool address, not the underlying SOL token.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: 0xkr3p
Repo: DefiLlama/yield-server PR: 2365
File: src/adaptors/reserve-protocol/index.js:94-94
Timestamp: 2026-02-18T09:16:55.042Z
Learning: In src/adaptors/reserve-protocol/index.js, the `underlyingTokens` field is intentionally set to the rtoken's own address (`[rtoken.id.toLowerCase()]`) to help with search and filtering. It's expected to be the same as the pool address.
There was a problem hiding this comment.
🧹 Nitpick comments (4)
src/adaptors/utils.js (4)
571-601: Parallelize the two independent RPC calls.
getSolanaAccountInfo(line 572) andgetEpochInfo(line 592) are independent; running them sequentially doubles the wall-clock latency for every call.♻️ Proposed refactor
- const stakePoolAccountData = await getSolanaAccountInfo(stakePoolAddress, rpcUrl); - - // Decode using official SPL stake pool layout - const stakePool = StakePoolLayout.decode(stakePoolAccountData); - - // ... BigInt conversions ... - - // Fetch current epoch info for epochs-per-year calculation - const epochResponse = await axios.post(rpcUrl, { - jsonrpc: '2.0', - id: 1, - method: 'getEpochInfo', - params: [{ commitment: 'confirmed' }], - }, { - headers: { 'Content-Type': 'application/json' }, - }); - const currentEpoch = epochResponse.data.result?.epoch || 0; + const [stakePoolAccountData, epochResponse] = await Promise.all([ + getSolanaAccountInfo(stakePoolAddress, rpcUrl), + axios.post(rpcUrl, { + jsonrpc: '2.0', + id: 1, + method: 'getEpochInfo', + params: [{ commitment: 'confirmed' }], + }, { headers: { 'Content-Type': 'application/json' } }), + ]); + + const stakePool = StakePoolLayout.decode(stakePoolAccountData); + // ... BigInt conversions ... + const currentEpoch = epochResponse.data.result?.epoch || 0;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/adaptors/utils.js` around lines 571 - 601, getStakePoolInfo currently awaits getSolanaAccountInfo before making the RPC call for getEpochInfo, doubling latency; start both independent async operations in parallel and await them together (e.g., initiate getSolanaAccountInfo(stakePoolAddress, rpcUrl) and the axios.post RPC for getEpochInfo immediately, then use Promise.all to await both), then continue to decode StakePoolLayout.decode using the fetched stakePoolAccountData and read currentEpoch from the RPC response; update references to getSolanaAccountInfo, StakePoolLayout.decode, and the axios.post/getEpochInfo call accordingly.
603-605:epochsPerYearuses a historical average that understates the current rate.Dividing
currentEpochby years-since-genesis gives a lifetime average. Early Solana epochs were shorter/disrupted, so this underestimates the current rate (~150–182 epochs/year). A more accurate method is to derive epoch duration fromgetEpochSchedule(slotsPerEpoch× slot time ≈ 400 ms):// Alternative: use on-chain schedule // epochDurationMs = slotsPerEpoch * 400; // ~2 days for 432,000 slots // epochsPerYear = (365.25 * 24 * 60 * 60 * 1000) / epochDurationMs;For the current adapter the magnitude error is small (~10–20%), but worth tracking as APY precision improves.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/adaptors/utils.js` around lines 603 - 605, The current epochsPerYear calculation uses a lifetime average (currentEpoch / yearsSinceGenesis) which understates the present epoch rate; instead call the on-chain schedule (getEpochSchedule) to obtain slotsPerEpoch and compute epochDurationMs = slotsPerEpoch * slotDurationMs (use a configurable slotDurationMs default ~400) then compute epochsPerYear = (365.25 * 24 * 60 * 60 * 1000) / epochDurationMs; update the logic around epochsPerYear (and remove reliance on SOLANA_GENESIS_MS for rate calculation) and keep currentEpoch only for lifetime metrics.
622-646:lastUpdateEpochis silently ignored — stale epoch data can overestimate APY.
stakePoolInfocontainslastUpdateEpoch, butcalcSolanaLstApydoesn't destructure or check it. If the pool'sUpdateStakePoolBalancetransaction was skipped for N epochs,epochGrowthreflects N epochs of compounding but is annualized as a single epoch, inflating the reported APY by roughly a factor of N.To fix this, expose
currentEpochfromgetStakePoolInfoand use the elapsed epoch count to normalize:In
getStakePoolInfo:return { ... lastUpdateEpoch: Number(lastUpdateEpoch), + currentEpoch, epochFee, epochsPerYear, };In
calcSolanaLstApy:const { - totalLamports, poolTokenSupply, + totalLamports, poolTokenSupply, currentEpoch, lastUpdateEpoch, lastEpochTotalLamports, lastEpochPoolTokenSupply, epochsPerYear, } = stakePoolInfo; + // Guard: stale update — delta spans more than one epoch + const epochsElapsed = currentEpoch - lastUpdateEpoch; + if (epochsElapsed < 1) return null; if (!lastEpochTotalLamports || !lastEpochPoolTokenSupply || !epochsPerYear) { return null; } const prevRate = lastEpochTotalLamports / lastEpochPoolTokenSupply; const currRate = totalLamports / poolTokenSupply; if (!prevRate || !currRate || currRate <= prevRate) { return null; } - const epochGrowth = currRate / prevRate; + // Normalize growth to a single-epoch rate before annualizing + const epochGrowth = Math.pow(currRate / prevRate, 1 / epochsElapsed); const apy = (Math.pow(epochGrowth, epochsPerYear) - 1) * 100;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/adaptors/utils.js` around lines 622 - 646, calcSolanaLstApy is ignoring lastUpdateEpoch/currentEpoch which lets skipped-update pools inflate APY; update calcSolanaLstApy to destructure lastUpdateEpoch and currentEpoch from stakePoolInfo, compute elapsedEpochs = currentEpoch - lastUpdateEpoch, guard that elapsedEpochs is a positive number, then derive per-epoch growth as Math.pow(currRate / prevRate, 1 / elapsedEpochs) (instead of using currRate/prevRate directly), and finally annualize with epochsPerYear: apy = (Math.pow(perEpochGrowth, epochsPerYear) - 1) * 100; return null on any invalid values (missing epochs, nonpositive elapsedEpochs, nonfinite rates) so the function (calcSolanaLstApy) correctly normalizes growth across skipped epochs.
568-569: Moverequireto the top of the file with other imports.All other
requirecalls are at lines 1–6; placing this one at line 569 is inconsistent and makes the dependency non-obvious to readers.♻️ Suggested move
const { default: BigNumber } = require('bignumber.js'); +const { StakePoolLayout } = require('@solana/spl-stake-pool'); exports.formatAddress = ...-// SPL Stake Pool data decoder using official library -const { StakePoolLayout } = require('@solana/spl-stake-pool'); - exports.getStakePoolInfo = async ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/adaptors/utils.js` around lines 568 - 569, Move the dynamic require for StakePoolLayout up with the other module imports: replace the late require('@solana/spl-stake-pool') call and add const { StakePoolLayout } = require('@solana/spl-stake-pool'); alongside the other requires at the top of utils.js so the dependency is declared consistently; remove the original trailing require statement to avoid duplication and run the linter/format step to ensure imports remain ordered and no unused-vars warnings are introduced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/adaptors/utils.js`:
- Around line 571-601: getStakePoolInfo currently awaits getSolanaAccountInfo
before making the RPC call for getEpochInfo, doubling latency; start both
independent async operations in parallel and await them together (e.g., initiate
getSolanaAccountInfo(stakePoolAddress, rpcUrl) and the axios.post RPC for
getEpochInfo immediately, then use Promise.all to await both), then continue to
decode StakePoolLayout.decode using the fetched stakePoolAccountData and read
currentEpoch from the RPC response; update references to getSolanaAccountInfo,
StakePoolLayout.decode, and the axios.post/getEpochInfo call accordingly.
- Around line 603-605: The current epochsPerYear calculation uses a lifetime
average (currentEpoch / yearsSinceGenesis) which understates the present epoch
rate; instead call the on-chain schedule (getEpochSchedule) to obtain
slotsPerEpoch and compute epochDurationMs = slotsPerEpoch * slotDurationMs (use
a configurable slotDurationMs default ~400) then compute epochsPerYear = (365.25
* 24 * 60 * 60 * 1000) / epochDurationMs; update the logic around epochsPerYear
(and remove reliance on SOLANA_GENESIS_MS for rate calculation) and keep
currentEpoch only for lifetime metrics.
- Around line 622-646: calcSolanaLstApy is ignoring lastUpdateEpoch/currentEpoch
which lets skipped-update pools inflate APY; update calcSolanaLstApy to
destructure lastUpdateEpoch and currentEpoch from stakePoolInfo, compute
elapsedEpochs = currentEpoch - lastUpdateEpoch, guard that elapsedEpochs is a
positive number, then derive per-epoch growth as Math.pow(currRate / prevRate, 1
/ elapsedEpochs) (instead of using currRate/prevRate directly), and finally
annualize with epochsPerYear: apy = (Math.pow(perEpochGrowth, epochsPerYear) -
1) * 100; return null on any invalid values (missing epochs, nonpositive
elapsedEpochs, nonfinite rates) so the function (calcSolanaLstApy) correctly
normalizes growth across skipped epochs.
- Around line 568-569: Move the dynamic require for StakePoolLayout up with the
other module imports: replace the late require('@solana/spl-stake-pool') call
and add const { StakePoolLayout } = require('@solana/spl-stake-pool'); alongside
the other requires at the top of utils.js so the dependency is declared
consistently; remove the original trailing require statement to avoid
duplication and run the linter/format step to ensure imports remain ordered and
no unused-vars warnings are introduced.
Summary by CodeRabbit
New Features
Chores