Skip to content

Commit

Permalink
Get merkl data for reward pools + use same source of merkl data for c…
Browse files Browse the repository at this point in the history
…lm/clm pool APY calc (#1486)
  • Loading branch information
ReflectiveChimp authored Jun 30, 2024
1 parent 9073bbb commit f036595
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 70 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.8.0
v20.10.0
31 changes: 24 additions & 7 deletions src/api/cowcentrated/getCowMerkleCampaigns.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ApiChain, toChainId } from '../../utils/chain';
import { sleep } from '../../utils/time';
import { mapValues, partition } from 'lodash';
import { isResultFulfilled } from '../../utils/promise';
import { Address, isAddressEqual } from 'viem';
Expand All @@ -9,14 +8,18 @@ import {
CampaignType,
CampaignTypeSetting,
CowClm,
isCowClmWithRewardPool,
MerklApiCampaign,
MerklApiCampaignsResponse,
} from './types';
import { isFiniteNumber } from '../../utils/number';
import { CachedByChain } from '../../utils/CachedByChain';
import { CachedThrottledPromise } from '../../utils/CachedThrottledPromise';
import { sleep } from '../../utils/time';

const INIT_DELAY = 5000; // 5 seconds
const UPDATE_INTERVAL = 30 * 60 * 1000; // 30 minutes
const MIN_UPDATE_INTERVAL = 10 * 60 * 1000; // will not update more often even if update called
const FRESH_LIFETIME = 30 * 60; // how many seconds is a response considered fresh for
const STALE_LIFETIME = 2 * 60 * 60; // how many additional seconds can a stale response be kept
const CACHE_KEY = 'COWCENTRATED_MERKL_CAMPAIGNS';
Expand All @@ -37,6 +40,7 @@ const campaignStore = new CachedByChain<Campaign[]>({
stale: STALE_LIFETIME,
version: 2, // increase if the shape of Campaign[] changes
});
const updater = new CachedThrottledPromise(updateAll, MIN_UPDATE_INTERVAL);

