Skip to content

Commit 8ccecbc

Browse files
authored
Merge pull request #2175 from clydedevv/master
Add Supervaults APY adapter (Neutron)
2 parents 4ea0bf9 + 212975a commit 8ccecbc

File tree

1 file changed

+136
-0
lines changed

1 file changed

+136
-0
lines changed

src/adaptors/supervaults/index.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
const axios = require('axios');
2+
3+
const INDEXER = 'https://app.neutron.org/api/indexer/v2/vaults';
4+
5+
// Browser-like headers to pass WAF checks
6+
const HDRS = {
7+
Origin: 'https://app.neutron.org',
8+
Referer: 'https://app.neutron.org/earn',
9+
Accept: 'application/json, text/plain, */*',
10+
'User-Agent':
11+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
12+
};
13+
14+
// Bitcoin LST Denominations (for proper symbol display)
15+
// Maps IBC denoms to actual token symbols
16+
const BTC_LST_DENOMS = {
17+
'ibc/2EB30350120BBAFC168F55D0E65551A27A724175E8FBCC7B37F9A71618FE136B': 'FBTC',
18+
'ibc/B7BF60BB54433071B49D586F54BD4DED5E20BEFBBA91958E87488A761115106B': 'LBTC',
19+
'ibc/C0F284F165E6152F6DDDA900537C1BC8DA1EA00F03B9C9EC1841FA7E004EF7A3': 'solvBTC',
20+
'ibc/E2A000FD3EDD91C9429B473995CE2C7C555BCC8CFC1D0A3D02F514392B7A80E8': 'eBTC',
21+
'ibc/1075520501498E008B02FD414CD8079C0A2BAF9657278F8FB8F7D37A857ED668': 'pumpBTC',
22+
'ibc/3F1D988D9EEA19EB0F3950B4C19664218031D8BCE68CE7DE30F187D5ACEA0463': 'uniBTC',
23+
// Base assets
24+
'ibc/0E293A7622DC9A6439DB60E6D234B5AF446962E27CA3AB44D0590603DFF6968E': 'wBTC',
25+
'ibc/694A6B26A43A2FBECCFFEAC022DEACB39578E54207FDD32005CD976B57B98004': 'ETH',
26+
'ibc/A585C2D15DCD3B010849B453A2CFCB5E213208A5AB665691792684C26274304D': 'ETH',
27+
// maxBTC factory denom
28+
'factory/neutron17sp75wng9vl2hu3sf4ky86d7smmk3wle9gkts2gmedn9x4ut3xcqa5xp34/maxbtc': 'maxBTC',
29+
};
30+
31+
/**
32+
* Fetch vaults data with retry logic to handle transient WAF/CDN issues
33+
* @returns {Array} - Array of vault objects
34+
*/
35+
async function fetchVaults() {
36+
let lastErr;
37+
for (let i = 0; i < 3; i++) {
38+
try {
39+
const res = await axios.get(INDEXER, { headers: HDRS });
40+
const rows = Array.isArray(res?.data?.data) ? res.data.data : [];
41+
if (rows.length) return rows;
42+
} catch (e) {
43+
lastErr = e;
44+
// Exponential backoff: 500ms, 1000ms, 1500ms
45+
await new Promise((r) => setTimeout(r, 500 * (i + 1)));
46+
}
47+
}
48+
throw lastErr || new Error('Indexer fetch failed after retries');
49+
}
50+
51+
/**
52+
* Main APY calculation function
53+
* Methodology: Uses indexer-calculated vault performance over 30d window
54+
* - apy_vault_over_hold_30d accounts for fees earned vs. impermanent loss
55+
* - Already calculated by the indexer (complex CL position management)
56+
* - Can be negative if IL > fees earned
57+
* - Omits points, boosts, and locked rewards per DefiLlama guidelines
58+
*/
59+
async function apy() {
60+
const vaults = await fetchVaults();
61+
const pools = [];
62+
63+
for (const vault of vaults) {
64+
if (!vault) continue;
65+
66+
// Skip paused vaults
67+
if (vault.paused) continue;
68+
69+
// Skip test/inactive vaults (deposit_cap = 0 indicates test vault)
70+
if (
71+
!vault.deposit_cap ||
72+
vault.deposit_cap === '0' ||
73+
vault.deposit_cap === 0
74+
) {
75+
continue;
76+
}
77+
78+
// Extract TVL data (already in USD from indexer)
79+
const tvl0 = Number(vault.tvl_0) || 0;
80+
const tvl1 = Number(vault.tvl_1) || 0;
81+
const tvlUsd = tvl0 + tvl1;
82+
83+
// Skip if no meaningful TVL
84+
if (tvlUsd === 0) continue;
85+
86+
// Use indexer-calculated APY (vault performance vs. holding over 30d)
87+
// This accounts for:
88+
// - Trading fees earned
89+
// - Impermanent loss from concentrated liquidity position
90+
// - Position rebalancing effects
91+
// Note: Can be negative if IL > fees
92+
const apyVaultOverHold = vault.apy_vault_over_hold_30d;
93+
94+
// Convert from decimal to percentage (e.g., 0.05 -> 5%)
95+
// Set to null if not available (indexer needs time to calculate)
96+
let apyBase = null;
97+
if (apyVaultOverHold !== null && apyVaultOverHold !== undefined) {
98+
apyBase = Number(apyVaultOverHold) * 100;
99+
}
100+
101+
// Extract token information with proper symbol mapping
102+
const token0Denom = vault.token_0_denom;
103+
const token1Denom = vault.token_1_denom;
104+
105+
// Use mapping for BTC LSTs and known tokens, fallback to indexer symbol
106+
const token0Symbol = BTC_LST_DENOMS[token0Denom] || vault.token_0_symbol || '?';
107+
const token1Symbol = BTC_LST_DENOMS[token1Denom] || vault.token_1_symbol || '?';
108+
109+
// Build underlying tokens array (filter out nullish values)
110+
const underlyingTokens = [token0Denom, token1Denom].filter(Boolean);
111+
112+
// Construct pool URL (use vault-specific link if available, otherwise campaign page)
113+
const poolUrl = vault.contract_address
114+
? `https://app.neutron.org/bitcoin-summer?vault=${vault.contract_address}`
115+
: 'https://app.neutron.org/bitcoin-summer';
116+
117+
pools.push({
118+
pool: `${vault.contract_address}-neutron`.toLowerCase(),
119+
chain: 'Neutron',
120+
project: 'supervaults',
121+
symbol: `${token0Symbol}-${token1Symbol}`,
122+
tvlUsd,
123+
apyBase,
124+
underlyingTokens,
125+
url: poolUrl,
126+
});
127+
}
128+
129+
return pools;
130+
}
131+
132+
module.exports = {
133+
timetravel: false,
134+
apy,
135+
url: 'https://app.neutron.org/bitcoin-summer',
136+
};

0 commit comments

Comments
 (0)