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

patch merkl apr calc #1551

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
35 changes: 15 additions & 20 deletions src/api/offchain-rewards/providers/merkl/MerklProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AppChain, fromChainNumber, toAppChain, toChainId } from '../../../../ut
import { CampaignType, IOffchainRewardProvider, MerklCampaign, Vault } from '../../types';
import { isProviderApiError, ProviderApiError, UnsupportedChainError } from '../../errors';
import { CampaignTypeSetting, MerklApiCampaign, MerklApiCampaignsResponse } from './types';
import { groupBy } from 'lodash';
import { groupBy, mapKeys } from 'lodash';
import { Address, getAddress, isAddressEqual } from 'viem';
import { isFiniteNumber } from '../../../../utils/number';
import { isDefined } from '../../../../utils/array';
Expand Down Expand Up @@ -109,9 +109,16 @@ export class MerklProvider implements IOffchainRewardProvider {
});

if (poolForwarders.length > 0) {
/*
* Each forwarder has an almApr field, however it is currently returning the same almApr for every campaign targeting the same pool.
* Therefore, we are using the campaign's `aprs` field to look up the APR for each forwarder (via its label).
* The `aprs` key includes checksummed addresses in its keys, so we build a map with lowercase keys for lookup.
*/
const aprByLabel = mapKeys(apiCampaign.aprs, (_, key) => key.toLowerCase());
totalApr = poolForwarders.reduce((acc, forwarder) => {
if (isFiniteNumber(forwarder.almAPR)) {
acc += forwarder.almAPR / 100;
const apr = aprByLabel[forwarder.label.toLowerCase()];
if (apr && isFiniteNumber(apr)) {
acc += apr / 100;
}
return acc;
}, 0);
Expand All @@ -123,9 +130,7 @@ export class MerklProvider implements IOffchainRewardProvider {
};
});

const computeChain = apiCampaign.computeChainId
? fromChainNumber(apiCampaign.computeChainId)
: undefined;
const computeChain = apiCampaign.computeChainId ? fromChainNumber(apiCampaign.computeChainId) : undefined;
const claimChain = apiCampaign.chainId ? fromChainNumber(apiCampaign.chainId) : undefined;

return {
Expand All @@ -149,9 +154,7 @@ export class MerklProvider implements IOffchainRewardProvider {
};
}

