-
Notifications
You must be signed in to change notification settings - Fork 15
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
APR Services - Add some yield tokens APR to linear pools #396
Changes from 11 commits
e6a65b3
1f18d5c
27ca26d
3d84c4b
6120e3a
65211ed
304988e
5cc075b
11cd519
a1a20dd
b3d5575
65047bb
19a48fc
bfe33c1
a7b904b
4cff5b7
1e74f53
6b00046
8c2432e
500f6c4
4d97f48
37366a4
32b5a52
74d2bce
8c04318
8f040cd
85f76fd
c509044
3d568ca
ae0d54c
7913842
98e9df6
fd7f136
a836720
114386d
9ac7a10
a5ca109
97a795f
ea59577
1a9352e
c174806
b00f57d
21cd9ae
1261ec6
5563895
e68f06a
c511a9b
57099bf
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 |
---|---|---|
@@ -1,5 +1,21 @@ | ||
import { Express } from 'express'; | ||
import { IbTokensAprService } from "../pool/lib/apr-data-sources/ib-tokens-apr.service"; | ||
import { networkContext } from "../network/network-context.service"; | ||
import { prismaPoolWithExpandedNesting } from "../../prisma/prisma-types"; | ||
import { prisma } from "../../prisma/prisma-client"; | ||
import { ibYieldAprHandlers } from "../pool/lib/apr-data-sources/ib-yield-apr-handlers/ib-yield-apr-handlers"; | ||
|
||
export function loadRestRoutesBalancer(app: Express) { | ||
app.use('/health', (req, res) => res.sendStatus(200)); | ||
app.use('/test', async (req, res) => { | ||
const pools = await prisma.prismaPool.findMany({ | ||
...prismaPoolWithExpandedNesting, | ||
where: { chain: networkContext.chain }, | ||
}); | ||
const ibTokensAprService = new IbTokensAprService(ibYieldAprHandlers); | ||
await ibTokensAprService.updateAprForPools(pools); | ||
|
||
return res.sendStatus(200) | ||
}); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
import { Dictionary } from 'lodash' | ||
|
||
export interface YearnVault { | ||
inception: number; | ||
address: string; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { PoolAprService } from "../../pool-types"; | ||
import { PrismaPoolWithExpandedNesting } from "../../../../prisma/prisma-types"; | ||
import { prisma } from "../../../../prisma/prisma-client"; | ||
import { networkContext } from "../../../network/network-context.service"; | ||
import { prismaBulkExecuteOperations } from "../../../../prisma/prisma-util"; | ||
import { PrismaPoolAprItemGroup } from "@prisma/client"; | ||
import { TokenApr } from "./ib-yield-apr-handlers/types"; | ||
import { IbYieldAprHandlers } from "./ib-yield-apr-handlers/ib-yield-apr-handlers"; | ||
|
||
export class IbTokensAprService implements PoolAprService { | ||
|
||
constructor(private readonly ibYieldAprHandlers: IbYieldAprHandlers) {} | ||
|
||
getAprServiceName(): string { | ||
return "IbTokensAprService"; | ||
} | ||
|
||
public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise<void> { | ||
const operations: any[] = []; | ||
const aprs = await this.fetchYieldTokensApr(); | ||
const tokenYieldPools = pools.filter((pool) => { | ||
return pool.tokens.find((token) => { | ||
return Array.from(aprs.keys()).map((key) => key.toLowerCase()).includes(token.address.toLowerCase()); | ||
}) | ||
} | ||
); | ||
for (const pool of tokenYieldPools) { | ||
for (const token of pool.tokens) { | ||
if ((aprs.get(token.address) !== undefined)) { | ||
const tokenSymbol = token.token.symbol; | ||
const itemId = `${ pool.id }-${ tokenSymbol }-yield-apr` | ||
|
||
operations.push(prisma.prismaPoolAprItem.upsert({ | ||
where: { id_chain: { id: itemId, chain: networkContext.chain } }, | ||
create: { | ||
id: itemId, | ||
chain: networkContext.chain, | ||
poolId: pool.id, | ||
title: `${ tokenSymbol } APR`, | ||
apr: aprs.get(token.address)?.val ?? 0, | ||
group: (aprs.get(token.address)?.group as PrismaPoolAprItemGroup) ?? null, | ||
type: pool.type === 'LINEAR' ? 'LINEAR_BOOSTED' : 'IB_YIELD', | ||
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. that is not enough, you'll need to check if it is the wrapped token when it is in a linear pool. only then it is linear_boosted, otherwise its IB_yield. E.g. for a wsteth/rfWSTETH linear pool |
||
}, | ||
update: { | ||
title: `${ tokenSymbol } APR`, | ||
apr: aprs.get(token.address)?.val | ||
}, | ||
})); | ||
} | ||
} | ||
} | ||
|
||
await prismaBulkExecuteOperations(operations); | ||
} | ||
|
||
private async fetchYieldTokensApr(): Promise<Map<string, TokenApr>> { | ||
const data = await this.ibYieldAprHandlers.getHandlersAprs() | ||
return new Map<string, TokenApr>( | ||
data.filter((apr) => !isNaN(apr.val)).map((apr) => [apr.address, apr]) | ||
); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { AprHandler, TokenApr } from "./types"; | ||
import { aprHandlers } from "./sources"; | ||
|
||
export class IbYieldAprHandlers { | ||
|
||
private handlers: AprHandler[] = []; | ||
constructor(handlers: AprHandler[]) { | ||
this.handlers = handlers; | ||
} | ||
|
||
getHandlersAprs = async (): Promise<TokenApr[]> => { | ||
const aprPromises = this.handlers.map(async (handler) => { | ||
const fetchedResponse: { [key: string]: number } = await handler.getAprs() | ||
return Object.entries(fetchedResponse).map(([address, aprValue]) => ({ | ||
val: aprValue, | ||
group: handler.group, | ||
address | ||
})) | ||
}); | ||
const res = Array(this.handlers.length) | ||
for (const [index, aprPromise] of aprPromises.entries()) { | ||
res[index] = await aprPromise | ||
} | ||
return res.flat(); | ||
} | ||
} | ||
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. Make this a proper function of this class |
||
|
||
export const ibYieldAprHandlers = new IbYieldAprHandlers(aprHandlers); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import axios from "axios"; | ||
import { ReserveResponse } from "./types"; | ||
import { | ||
aaveTokensV2Mainnet, | ||
aaveTokensV2Polygon, | ||
aaveTokensV3Arbitrum, | ||
aaveTokensV3Mainnet, | ||
aaveTokensV3Polygon, underlyingTokensArbitrum, | ||
underlyingTokensMainnet, | ||
underlyingTokensPolygon, | ||
wrappedAaveTokensV2Mainnet, | ||
wrappedAaveTokensV2Polygon, | ||
wrappedAaveTokensV3Arbitrum, | ||
wrappedAaveTokensV3Mainnet, | ||
wrappedAaveTokensV3Polygon | ||
} from "./tokens"; | ||
import { AprHandler } from "../../types"; | ||
|
||
class AaveAprHandler implements AprHandler { | ||
|
||
wrappedTokens: Map<string, string> | ||
aaveTokens: Map<string, string> | ||
underlyingTokens: Map<string, string> | ||
subgraphUrl: string | ||
|
||
readonly group = 'AAVE'; | ||
|
||
readonly query = `query getReserves($aTokens: [String!], $underlyingAssets: [Bytes!]) { | ||
reserves( | ||
where: { | ||
aToken_in: $aTokens | ||
underlyingAsset_in: $underlyingAssets | ||
isActive: true | ||
} | ||
) { | ||
id | ||
underlyingAsset | ||
liquidityRate | ||
} | ||
}` | ||
|
||
constructor(wrappedTokens: Map<string, string>, | ||
aaveTokens: Map<string, string>, | ||
underlyingTokens: Map<string, string>, | ||
subgraphUrl: string | ||
) { | ||
this.wrappedTokens = wrappedTokens | ||
this.aaveTokens = aaveTokens | ||
this.underlyingTokens = underlyingTokens | ||
this.subgraphUrl = subgraphUrl | ||
} | ||
|
||
async getAprs() { | ||
try { | ||
const requestQuery = { | ||
operationName: 'getReserves', | ||
query: this.query, | ||
variables: { | ||
aTokens: Array.from(this.aaveTokens.values()), | ||
underlyingAssets: Array.from(this.underlyingTokens.values()), | ||
}, | ||
} | ||
const { data } = await axios({ | ||
url: this.subgraphUrl, | ||
method: "post", | ||
data: requestQuery, | ||
headers: { "Content-Type": "application/json" } | ||
}) | ||
const { | ||
data: { reserves }, | ||
} = data as ReserveResponse | ||
|
||
const aprsByUnderlyingAddress = Object.fromEntries(reserves.map((r) => [ | ||
r.underlyingAsset, | ||
// Note: our assumption is frontend usage, this service is not a good source where more accuracy is needed. | ||
// Converting from aave ray number (27 digits) to bsp | ||
// essentially same as here: | ||
// https://github.com/aave/aave-utilities/blob/master/packages/math-utils/src/formatters/reserve/index.ts#L231 | ||
Number(r.liquidityRate.slice(0, 27)) / 1e27, | ||
])) | ||
const aprEntries = Object.fromEntries( | ||
Array.from(this.underlyingTokens.entries()) | ||
//Removing undefined aprs | ||
.filter(([, address]) => !!aprsByUnderlyingAddress[address]) | ||
//Mapping aprs by wrapped instead of underlying addresses | ||
.map(([underlyingTokenName, underlyingTokenAddress]) => [ | ||
this.wrappedTokens.get('wa' + underlyingTokenName) as string, | ||
aprsByUnderlyingAddress[underlyingTokenAddress], | ||
])) | ||
return aprEntries; | ||
} catch (e) { | ||
console.error(`Failed to fetch Aave APR in subgraph ${ this.subgraphUrl }:`, e) | ||
return {} | ||
} | ||
} | ||
} | ||
|
||
export const aaveV2MainnetAprHandler = new AaveAprHandler(wrappedAaveTokensV2Mainnet, aaveTokensV2Mainnet, underlyingTokensMainnet, 'https://api.thegraph.com/subgraphs/name/aave/protocol-v2') | ||
export const aaveV2PolygonAprHandler = new AaveAprHandler(wrappedAaveTokensV2Polygon, aaveTokensV2Polygon, underlyingTokensPolygon, 'https://api.thegraph.com/subgraphs/name/aave/aave-v2-matic') | ||
export const aaveV3MainnetAprHandler = new AaveAprHandler(wrappedAaveTokensV3Mainnet, aaveTokensV3Mainnet, underlyingTokensMainnet, 'https://api.thegraph.com/subgraphs/name/aave/protocol-v3') | ||
export const aaveV3PolygonAprHandler = new AaveAprHandler(wrappedAaveTokensV3Polygon, aaveTokensV3Polygon, underlyingTokensPolygon, 'https://api.thegraph.com/subgraphs/name/aave/protocol-v3-polygon'); | ||
export const aaveV3ArbitrumAprHandler = new AaveAprHandler(wrappedAaveTokensV3Arbitrum, aaveTokensV3Arbitrum, underlyingTokensArbitrum, 'https://api.thegraph.com/subgraphs/name/aave/protocol-v3-arbitrum') | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
export const wrappedAaveTokensV2Mainnet = new Map<string, string>(Object.entries({ | ||
waUSDT: '0xf8fd466f12e236f4c96f7cce6c79eadb819abf58', | ||
waUSDC: '0xd093fa4fb80d09bb30817fdcd442d4d02ed3e5de', | ||
waDAI: '0x02d60b84491589974263d922d9cc7a3152618ef6', | ||
})); | ||
|
||
export const aaveTokensV2Mainnet = new Map<string, string>(Object.entries({ | ||
aUSDT: '0x3ed3b47dd13ec9a98b44e6204a523e766b225811', | ||
aUSDC: '0xbcca60bb61934080951369a648fb03df4f96263c', | ||
aDAI: '0x028171bca77440897b824ca71d1c56cac55b68a3', | ||
})); | ||
|
||
export const wrappedAaveTokensV2Polygon = new Map<string, string>(Object.entries({ | ||
waUSDT: '0x19c60a251e525fa88cd6f3768416a8024e98fc19', | ||
waUSDC: '0x221836a597948dce8f3568e044ff123108acc42a', | ||
waDAI: '0xee029120c72b0607344f35b17cdd90025e647b00', | ||
})); | ||
|
||
export const aaveTokensV2Polygon = new Map<string, string>(Object.entries({ | ||
aUSDT: '0x60d55f02a771d515e077c9c2403a1ef324885cec', | ||
aUSDC: '0x1a13f4ca1d028320a707d99520abfefca3998b7f', | ||
aDAI: '0x27f8d03b3a2196956ed754badc28d73be8830a6e', | ||
})); | ||
|
||
export const wrappedAaveTokensV3Mainnet = new Map<string, string>(Object.entries({ | ||
waUSDT: '0xa7e0e66f38b8ad8343cff67118c1f33e827d1455', | ||
waUSDC: '0x57d20c946a7a3812a7225b881cdcd8431d23431c', | ||
waDAI: '0x098256c06ab24f5655c5506a6488781bd711c14b', | ||
waWETH: '0x59463bb67ddd04fe58ed291ba36c26d99a39fbc6', | ||
})); | ||
|
||
export const aaveTokensV3Mainnet = new Map<string, string>(Object.entries({ | ||
aUSDT: '0x23878914efe38d27c4d67ab83ed1b93a74d4086a', | ||
aUSDC: '0x98c23e9d8f34fefb1b7bd6a91b7ff122f4e16f5c', | ||
aDAI: '0x018008bfb33d285247a21d44e50697654f754e63', | ||
aWETH: '0x4d5f47fa6a74757f35c14fd3a6ef8e3c9bc514e8' | ||
})); | ||
|
||
|
||
export const wrappedAaveTokensV3Polygon = new Map<string, string>(Object.entries({ | ||
waMATIC: '0x0d6135b2cfbae3b1c58368a93b855fa54fa5aae1', | ||
waUSDT: '0x7c76b6b3fe14831a39c0fec908da5f17180df677', | ||
waUSDC: '0x9719d867a500ef117cc201206b8ab51e794d3f82', | ||
waDAI: '0x27f8d03b3a2196956ed754badc28d73be8830a6e', | ||
waWETH: '0xa5bbf0f46b9dc8a43147862ba35c8134eb45f1f5', | ||
})); | ||
|
||
export const aaveTokensV3Polygon = new Map<string, string>(Object.entries({ | ||
aMATIC: '0x6d80113e533a2c0fe82eabd35f1875dcea89ea97', | ||
aUSDT: '0x60d55f02a771d515e077c9c2403a1ef324885cec', | ||
aUSDC: '0x1a13f4ca1d028320a707d99520abfefca3998b7f', | ||
aDAI: '0x27f8d03b3a2196956ed754badc28d73be8830a6e', | ||
aWETH: '0xe50fa9b3c56ffb159cb0fca61f5c9d750e8128c8', | ||
})); | ||
|
||
|
||
export const wrappedAaveTokensV3Arbitrum = new Map<string, string>(Object.entries({ | ||
waUSDT: '0x3c7680dfe7f732ca0279c39ff30fe2eafdae49db', | ||
waUSDC: '0xe719aef17468c7e10c0c205be62c990754dff7e5', | ||
waDAI: '0x345a864ac644c82c2d649491c905c71f240700b2', | ||
waWETH: '0x18c100415988bef4354effad1188d1c22041b046' | ||
})); | ||
|
||
export const aaveTokensV3Arbitrum = new Map<string, string>(Object.entries({ | ||
aUSDT: '0x6ab707aca953edaefbc4fd23ba73294241490620', | ||
aUSDC: '0x625e7708f30ca75bfd92586e17077590c60eb4cd', | ||
aDAI: '0x82e64f49ed5ec1bc6e43dad4fc8af9bb3a2312ee', | ||
aWETH: '0xe50fa9b3c56ffb159cb0fca61f5c9d750e8128c8', | ||
})); | ||
export const underlyingTokensMainnet = new Map<string, string>(Object.entries({ | ||
USDT: '0xdac17f958d2ee523a2206206994597c13d831ec7', | ||
USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', | ||
DAI: '0x6b175474e89094c44da98b954eedeac495271d0f', | ||
WETH: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' | ||
})); | ||
export const underlyingTokensPolygon = new Map<string, string>(Object.entries({ | ||
MATIC: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', | ||
USDT: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', | ||
USDC: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', | ||
DAI: '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', | ||
WETH: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619' | ||
})); | ||
|
||
export const underlyingTokensArbitrum = new Map<string, string>(Object.entries({ | ||
USDT: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', | ||
USDC: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', | ||
DAI: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', | ||
WETH: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', | ||
})); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export interface ReserveResponse { | ||
data: { | ||
reserves: [ | ||
{ | ||
underlyingAsset: string | ||
liquidityRate: string | ||
} | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
export const abi = [ | ||
{ | ||
inputs: [], | ||
name: "borrowRatePerTimestamp", | ||
outputs: [ | ||
{ | ||
internalType: "uint256", | ||
name: "", | ||
type: "uint256" | ||
} | ||
], | ||
stateMutability: "view", | ||
type: "function" | ||
} | ||
] as const |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
export const abi = [ | ||
{ | ||
inputs: [ | ||
{ | ||
internalType: 'int256', | ||
name: '_n', | ||
type: 'int256', | ||
}, | ||
], | ||
name: 'averageAPRAcrossLastNHarvests', | ||
outputs: [ | ||
{ | ||
internalType: 'int256', | ||
name: '', | ||
type: 'int256', | ||
}, | ||
], | ||
stateMutability: 'view', | ||
type: 'function', | ||
}, | ||
] as const |
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.
remove unused
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.
still there :)