function getCampaignType(creator: Address, chain: ApiChain): CampaignType {
const type = CAMPAIGN_CREATOR_TO_TYPE[creator] || 'external';
Expand All @@ -50,21 +54,29 @@ function getCampaignType(creator: Address, chain: ApiChain): CampaignType {
function getCampaign(
apiChain: ApiChain,
campaign: MerklApiCampaign,
pools: ReadonlyArray<CowClm>
clms: ReadonlyArray<CowClm>
): Campaign | undefined {
// skip campaigns with merkl test token reward
if (campaign.campaignParameters.symbolRewardToken === 'aglaMerkl') {
return undefined;
}

const type = getCampaignType(campaign.creator, apiChain);
const vaults = pools.filter(
const clmsForPool = clms.filter(
p => p.lpAddress.toLowerCase() === campaign.mainParameter.toLowerCase()
);
if (!vaults.length) {
if (!clmsForPool.length) {
return undefined;
}

const vaults = clmsForPool.flatMap(clm => {
const vaults = [{ id: clm.oracleId, address: clm.address }];
if (isCowClmWithRewardPool(clm)) {
vaults.push({ id: clm.rewardPool.oracleId, address: clm.rewardPool.address });
}
return vaults;
});

const vaultsWithApr = vaults.map(vault => {
let totalApr: number = 0;

Expand All @@ -81,8 +93,7 @@ function getCampaign(
}

return {
id: vault.oracleId,
address: vault.address,
...vault,
apr: totalApr,
};
});
Expand Down Expand Up @@ -160,7 +171,7 @@ async function updateAll() {

function scheduleUpdate(wait = UPDATE_INTERVAL) {
sleep(wait)
.then(updateAll)
.then(() => updater.update())
.catch(err => {
console.error(`> [CLM Merkl] Update all failed`, err);
scheduleUpdate();
Expand Down Expand Up @@ -209,3 +220,9 @@ export function getCowBeefyMerklCampaignsForChain(chain: ApiChain) {

return undefined;
}

/** updates merkl campaigns (if last update was more than MIN_UPDATE_INTERVAL ago) and returns campaigns that target a clm or clm pool */
export async function updateAndGetCowMerklCampaignsForChain(chain: ApiChain) {
await updater.update();
return getCowMerklCampaignsForChain(chain);
}
2 changes: 2 additions & 0 deletions src/api/cowcentrated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,5 @@ export type Campaign = {
type: CampaignType;
vaults: CampaignVault[];
};

export type CampaignForVault = Omit<Campaign, 'vaults'> & CampaignVault;
111 changes: 49 additions & 62 deletions src/api/stats/common/getCowVaultApys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ import { ApiChain, toChainId } from '../../../utils/chain';
import { getCowVaultsMeta } from '../../cowcentrated/getCowVaultsMeta';
import {
type AnyCowClmMeta,
type CowClmMeta,
CampaignForVault,
type CowClmWithRewardPoolMeta,
isCowClmWithRewardPoolMeta,
} from '../../cowcentrated/types';
import { isDefined } from '../../../utils/array';
import { getBeefyRewardPoolV2Apr } from './getBeefyRewardPoolV2Apr';
import { ApyBreakdownRequest, getApyBreakdown, ApyBreakdownResult } from './getApyBreakdownNew';
import { ApyBreakdownRequest, ApyBreakdownResult, getApyBreakdown } from './getApyBreakdownNew';
import { updateAndGetCowMerklCampaignsForChain } from '../../cowcentrated/getCowMerkleCampaigns';
import { getUnixTime } from 'date-fns';
import { mapValues, omit } from 'lodash';

type MerklVaultData = {
totalApr: number;
campaigns: CampaignForVault[];
};

/**
* Base CLMs + Reward Pools
Expand All @@ -20,9 +28,10 @@ export const getCowApys = async (apiChain: ApiChain) => {
throw new Error(`No clms found for ${apiChain}`);
}

const campaignByVault = await getMerklCampaignsByVault(apiChain);
const chainId = toChainId(apiChain);
const [clmBreakdownsResult, rewardPoolAprsResult] = await Promise.allSettled([
getCowClmApyBreakdown(chainId, clms),
getCowClmApyBreakdown(clms, campaignByVault),
getCowRewardPoolAprs(chainId, clms),
]);

Expand All @@ -42,7 +51,12 @@ export const getCowApys = async (apiChain: ApiChain) => {
}

const rewardPoolAprs = rewardPoolAprsResult.value;
const rewardPoolBreakdowns = getCowRewardPoolApyBreakdown(clms, clmBreakdowns, rewardPoolAprs);
const rewardPoolBreakdowns = getCowRewardPoolApyBreakdown(
clms,
clmBreakdowns,
rewardPoolAprs,
campaignByVault
);

if (!rewardPoolBreakdowns) {
// this just means none of the CLMs had reward pools defined in config
Expand All @@ -55,10 +69,35 @@ export const getCowApys = async (apiChain: ApiChain) => {
};
};

async function getMerklCampaignsByVault(
apiChain: ApiChain
): Promise<Record<string, MerklVaultData>> {
const merklCampaigns = await updateAndGetCowMerklCampaignsForChain(apiChain);
const byVaultId: Record<string, CampaignForVault[]> = {};
const now = getUnixTime(new Date());
for (const campaign of merklCampaigns.value) {
if (campaign.startTimestamp <= now && campaign.endTimestamp >= now) {
for (const vault of campaign.vaults) {
byVaultId[vault.id] ??= [];
byVaultId[vault.id].push({
...omit(campaign, 'vaults'),
...vault,
});
}
}
}

return mapValues(byVaultId, campaigns => ({
campaigns,
totalApr: campaigns.reduce((total, campaign) => total + campaign.apr, 0),
}));
}

function getCowRewardPoolApyBreakdown(
clms: AnyCowClmMeta[],
clmApys: ApyBreakdownResult,
rewardPoolAprs: (number | undefined)[]
rewardPoolAprs: (number | undefined)[],
merklById: Record<string, MerklVaultData>
): ApyBreakdownResult | undefined {
const inputs = clms
.map((clm, index): ApyBreakdownRequest | undefined => {
Expand All @@ -67,8 +106,8 @@ function getCowRewardPoolApyBreakdown(
vaultId: clm.rewardPool.oracleId,
beefyFee: 0,
rewardPool: rewardPoolAprs[index],
clm: clmApys.apyBreakdowns[clm.oracleId]?.clmApr,
merkl: clmApys.apyBreakdowns[clm.oracleId]?.merklApr,
clm: clmApys.apyBreakdowns[clm.oracleId]?.clmApr, // after fee from CLM; reward pool fee = 0; so this works
merkl: merklById[clm.rewardPool.oracleId]?.totalApr || 0, // we can't copy from CLM in case it is not forwarded correctly
};
}
return undefined;
Expand Down Expand Up @@ -121,66 +160,14 @@ const getCowRewardPoolApr = async (
};

const getCowClmApyBreakdown = async (
chainId: ChainId,
vaults: AnyCowClmMeta[]
vaults: AnyCowClmMeta[],
merklById: Record<string, MerklVaultData>
): Promise<ApyBreakdownResult> => {
const merklCampaigns = await getMerklCampaigns(chainId);
return getApyBreakdown(
vaults.map(vault => ({
vaultId: vault.oracleId,
clm: vault.apr,
merkl: getMerklAprForVault(vault, merklCampaigns),
merkl: merklById[vault.oracleId]?.totalApr || 0,
}))
);
};

type Forwarder = {
almAPR: number;
almAddress: string;
};

type Campaign = {
mainParameter: string;
forwarders: Forwarder[];
};

type MerklChainCampaigns = {
[poolIdentifier: string]: {
[campaignID: string]: Campaign;
};
};

type MerklAPIChainCampaigns = {
[chainId in ChainId]: MerklAPIChainCampaigns;
};

const getMerklCampaigns = async (chainID: ChainId) => {
try {
const response = await fetch('https://api.merkl.xyz/v3/campaigns?chainIds=' + chainID).then(
res => res.json() as Promise<MerklAPIChainCampaigns>
);
return response[chainID];
} catch (err) {
console.error(`> getMerklCampaigns Error on ${chainID} ${err.message}`);
console.error(err);
return {};
}
};

const getMerklAprForVault = (vault: CowClmMeta, merklCampaigns: MerklChainCampaigns) => {
if (!merklCampaigns) return 0;
let apr = 0;
for (const [poolId, campaigns] of Object.entries(merklCampaigns)) {
for (const [campaignId, campaign] of Object.entries(campaigns)) {
if (campaign.mainParameter.toLowerCase() === vault.lpAddress.toLowerCase()) {
campaign.forwarders.forEach(forwarder => {
if (forwarder.almAddress.toLowerCase() === vault.address.toLowerCase()) {
if (forwarder.almAPR === 0 || isNaN(forwarder.almAPR)) return;
apr += forwarder.almAPR / 100;
}
});
}
}
}
return apr;
};
3 changes: 3 additions & 0 deletions src/utils/CachedByChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ export type CachedByChainOptions = MemoryStoreOptions & {

export type ChainMeta<T> = {
value: T;
/** unix timestamp */
updatedAt: number;
/** unix timestamp */
freshUntil: number;
/** unix timestamp */
staleUntil: number;
version: number;
};
Expand Down
32 changes: 32 additions & 0 deletions src/utils/CachedThrottledPromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export class CachedThrottledPromise<T> {
protected inProgress: Promise<T> | undefined;
protected lastResult: T;
protected lastUpdate: number | undefined;

constructor(protected updateFn: () => Promise<T>, protected minInterval: number) {}

/**
* Returns the last result if the last update was less than minInterval ago.
*/
public async update(): Promise<T> {
if (this.inProgress) {
return this.inProgress;
}

const now = Date.now();
if (this.lastUpdate && now - this.lastUpdate < this.minInterval) {
return this.lastResult;
}

this.lastUpdate = now;
this.inProgress = this.updateFn();

try {
const result = await this.inProgress;
this.lastResult = result;
return result;
} finally {
this.inProgress = undefined;
}
}
}

0 comments on commit f036595

Please sign in to comment.