protected async fetchCampaignsForChain(
chainId: AppChain
): Promise<MerklApiCampaignsResponse[string]> {
protected async fetchCampaignsForChain(chainId: AppChain): Promise<MerklApiCampaignsResponse[string]> {
const numericChainId = toChainId(chainId);

try {
Expand All @@ -162,18 +165,12 @@ export class MerklProvider implements IOffchainRewardProvider {
},
});
if (!data || typeof data !== 'object') {
throw new ProviderApiError(
`fetchCampaignsForChain(${chainId}): response error`,
providerId
);
throw new ProviderApiError(`fetchCampaignsForChain(${chainId}): response error`, providerId);
}

if (Object.keys(data).length === 0) {
if (throwIfNoData) {
throw new ProviderApiError(
`fetchCampaignsForChain(${chainId}): no data returned`,
providerId
);
throw new ProviderApiError(`fetchCampaignsForChain(${chainId}): no data returned`, providerId);
}
return {};
}
Expand All @@ -192,9 +189,7 @@ export class MerklProvider implements IOffchainRewardProvider {
throw err;
}
throw new ProviderApiError(
`fetchCampaignsForChain(${chainId}): ${
err && err instanceof Error ? err.message : 'unknown error'
}`,
`fetchCampaignsForChain(${chainId}): ${err && err instanceof Error ? err.message : 'unknown error'}`,
providerId,
err && err instanceof Error ? err : undefined
);
Expand Down
3 changes: 3 additions & 0 deletions src/api/offchain-rewards/providers/merkl/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type MerklApiForwarder = {
target: string;
owner: string;
type: number;
label: string;
};

type MerklApiCampaignParameters = {
Expand All @@ -29,6 +30,8 @@ export type MerklApiCampaign = {
mainParameter: string;
forwarders: MerklApiForwarder[];
campaignParameters: MerklApiCampaignParameters;
/** e.g. { "Beefy 0xDbaF4a5Ad4352adCDD2E914C1B1515E6F7451A82": 6.0934309620835405 } */
aprs: { [label: string]: number };
};

export type MerklApiCampaignsResponse = {
Expand Down
35 changes: 6 additions & 29 deletions src/api/stats/arbitrum/getMerklGammaApys.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,17 @@
import { getApyBreakdown } from '../common/getApyBreakdown';
import { ChainId } from '../../../../packages/address-book/src/types/chainid';
import { getMerklAprs } from '../common/getMerklAprs';

const BigNumber = require('bignumber.js');
const uniPools = require('../../../data/arbitrum/uniswapGammaPools.json');
const sushiPools = require('../../../data/arbitrum/sushiGammaPools.json');
const { ARBITRUM_CHAIN_ID: chainId } = require('../../../constants');
import { getApyBreakdown } from '../common/getApyBreakdown';

const merklApi = 'https://api.angle.money/v2/merkl?chainIds=42161';
const gammaApi = 'https://wire2.gamma.xyz/arbitrum/hypervisors/allData';
const sushiGammaApi = 'https://wire2.gamma.xyz/sushi/arbitrum/hypervisors/allData';

const pools = [...uniPools, ...sushiPools];
const getMerklGammaApys = async () => {
let poolAprs = {};
try {
poolAprs = await fetch(merklApi).then(res => res.json());
} catch (e) {
console.error(`Failed to fetch Merkl APRs: ${chainId}`);
}

let aprs = [];
for (let i = 0; i < pools.length; ++i) {
let apr = BigNumber(0);
let merklPools = poolAprs[chainId].pools;
if (Object.keys(merklPools).length !== 0) {
for (const [key, value] of Object.entries(merklPools)) {
if (key.toLowerCase() === pools[i].pool.toLowerCase()) {
for (const [k, v] of Object.entries(value.alm)) {
if (k.toLowerCase() === pools[i].address.toLowerCase()) {
apr = BigNumber(v.almAPR).dividedBy(100);
}
}
}
}
}

aprs.push(apr);
}
const merklAprs = await getMerklAprs(ChainId.arbitrum, pools);

let tradingAprs = {};
try {
Expand All @@ -53,7 +30,7 @@ const getMerklGammaApys = async () => {
console.log('Gamma Api Error', e);
}

return await getApyBreakdown(pools, tradingAprs, aprs, 0);
return await getApyBreakdown(pools, tradingAprs, merklAprs, 0);
};

module.exports = getMerklGammaApys;
35 changes: 6 additions & 29 deletions src/api/stats/base/getMerklBaseApys.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,15 @@
import { getMerklAprs } from '../common/getMerklAprs';
import { getApyBreakdown } from '../common/getApyBreakdown';
import { ChainId } from '../../../../packages/address-book/src/types/chainid';

const BigNumber = require('bignumber.js');
const sushiPools = require('../../../data/base/sushiGammaPools.json');
const { BASE_CHAIN_ID: chainId } = require('../../../constants');
import { getApyBreakdown } from '../common/getApyBreakdown';

const merklApi = 'https://api.angle.money/v2/merkl?chainIds=8453';
const gammaApi = 'https://wire2.gamma.xyz/sushi/base/hypervisors/allData';

const pools = [...sushiPools];
const getBaseMerklGammaApys = async () => {
let poolAprs = {};
try {
poolAprs = await fetch(merklApi).then(res => res.json());
} catch (e) {
console.error(`Failed to fetch Merkl APRs: ${chainId}`);
}

let aprs = [];
for (let i = 0; i < pools.length; ++i) {
let apr = BigNumber(0);
let merklPools = poolAprs[chainId].pools;
if (Object.keys(merklPools).length !== 0) {
for (const [key, value] of Object.entries(merklPools)) {
if (key.toLowerCase() === pools[i].pool.toLowerCase()) {
for (const [k, v] of Object.entries(value.alm)) {
if (k.toLowerCase() === pools[i].address.toLowerCase()) {
apr = BigNumber(v.almAPR).dividedBy(100);
}
}
}
}
}

aprs.push(apr);
}
const merklAprs = await getMerklAprs(ChainId.base, pools);

let tradingAprs = {};
try {
Expand All @@ -47,7 +24,7 @@ const getBaseMerklGammaApys = async () => {
console.log('Gamma Base Api Error', e);
}

return await getApyBreakdown(pools, tradingAprs, aprs, 0);
return await getApyBreakdown(pools, tradingAprs, merklAprs, 0);
};

module.exports = getBaseMerklGammaApys;
91 changes: 91 additions & 0 deletions src/api/stats/common/getMerklAprs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { ChainId } from '../../../../packages/address-book/src/types/chainid';
import BigNumber from 'bignumber.js';
import { getJson } from '../../../utils/http';
import { MerklApiCampaignsResponse } from '../../offchain-rewards/providers/merkl/types';
import { BIG_ZERO } from '../../../utils/big-number';
import { mapKeys } from 'lodash';
import { isFiniteNumber } from '../../../utils/number';

type MerklPoolRequest = {
/** address of ALM contract */
address: string;
/** address of pool (mainParameter) */
pool: string;
};

export enum MerklPoolType {
ERC20 = 1,
ConcentratedLiquidity,
ERC20Snapshot,
Airdrops,
Silo,
RadiantEmissions,
Morpho,
Dolomite,
}

export async function getMerklAprs(
chainId: ChainId,
pools: MerklPoolRequest[],
types: MerklPoolType[] = [MerklPoolType.ConcentratedLiquidity]
): Promise<BigNumber[]> {
const aprs = pools.map(() => BIG_ZERO);

try {
const params = new URLSearchParams({
chainIds: chainId.toString(),
live: 'true',
});
for (const type of types) {
params.append('types', type.toString());
}

const data = await getJson<MerklApiCampaignsResponse>({
url: 'https://api.merkl.xyz/v3/campaigns',
params,
});

if (!data || typeof data !== 'object') {
throw new Error(`response error`);
}

if (Object.keys(data).length === 0) {
throw new Error(`no data returned`);
}

const dataForChain = data[chainId];
if (!dataForChain) {
throw new Error(`no data for chain returned`);
}

for (const poolCampaigns of Object.values(dataForChain)) {
for (const campaign of Object.values(poolCampaigns)) {
for (let i = 0; i < pools.length; ++i) {
const pool = pools[i];
if (pool.pool.toLowerCase() !== campaign.mainParameter.toLowerCase()) {
continue;
}

const poolForwarders = campaign.forwarders.filter(
forwarder => pool.address.toLowerCase() === forwarder.almAddress.toLowerCase()
);
if (poolForwarders.length === 0) {
continue;
}

const aprByLabel = mapKeys(campaign.aprs, (_, key) => key.toLowerCase());
for (const forwarder of poolForwarders) {
const apr = aprByLabel[forwarder.label.toLowerCase()];
if (apr && isFiniteNumber(apr)) {
aprs[i] = aprs[i].plus(apr / 100);
}
}
}
}
}
} catch (err: unknown) {
console.error(`getMerklAprs(${chainId}):`, err);
}

return aprs;
}
35 changes: 6 additions & 29 deletions src/api/stats/ethereum/getMerklApys.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,18 @@
import { getMerklAprs } from '../common/getMerklAprs';
import { getApyBreakdown } from '../common/getApyBreakdown';
import { ChainId } from '../../../../packages/address-book/src/types/chainid';

const BigNumber = require('bignumber.js');
const uniPools = require('../../../data/ethereum/rangeLpPools.json');
const { ETH_CHAIN_ID: chainId } = require('../../../constants');
import { getApyBreakdown } from '../common/getApyBreakdown';

const merklApi = 'https://api.angle.money/v2/merkl?chainIds=1';
const rangeApi =
'https://rangeprotocol-public.s3.ap-southeast-1.amazonaws.com/data/fees-ethereum-uniswap.json';
const rangePancakeApi =
'https://rangeprotocol-public.s3.ap-southeast-1.amazonaws.com/data/fees-ethereum-pancakeswap.json';

const pools = [...uniPools];
const getMerklApys = async () => {
let poolAprs = {};
try {
poolAprs = await fetch(merklApi).then(res => res.json());
} catch (e) {
console.error(`Failed to fetch Merkl APRs: ${chainId}`);
}

let aprs = [];
for (let i = 0; i < pools.length; ++i) {
let apr = BigNumber(0);
let merklPools = poolAprs[chainId].pools;
if (Object.keys(merklPools).length !== 0) {
for (const [key, value] of Object.entries(merklPools)) {
if (key.toLowerCase() === pools[i].pool.toLowerCase()) {
for (const [k, v] of Object.entries(value.alm)) {
if (k.toLowerCase() === pools[i].address.toLowerCase()) {
apr = BigNumber(v.almAPR).dividedBy(100);
}
}
}
}
}

aprs.push(apr);
}
const merklAprs = await getMerklAprs(ChainId.ethereum, pools);

let tradingAprs = {};
try {
Expand All @@ -52,7 +29,7 @@ const getMerklApys = async () => {
console.log('Range Api Error', e);
}

return await getApyBreakdown(pools, tradingAprs, aprs, 0);
return await getApyBreakdown(pools, tradingAprs, merklAprs, 0);
};

module.exports = getMerklApys;
Loading