Part 3: Parsing Aave's GraphQL loan data
Aave's GraphQL database is a trove of information. It also has its own intricacies. In this post, I'll show how I parsed its information into a friendly format in order to chart it in Vega. This is a continuation of my post on how to query a dApp's GraphQL data in React and TypeScript.
Basic GraphQL query
This query provides the data I need to get started:
{
borrows(first: 5) {
timestamp
amount
borrowRate
reserve {
name
symbol
decimals
liquidityRate
price {
priceInEth
}
}
}
}
Just like on the Aave subgraph Playground, the borrowRate
is in a format which I don't know how to parse. I'll have to figure that out later. It's actually not absolutely required for the questions I was trying to answer. Same for liquidityRate
, so I'll remove them for now. We can come back to them later.
Now my query in my React code looks like this:
const BORROWS = gql`
query GetBorrows {
borrows(first: 5) {
timestamp
amount
reserve {
name
symbol
decimals
price {
priceInEth
}
}
}
}
`;
And my rendering code looks like this:
function ExchangeRates() {
const { loading, error, data } = useQuery(BORROWS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
console.log(data.borrows);
return data.borrows.map(({ timestamp, amount, reserve } : GetBorrows_borrows) => (
<div key={timestamp}>
<p>
{timestamp}: {Number(amount) / Math.pow(10, reserve.decimals) } {reserve.symbol}
</p>
</div>
));
}
My data and Aave Watch seem to differ though.
Let's convert the timestamps to actual dates. With a bit of trial and error, I figured that you can parse the date like this:
new Date(timestamp * 1000).toDateString()
And here's the resulting data:
Fri Dec 10 2021: 5000 USDC
Mon Oct 04 2021: 15 WETH
Tue May 18 2021: 41501.629937448706 DAI
Tue Jun 01 2021: 7000000 USDT
Fri Apr 30 2021: 15000 USDC
Now it makes sense. The borrow info I get is not sorted.
To figure out how to sort the borrow information, I need to check Aave subgraph's schema. It is not exposed in the pre-processed schema at https://github.com/aave/protocol-v2-subgraph/blob/main/schema.graphql. But the fully generated schema shows it:
enum OrderDirection {
asc
desc
}
enum Borrow_orderBy {
id
pool
user
onBehalfOf
reserve
userReserve
amount
borrowRate
borrowRateMode
referrer
timestamp
stableTokenDebt
variableTokenDebt
}
input Borrow_filter {
id: ID
id_not: ID
id_gt: ID
id_lt: ID
id_gte: ID
id_lte: ID
id_in: [ID!]
id_not_in: [ID!]
pool: String
pool_not: String
pool_gt: String
pool_lt: String
pool_gte: String
pool_lte: String
pool_in: [String!]
pool_not_in: [String!]
pool_contains: String
pool_not_contains: String
pool_starts_with: String
pool_not_starts_with: String
pool_ends_with: String
pool_not_ends_with: String
user: String
user_not: String
user_gt: String
user_lt: String
user_gte: String
user_lte: String
user_in: [String!]
user_not_in: [String!]
user_contains: String
user_not_contains: String
user_starts_with: String
user_not_starts_with: String
user_ends_with: String
user_not_ends_with: String
onBehalfOf: String
onBehalfOf_not: String
onBehalfOf_gt: String
onBehalfOf_lt: String
onBehalfOf_gte: String
onBehalfOf_lte: String
onBehalfOf_in: [String!]
onBehalfOf_not_in: [String!]
onBehalfOf_contains: String
onBehalfOf_not_contains: String
onBehalfOf_starts_with: String
onBehalfOf_not_starts_with: String
onBehalfOf_ends_with: String
onBehalfOf_not_ends_with: String
reserve: String
reserve_not: String
reserve_gt: String
reserve_lt: String
reserve_gte: String
reserve_lte: String
reserve_in: [String!]
reserve_not_in: [String!]
reserve_contains: String
reserve_not_contains: String
reserve_starts_with: String
reserve_not_starts_with: String
reserve_ends_with: String
reserve_not_ends_with: String
userReserve: String
userReserve_not: String
userReserve_gt: String
userReserve_lt: String
userReserve_gte: String
userReserve_lte: String
userReserve_in: [String!]
userReserve_not_in: [String!]
userReserve_contains: String
userReserve_not_contains: String
userReserve_starts_with: String
userReserve_not_starts_with: String
userReserve_ends_with: String
userReserve_not_ends_with: String
amount: BigInt
amount_not: BigInt
amount_gt: BigInt
amount_lt: BigInt
amount_gte: BigInt
amount_lte: BigInt
amount_in: [BigInt!]
amount_not_in: [BigInt!]
borrowRate: BigInt
borrowRate_not: BigInt
borrowRate_gt: BigInt
borrowRate_lt: BigInt
borrowRate_gte: BigInt
borrowRate_lte: BigInt
borrowRate_in: [BigInt!]
borrowRate_not_in: [BigInt!]
borrowRateMode: BorrowRateMode
borrowRateMode_not: BorrowRateMode
borrowRateMode_in: [BorrowRateMode!]
borrowRateMode_not_in: [BorrowRateMode!]
referrer: String
referrer_not: String
referrer_gt: String
referrer_lt: String
referrer_gte: String
referrer_lte: String
referrer_in: [String!]
referrer_not_in: [String!]
referrer_contains: String
referrer_not_contains: String
referrer_starts_with: String
referrer_not_starts_with: String
referrer_ends_with: String
referrer_not_ends_with: String
timestamp: Int
timestamp_not: Int
timestamp_gt: Int
timestamp_lt: Int
timestamp_gte: Int
timestamp_lte: Int
timestamp_in: [Int!]
timestamp_not_in: [Int!]
stableTokenDebt: BigInt
stableTokenDebt_not: BigInt
stableTokenDebt_gt: BigInt
stableTokenDebt_lt: BigInt
stableTokenDebt_gte: BigInt
stableTokenDebt_lte: BigInt
stableTokenDebt_in: [BigInt!]
stableTokenDebt_not_in: [BigInt!]
variableTokenDebt: BigInt
variableTokenDebt_not: BigInt
variableTokenDebt_gt: BigInt
variableTokenDebt_lt: BigInt
variableTokenDebt_gte: BigInt
variableTokenDebt_lte: BigInt
variableTokenDebt_in: [BigInt!]
variableTokenDebt_not_in: [BigInt!]
}
Query {
...
borrows(skip: Int = 0, first: Int = 100, orderBy: Borrow_orderBy, orderDirection: OrderDirection, where: Borrow_filter, ...
...
}
If you want to know how I generated it, take a look at my previous post, more specifically the part where I compiled the tool with Cargo and ran graph-graphql
.
If I add the sorting info, the query looks like this:
const BORROWS = gql`
query GetBorrows {
borrows(first: 5, orderBy: timestamp) {
timestamp
amount
reserve {
name
symbol
decimals
price {
priceInEth
}
}
}
}
`;
Nearly there! Looks like it is querying it in ascending order, but we want it in descending order. Tweaked the query accordingly:
const BORROWS = gql`
query GetBorrows {
borrows(first: 5, orderBy: timestamp, orderDirection: desc) {
And now I am seeing the same data as Aave Watch!
The next step is to convert the amounts to Ether, then to USD.
Converting BigInt decimals to number
According to some websites online, a USD Coin today is worth 0.00025512 ETH. Also, according to Aave Watch, Aave's Oracle says that one ETH is worth $3,919.151.
If I query the info on the GraphQL subgraph:
{
priceOracles {
usdPriceEth
}
}
I get:
{
"data": {
"priceOracles": [
{
"usdPriceEth": "255157297889700"
}
]
}
}
How do you convert 255157297889700 to $3,919.151?
According to this project, one can convert BigInt to a DecimalBigInt like this (see code), DEFAULT_DECIMALS being 18 (see code):
export function fromBigInt(value: BigInt, decimals: i32 = DEFAULT_DECIMALS): BigDecimal {
let precision = BigInt.fromI32(10)
.pow(<u8>decimals)
.toBigDecimal()
return value.divDecimal(precision)
}
So just divide by a power of 10? I must be missing something.
Let's try with some more data. This time I'll check the borrow rates for USD Coin. The query looks like this:
query {
reserves {
name
variableBorrowRate
stableBorrowRate
}
}
The result is:
...
"reserves": [
...
{
"name": "Maker",
"stableBorrowRate": "30667806888971627742233250",
"variableBorrowRate": "467464822280139419563276"
},
{
"name": "USD Coin",
"stableBorrowRate": "108510146912845866609424453",
"variableBorrowRate": "37020293825691733218848905"
},
{
"name": "USD Coin",
"stableBorrowRate": "109143193722328678434486054",
"variableBorrowRate": "38286387444657356868972108"
},
...
]
}
}
Meanwhile Aave Watch shows:
In this case it does indeed seem like the number was divided by a power of 10. In this case, they were divided by 10 to the power of 27: 1.09e26 = 0.109e27 = 10.9%e27 and 3.8e25 = .038e27.
... Oh!! I was looking at the USD/Ether conversion the wrong way. The Oracle returns how many Ether a USD is worth. Which is a tiny amount. So if an Ether is worth $3,919.151, one USD should be worth 0.000255157303202 Ether. How did the Oracle return 255157297889700?
0.000255157303202
= 000255157303202e-15
= 255157303202e-15
= 255157303202000e-18
~ 255157297889700e-18
This time it looks like the power of 10 is 18. It'd be nice to check the source code of Aave later. For now I can go with these constants.
Ah and I forgot. The priceInEth of USDC is 254200000000000. Online they say that one USDC is worth 0.00024569492 Ether.
254200000000000
= .254200000000000e-15
= .000254200000000000e-18
The constant is e18 again. Now, the subgraph-toolkit I stumbled on earlier is starting to make sense. Also they seem to be handling BigInt
s like experts. I'm probably at risk of losing precision by using Number
s. Eventually I should try and reuse that library.
Here's the final code after all conversions are done. Thanks to MDN's article on exponentiation, I learned of the **
operator, which is equivalent to the old Math.pow
function:
const ethToUsd = 1 / (data.priceOracles[0].usdPriceEth * 10 ** -18);
return data.borrows.map(({ timestamp, amount, reserve } : GetBorrows_borrows) => {
const amountInCoin = Number(amount) / 10 ** reserve.decimals;
const coinInEth = Number(reserve.price.priceInEth) / 10 ** 18;
return (
<div key={timestamp}>
<p>
{new Date(timestamp * 1000).toDateString()}:
{amountInCoin} {reserve.symbol}, ie ${amountInCoin * coinInEth * ethToUsd}
</p>
</div>
);
});
And the corresponding output is:
Tue Dec 28 2021:20100 USDC, ie $19952.245447426398
Tue Dec 28 2021:20000 USDT, ie $19939.922528031355
Tue Dec 28 2021:200000 USDT, ie $199399.22528031355
Tue Dec 28 2021:50000 USDC, ie $49632.4513617572
Tue Dec 28 2021:5000000 USDT, ie $4984980.632007839
Generating a JSON
Now let's convert this to JSON so that we can feed it to Vega.
interface BorrowRow {
date: Date
coinSymbol: string
amountCoin: number
amountEth: number
amountUsd: number
}
function toRows(borrows: GetBorrows): BorrowRow[] {
const ethToUsd =
1 / (Number(borrows.priceOracles[0].usdPriceEth) * 10 ** -18);
return borrows.borrows.map(
({ timestamp, amount, reserve }: GetBorrows_borrows) => {
const amountInCoin = Number(amount) / 10 ** reserve.decimals;
const coinInEth = Number(reserve.price.priceInEth) / 10 ** 18;
return {
date: new Date(timestamp * 1000),
coinSymbol: reserve.symbol,
amountCoin: amountInCoin,
amountEth: amountInCoin * coinInEth,
amountUsd: amountInCoin * coinInEth * ethToUsd,
};
}
);
}
After running this code on the latest 1000 loans, I get the following JSON.