Skip to content

Commit b75ee9d

Browse files
committed
refactor(ploutos): deduplicate opportunities, improve Merkl reward integration logic
1 parent 0f4a0e4 commit b75ee9d

File tree

1 file changed

+58
-44
lines changed

1 file changed

+58
-44
lines changed

src/adaptors/ploutos-money/index.js

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Ploutos money market (Aave v3 fork) — yield adapter with Merkl integration via Map (clean, SES-safe)
1+
// Ploutos Money Market (Aave v3 fork) — yield adapter with Merkl integration (dedup by opportunity id, SES-safe)
22
// project: 'ploutos-money'
33

44
const axios = require('axios')
@@ -25,7 +25,7 @@ const CHAIN_NAME = {
2525
hemi: 'Hemi',
2626
}
2727

28-
// chainIds (актуальные)
28+
// chain IDs
2929
const CHAIN_ID = {
3030
base: 8453,
3131
arbitrum: 42161,
@@ -48,16 +48,17 @@ const RAY = 1e27
4848
const aprRayToDecimal = (ray) => Number(ray) / RAY
4949
const aprToApyDecimal = (apr) => Math.pow(1 + apr / 365, 365) - 1
5050

51-
// ---------- utils ----------
52-
const WARN = (...a) => console.warn('[ploutos]', ...a)
51+
// ---------- helpers ----------
5352
const setToArray = (s) => Array.from(s ? s.values() : [])
5453

55-
// Вытягивает первое валидное 0x-адресное вхождение из «грязной» строки (SES-safe)
54+
/**
55+
* Extracts first valid 0x-address from any string (SES-safe)
56+
*/
5657
function extractAddrLoose(x) {
5758
if (x == null) return ''
5859
const s = String(x)
5960
.toLowerCase()
60-
.replace(/[\u200b-\u200d\uFEFF]/g, '') // удалить zero-width
61+
.replace(/[\u200b-\u200d\uFEFF]/g, '') // remove zero-width chars
6162
.trim()
6263
const m = s.match(/0x[0-9a-f]{40}/i)
6364
return m ? m[0] : ''
@@ -76,16 +77,20 @@ async function fetchMerkl() {
7677
})
7778
merklCache = Array.isArray(data) ? data : (data ? [data] : [])
7879
} catch (e) {
79-
WARN('Merkl fetch failed:', e?.message || e)
80+
console.warn('[ploutos]', 'Merkl fetch failed:', e?.message || e)
8081
merklCache = []
8182
}
8283
return merklCache
8384
}
8485

