-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: add DorkFi yield adapter (Algorand + Voi Network) #2787
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,195 @@ | ||||||||||
| /** | ||||||||||
| * DorkFi — DefiLlama Yield Server Adapter | ||||||||||
| * | ||||||||||
| * Reports per-market supply and borrow APY for all DorkFi lending pools | ||||||||||
| * on Algorand and Voi Network. | ||||||||||
| * | ||||||||||
| * Interest Rate Model (kinked utilization curve): | ||||||||||
| * utilization = totalScaledBorrows / totalScaledDeposits | ||||||||||
| * borrowRateBPS = borrowRate + utilization × slope (fields in bps) | ||||||||||
| * supplyRateBPS = borrowRateBPS × utilization × (1 − reserveFactor) | ||||||||||
| * APY = (1 + rateBPS/10000/365)^365 − 1 | ||||||||||
| */ | ||||||||||
|
|
||||||||||
| const axios = require('axios'); | ||||||||||
|
|
||||||||||
| const API_BASE = 'https://dorkfi-api.nautilus.sh'; | ||||||||||
| const NETWORK = { algorand: 'algorand-mainnet', voi: 'voi-mainnet' }; | ||||||||||
| const DL_CHAIN = { algorand: 'Algorand', voi: 'Voi Network' }; | ||||||||||
| const PROJECT = 'dorkfi'; | ||||||||||
| const REQUEST_TIMEOUT_MS = 30_000; | ||||||||||
|
|
||||||||||
| // ── Known market → token symbol mapping ────────────────────────────────────── | ||||||||||
| // Voi Pool A (47139778) and Pool B (47139781) | ||||||||||
| // Algorand Pool A (3333688282) and Pool B (3345940978) | ||||||||||
| // Algorand symbols resolved from on-chain ASA metadata | ||||||||||
|
|
||||||||||
| const MARKET_SYMBOLS = { | ||||||||||
| // Voi Pool A | ||||||||||
| 41877720: 'VOI', | ||||||||||
| 395614: 'aUSDC', | ||||||||||
| 420069: 'UNIT', | ||||||||||
| 47138068: 'WAD', | ||||||||||
| 40153155: 'POW', | ||||||||||
| 413153: 'aALGO', | ||||||||||
| 40153308: 'aETH', | ||||||||||
| 40153368: 'aWBTC', | ||||||||||
| 40153415: 'acbBTC', | ||||||||||
| // Voi Pool B | ||||||||||
| 300279: 'USDC', | ||||||||||
| 412682: 'ALGO', | ||||||||||
| 410111: 'USDT', | ||||||||||
| 419744: 'AVAX', | ||||||||||
| 302222: 'BNB', | ||||||||||
| 410811: 'wBTC', | ||||||||||
| 798968: 'MATIC', | ||||||||||
| 420024: 'LINK', | ||||||||||
| 8471125: 'SOL', | ||||||||||
| 8324600: 'DOT', | ||||||||||
| 828295: 'ADA', | ||||||||||
| 770561: 'DOGE', | ||||||||||
| // Algorand Pool A + B — symbols from ASA on-chain metadata | ||||||||||
| 3207744109: 'ALGO', | ||||||||||
| 3211820549: 'goBTC', | ||||||||||
| 3210682240: 'USDC', | ||||||||||
| 3220125024: 'UNIT', | ||||||||||
| 3080081069: 'POW', | ||||||||||
| 3210709899: 'aVOI', | ||||||||||
| 3211827406: 'WBTC', | ||||||||||
| 3211806149: 'goETH', | ||||||||||
| 3211811648: 'WETH', | ||||||||||
| 3211838479: 'LINK', | ||||||||||
| 3211883276: 'SOL', | ||||||||||
| 3211885849: 'DOGE', | ||||||||||
| 3490783147: 'tALGO', | ||||||||||
| 3490854290: 'tALGO', | ||||||||||
| 3211805086: 'FINITE', | ||||||||||
| 3346185062: 'FOLKS', | ||||||||||
| 3212524778: 'COOP', | ||||||||||
| 3212773584: 'HAY', | ||||||||||
| 3346408431: 'WAD', | ||||||||||
| 3346881192: 'WAD', | ||||||||||
| 3211890928: 'HAY', | ||||||||||
| 3212768756: 'GOLD$', | ||||||||||
| 3212531816: 'SOL', | ||||||||||
| 3212534634: 'LINK', | ||||||||||
| 3212771255: 'FOLKS', | ||||||||||
| 3220347315: 'GOLD$', | ||||||||||
| 3333688448: 'WAD', | ||||||||||
| 3211740909: 'FINITE', | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| // ── Helpers ─────────────────────────────────────────────────────────────────── | ||||||||||
|
|
||||||||||
| async function fetchMarketData(chain) { | ||||||||||
| try { | ||||||||||
| const { data } = await axios.get(`${API_BASE}/market-data/${NETWORK[chain]}`, { | ||||||||||
| timeout: REQUEST_TIMEOUT_MS, | ||||||||||
| }); | ||||||||||
| if (!data.success) return []; | ||||||||||
| return data.data || []; | ||||||||||
| } catch (error) { | ||||||||||
| console.warn(`DorkFi market data unavailable for ${chain}: ${error.message}`); | ||||||||||
| return []; | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| async function fetchAnalyticsTVL(chain) { | ||||||||||
| try { | ||||||||||
| const { data } = await axios.get(`${API_BASE}/analytics/tvl/${NETWORK[chain]}`, { | ||||||||||
| timeout: REQUEST_TIMEOUT_MS, | ||||||||||
| }); | ||||||||||
| if (!data.success) return {}; | ||||||||||
| const map = {}; | ||||||||||
| for (const m of data.data.markets || []) map[`${m.appId}:${m.marketId}`] = m.tvl; | ||||||||||
| return map; | ||||||||||
| } catch (error) { | ||||||||||
| console.warn(`DorkFi TVL data unavailable for ${chain}: ${error.message}`); | ||||||||||
| return {}; | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function dedup(markets) { | ||||||||||
| const seen = new Map(); | ||||||||||
| for (const m of markets) { | ||||||||||
| const key = `${m.appId}:${m.marketId}`; | ||||||||||
| if (!seen.has(key) || m.lastUpdated > seen.get(key).lastUpdated) seen.set(key, m); | ||||||||||
| } | ||||||||||
| return [...seen.values()]; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // ── APY calculation ─────────────────────────────────────────────────────────── | ||||||||||
|
|
||||||||||
| function bpsToApy(rateBps) { | ||||||||||
| const r = rateBps / 10000; | ||||||||||
| return (Math.pow(1 + r / 365, 365) - 1) * 100; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function computeRates(market) { | ||||||||||
| const totalDep = Number(market.totalScaledDeposits || 0); | ||||||||||
| const totalBor = Number(market.totalScaledBorrows || 0); | ||||||||||
| if (totalDep === 0) return { borrowApy: 0, supplyApy: 0, utilization: 0 }; | ||||||||||
|
|
||||||||||
| const utilization = totalBor / totalDep; | ||||||||||
| const borrowRate = Number(market.borrowRate || 0); | ||||||||||
| const slope = Number(market.slope || 0); | ||||||||||
| const reserveFactor = Number(market.reserveFactor || 0) / 10000; | ||||||||||
|
|
||||||||||
| const borrowBps = borrowRate + utilization * slope; | ||||||||||
| const supplyBps = borrowBps * utilization * (1 - reserveFactor); | ||||||||||
|
|
||||||||||
| return { | ||||||||||
| borrowApy: parseFloat(bpsToApy(borrowBps).toFixed(4)), | ||||||||||
| supplyApy: parseFloat(bpsToApy(supplyBps).toFixed(4)), | ||||||||||
| utilization, | ||||||||||
| }; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function getUnderlyingTokens(market) { | ||||||||||
| return [String(market.marketId)]; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // ── Main ────────────────────────────────────────────────────────────────────── | ||||||||||
|
|
||||||||||
| const apy = async () => { | ||||||||||
| const results = []; | ||||||||||
|
|
||||||||||
| for (const chain of ['algorand', 'voi']) { | ||||||||||
| const [markets, tvlMap] = await Promise.all([ | ||||||||||
| fetchMarketData(chain), | ||||||||||
| fetchAnalyticsTVL(chain), | ||||||||||
| ]); | ||||||||||
|
|
||||||||||
| for (const market of dedup(markets)) { | ||||||||||
| const key = `${market.appId}:${market.marketId}`; | ||||||||||
| const hasTvl = Object.prototype.hasOwnProperty.call(tvlMap, key); | ||||||||||
| if (!hasTvl) { | ||||||||||
| console.warn(`DorkFi TVL missing for ${chain} market ${key}`); | ||||||||||
| continue; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const tvlUsd = tvlMap[key] || 0; | ||||||||||
| if (tvlUsd < 1) continue; | ||||||||||
|
Comment on lines
+171
to
+172
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Ensure
🛡️ Proposed fix- const tvlUsd = tvlMap[key] || 0;
+ const tvlUsd = Number(tvlMap[key]) || 0;
if (tvlUsd < 1) continue;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| const { borrowApy, supplyApy } = computeRates(market); | ||||||||||
| const symbol = MARKET_SYMBOLS[market.marketId] || `ASA-${market.marketId}`; | ||||||||||
|
|
||||||||||
| results.push({ | ||||||||||
| pool: `dorkfi-${chain}-${market.appId}-${market.marketId}`, | ||||||||||
| chain: DL_CHAIN[chain], | ||||||||||
| project: PROJECT, | ||||||||||
| symbol, | ||||||||||
| tvlUsd, | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just checking tvlUsd should be available liquidity, e.g. deposits - borrows |
||||||||||
| apyBase: supplyApy, | ||||||||||
| apyBaseBorrow: borrowApy, | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing these fields: |
||||||||||
| underlyingTokens: getUnderlyingTokens(market), | ||||||||||
| poolMeta: `Pool ${market.appId}`, | ||||||||||
| url: 'https://dork.fi', | ||||||||||
| }); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return results; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| module.exports = { apy, timeTravel: false, url: 'https://dork.fi' }; | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pls add
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we skip 'paused' markets?