Skip to content
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

Add investors api #26

Merged
merged 2 commits into from
Jun 18, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# beefy-clm-api

https://clm-api.beefy.finance/api/documentation
http://localhost:4000/api/documentation

https://clm-api.beefy.finance/api/v1/investor/0xb1F1000b4FCae7CD07370cE1A3E3b11270caC0dE/timeline
http://localhost:4000/api/v1/investor/0xb1F1000b4FCae7CD07370cE1A3E3b11270caC0dE/timeline

47 changes: 47 additions & 0 deletions src/queries/VaultInvestors.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
fragment Token on Token {
name
symbol
decimals
}

query VaultInvestors($clmAddress: String!, $first: Int! = 1000, $skip: Int! = 0) {
clmPositions(
first: $first
skip: $skip
orderBy: id
orderDirection: asc
where: { totalBalance_gt: 0, clm: $clmAddress }
) {
clm {
vaultAddress: id
managerToken {
...Token
}
underlyingToken0 {
...Token
}
underlyingToken1 {
...Token
}
rewardPoolToken {
...Token
}
managerTotalSupply

token0ToNativePrice
token1ToNativePrice
nativeToUSDPrice

underlyingAltAmount0
underlyingAltAmount1
underlyingMainAmount0
underlyingMainAmount1
}
investor {
userAddress: id
}
managerBalance
rewardPoolBalance
totalBalance
}
}
2 changes: 1 addition & 1 deletion src/routes/v1/investor.ts
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ export default async function (
const responseSchema = S.array().items(S.object());

const schema: FastifySchema = {
tags: ['v1'],
tags: ['investor'],
params: urlParamsSchema,
response: {
200: responseSchema,
116 changes: 111 additions & 5 deletions src/routes/v1/vault.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { FastifyInstance, FastifyPluginOptions, FastifySchema } from 'fastify';
import S from 'fluent-json-schema';
import { ChainId } from '../../config/chains';
import { addressSchema } from '../../schema/address';
import { getSdksForChain } from '../../utils/sdk';
import { getSdksForChain, paginateSdkCalls } from '../../utils/sdk';
import { getPeriodSeconds, Period, periodSchema } from '../../schema/period';
import { chainSchema } from '../../schema/chain';
import { bigintSchema } from '../../schema/bigint';
@@ -31,7 +31,7 @@ export default async function (
const responseSchema = S.array().items(S.object());

const schema: FastifySchema = {
tags: ['v1'],
tags: ['vault'],
params: urlParamsSchema,
response: {
200: responseSchema,
@@ -72,7 +72,7 @@ export default async function (
const responseSchema = S.array().items(S.object());

const schema: FastifySchema = {
tags: ['v1'],
tags: ['vault'],
params: urlParamsSchema,
response: {
200: responseSchema,
@@ -118,7 +118,7 @@ export default async function (
const responseSchema = S.array().items(S.object());

const schema: FastifySchema = {
tags: ['v1'],
tags: ['vault'],
params: urlParamsSchema,
response: {
200: responseSchema,
@@ -163,7 +163,7 @@ export default async function (
const responseSchema = S.array().items(S.object());

const schema: FastifySchema = {
tags: ['v1'],
tags: ['vault'],
params: urlParamsSchema,
response: {
200: responseSchema,
@@ -190,6 +190,52 @@ export default async function (
);
}

{
type UrlParams = {
chain: ChainId;
vault_address: string;
};

const urlParamsSchema = S.object()
.prop('chain', chainSchema.required().description('The chain the vault is on'))
.prop('vault_address', addressSchema.required().description('The vault contract address'));

const responseSchema = S.array().items(
S.object()
.prop('investor_address', addressSchema.required().description('The investor address'))
.prop('total_shares_balance', S.string().required().description('The total shares balance'))
.prop('underlying_balance0', S.string().required().description('The underlying balance 0'))
.prop('underlying_balance1', S.string().required().description('The underlying balance 1'))
.prop('usd_balance0', S.string().required().description('The USD balance 0'))
.prop('usd_balance1', S.string().required().description('The USD balance 1'))
.prop('usd_balance', S.string().required().description('The USD balance'))
);

const schema: FastifySchema = {
tags: ['vault'],
params: urlParamsSchema,
summary: 'Get all investor positions for a vault',
description: 'Get all investor positions for a vault',
response: {
200: responseSchema,
},
};

instance.get<{ Params: UrlParams }>(
'/:chain/:vault_address/investors',
{ schema },
async (request, reply) => {
const { chain, vault_address } = request.params;
const result = await asyncCache.wrap(
`vault-investors:${chain}:${vault_address.toLocaleLowerCase()}`,
30 * 1000,
async () => await getVaultInvestors(chain, vault_address)
);
reply.send(result);
}
);
}

done();
}

@@ -329,3 +375,63 @@ const getVaultHistoricPricesRange = async (
max: parseInt(vault.maxSnapshot?.[0]?.roundedTimestamp || 0),
};
};

const getVaultInvestors = async (chain: ChainId, vault_address: string) => {
const res = await Promise.all(
getSdksForChain(chain).map(async sdk =>
paginateSdkCalls(
sdk,
(sdk, skip, first) =>
sdk.VaultInvestors({
clmAddress: vault_address,
skip,
first,
}),
res => res.data.clmPositions.length,
{ pageSize: 1000, fetchAtMost: 100_000 }
)
)
);

const positions = res.flatMap(chainRes =>
chainRes.flatMap(chainPage => chainPage.data.clmPositions)
);

return positions.map(position => {
const managerToken = position.clm.managerToken;
const token0 = position.clm.underlyingToken0;
const token1 = position.clm.underlyingToken1;
const token0ToNativePrice = interpretAsDecimal(position.clm.token0ToNativePrice, 18);
const token1ToNativePrice = interpretAsDecimal(position.clm.token1ToNativePrice, 18);
const nativeToUsd = interpretAsDecimal(position.clm.nativeToUSDPrice, 18);
const positionShareBalance = interpretAsDecimal(position.totalBalance, managerToken.decimals);
const vaultBalance0 = interpretAsDecimal(
position.clm.underlyingAltAmount0,
token0.decimals
).plus(interpretAsDecimal(position.clm.underlyingMainAmount0, token0.decimals));
const vaultBalance1 = interpretAsDecimal(
position.clm.underlyingAltAmount1,
token1.decimals
).plus(interpretAsDecimal(position.clm.underlyingMainAmount1, token1.decimals));
const vaultTotalSupply = interpretAsDecimal(
position.clm.managerTotalSupply,
managerToken.decimals
);

const positionPercentShare = positionShareBalance.div(vaultTotalSupply);
const positionBalance0 = vaultBalance0.mul(positionPercentShare);
const positionBalance1 = vaultBalance1.mul(positionPercentShare);
const positionBalance0Usd = positionBalance0.mul(token0ToNativePrice).mul(nativeToUsd);
const positionBalance1Usd = positionBalance1.mul(token1ToNativePrice).mul(nativeToUsd);
const positionBalanceUsd = positionBalance0Usd.add(positionBalance1Usd);
return {
investor_address: position.investor.userAddress,
total_shares_balance: positionShareBalance.toFixed(10),
underlying_balance0: positionBalance0.toFixed(10),
underlying_balance1: positionBalance1.toFixed(10),
usd_balance0: positionBalance0Usd.toFixed(10),
usd_balance1: positionBalance1Usd.toFixed(10),
usd_balance: positionBalanceUsd.toFixed(10),
};
});
};
4 changes: 2 additions & 2 deletions src/routes/v1/vaults.ts
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ export default async function (
const responseSchema = S.array().items(S.object());

const schema: FastifySchema = {
tags: ['v1'],
tags: ['vaults'],
params: urlParamsSchema,
response: {
200: responseSchema,
@@ -83,7 +83,7 @@ export default async function (
const responseSchema = S.array().items(S.object());

const schema: FastifySchema = {
tags: ['v1'],
tags: ['vaults'],
params: urlParamsSchema,
querystring: queryParamsSchema,
response: {