8586
/**
86-
* Индекс по ключу `${chainId}:${addressLower}`
87-
* Map<string, { supply:{apr:number,rewardTokens:string[]}, borrow:{apr:number,rewardTokens:string[]} }>
88-
* SES-safe: без спредов/for..of по Set
87+
* Build Merkl index by key `${chainId}:${addressLower}`
88+
* Value shape:
89+
* { supplyOps: Map<opId,{apr:number,rewardTokens:string[]}>, borrowOps: Map<...> }
90+
*
91+
* Deduplicated by opportunity ID (it.id) to avoid double-counting the same
92+
* opportunity that appears under both aToken and underlying addresses.
93+
* SES-safe: no direct iteration with spreads.
8994
*/
9095
async function buildMerklIndex() {
9196
const items = await fetchMerkl()
@@ -97,9 +102,11 @@ async function buildMerklIndex() {
97102
if (!chainId) continue
98103

99104
const side = String(it.type || '').toUpperCase().includes('BORROW') ? 'borrow' : 'supply'
100-
const apr = Number(it.apr || 0)
105+
const apr = Number(it.apr || 0)
106+
const opId = String(it.id || '')
107+
if (!opId) continue
101108

102-
// reward tokens
109+
// collect reward tokens
103110
const rewardSet = new Set()
104111
const br = (it.rewardsRecord && it.rewardsRecord.breakdowns) || []
105112
for (let j = 0; j < br.length; j++) {
@@ -108,13 +115,12 @@ async function buildMerklIndex() {
108115
}
109116
const rewardTokens = setToArray(rewardSet)
110117

111-
// keys
118+
// bind this opportunity to possible keys (identifier, explorerAddress, tokens)
112119
const keysRaw = new Set()
113120
const k1 = normAddr(it.identifier)
114121
const k2 = normAddr(it.explorerAddress)
115122
if (k1) keysRaw.add(k1)
116123
if (k2) keysRaw.add(k2)
117-
118124
const toks = Array.isArray(it.tokens) ? it.tokens : []
119125
for (let k = 0; k < toks.length; k++) {
120126
const a = normAddr(typeof toks[k] === 'string' ? toks[k] : toks[k]?.address)
@@ -124,14 +130,18 @@ async function buildMerklIndex() {
124130
const keysArr = setToArray(keysRaw).map(a => `${chainId}:${a}`)
125131
for (let qi = 0; qi < keysArr.length; qi++) {
126132
const key = keysArr[qi]
127-
const cur = index.get(key) || {
128-
supply: { apr: 0, rewardTokens: [] },
129-
borrow: { apr: 0, rewardTokens: [] },
133+
const cur = index.get(key) || { supplyOps: new Map(), borrowOps: new Map() }
134+
const bucket = side === 'borrow' ? cur.borrowOps : cur.supplyOps
135+
const ex = bucket.get(opId)
136+
if (ex) {
137+
// merge duplicate entries if the same op appears twice in API
138+
ex.apr += apr
139+
const s = new Set(ex.rewardTokens || [])
140+
for (let ri = 0; ri < rewardTokens.length; ri++) s.add(rewardTokens[ri])
141+
ex.rewardTokens = setToArray(s)
142+
} else {
143+
bucket.set(opId, { apr, rewardTokens })
130144
}
131-
cur[side].apr += apr
132-
const curSet = new Set(cur[side].rewardTokens)
133-
for (let rtIdx = 0; rtIdx < rewardTokens.length; rtIdx++) curSet.add(rewardTokens[rtIdx])
134-
cur[side].rewardTokens = setToArray(curSet)
135145
index.set(key, cur)
136146
}
137147
}
@@ -231,34 +241,39 @@ async function getApy(market) {
231241
const marketUrlParam = toMarketUrlParam(market)
232242
const url = `https://app.ploutos.money/reserve-overview/?underlyingAsset=${r.tokenAddress.toLowerCase()}&marketName=proto_${marketUrlParam}_v3`
233243

234-
// Merkl match by chainId + (aToken | underlying)
244+
// Merkl match by chainId + (aToken | underlying), deduplicated by opId
235245
const aTok = normAddr(aTokens[i].tokenAddress)
236246
const uTok = normAddr(r.tokenAddress)
237247

238248
const mAT = chainId ? merklMap.get(`${chainId}:${aTok}`) : undefined
239249
const mUA = chainId ? merklMap.get(`${chainId}:${uTok}`) : undefined
240250

241-
let apyReward
242-
let apyRewardBorrow
243-
const rewardSet = new Set()
244-
245-
if (mAT?.supply) {
246-
if (mAT.supply.apr > 0) apyReward = (apyReward || 0) + mAT.supply.apr
247-
for (const rt of mAT.supply.rewardTokens || []) rewardSet.add(rt)
248-
}
249-
if (mUA?.supply) {
250-
if (mUA.supply.apr > 0) apyReward = (apyReward || 0) + mUA.supply.apr
251-
for (const rt of mUA.supply.rewardTokens || []) rewardSet.add(rt)
252-
}
253-
if (mAT?.borrow) {
254-
if (mAT.borrow.apr > 0) apyRewardBorrow = (apyRewardBorrow || 0) + mAT.borrow.apr
255-
for (const rt of mAT.borrow.rewardTokens || []) rewardSet.add(rt)
256-
}
257-
if (mUA?.borrow) {
258-
if (mUA.borrow.apr > 0) apyRewardBorrow = (apyRewardBorrow || 0) + mUA.borrow.apr
259-
for (const rt of mUA.borrow.rewardTokens || []) rewardSet.add(rt)
251+
// Merge by unique opportunity ID for each side
252+
function unionOps(a, b, side) {
253+
const mapA = a ? (side === 'borrow' ? a.borrowOps : a.supplyOps) : null
254+
const mapB = b ? (side === 'borrow' ? b.borrowOps : b.supplyOps) : null
255+
const ids = new Set()
256+
if (mapA) mapA.forEach((_, id) => ids.add(id))
257+
if (mapB) mapB.forEach((_, id) => ids.add(id))
258+
259+
let aprSum = 0
260+
const rts = new Set()
261+
ids.forEach((id) => {
262+
const rec = (mapA && mapA.get(id)) || (mapB && mapB.get(id)) || null
263+
if (!rec) return
264+
aprSum += Number(rec.apr || 0)
265+
const tokens = rec.rewardTokens || []
266+
for (let t = 0; t < tokens.length; t++) rts.add(tokens[t])
267+
})
268+
return { apr: aprSum, rts: setToArray(rts) }
260269
}
261270

271+
const sup = unionOps(mAT, mUA, 'supply')
272+
const bor = unionOps(mAT, mUA, 'borrow')
273+
274+
const rewardUnion = new Set([...sup.rts, ...bor.rts])
275+
const rewardTokens = setToArray(rewardUnion)
276+
262277
const poolObj = {
263278
pool: `${aTokens[i].tokenAddress}-${(market === 'avax' ? 'avalanche' : market)}`.toLowerCase(),
264279
chain: chainOut,
@@ -275,10 +290,9 @@ async function getApy(market) {
275290
url,
276291
}
277292

278-
const rewardTokens = setToArray(rewardSet)
293+
if (sup.apr > 0) poolObj.apyReward = sup.apr
294+
if (bor.apr > 0) poolObj.apyRewardBorrow = bor.apr
279295
if (rewardTokens.length) poolObj.rewardTokens = rewardTokens
280-
if (apyReward > 0) poolObj.apyReward = apyReward // Merkl APR уже в процентах
281-
if (apyRewardBorrow > 0) poolObj.apyRewardBorrow = apyRewardBorrow
282296

283297
out.push(poolObj)
284298
}

0 commit comments

Comments
 (0)