@defisaver/positions-sdk 2.1.142-dev → 2.1.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/cjs/aaveV3/merkl.js +4 -7
  2. package/cjs/aaveV4/merkl.js +5 -6
  3. package/cjs/claiming/aaveV3.d.ts +2 -1
  4. package/cjs/claiming/aaveV3.js +13 -4
  5. package/cjs/compoundV3/merkl.js +4 -6
  6. package/cjs/fluid/index.d.ts +9 -1
  7. package/cjs/fluid/index.js +10 -4
  8. package/cjs/fluid/merkl.d.ts +7 -4
  9. package/cjs/fluid/merkl.js +22 -16
  10. package/cjs/helpers/fluidHelpers/index.d.ts +3 -1
  11. package/cjs/helpers/fluidHelpers/index.js +37 -4
  12. package/cjs/morphoBlue/index.d.ts +1 -1
  13. package/cjs/morphoBlue/index.js +2 -2
  14. package/cjs/morphoBlue/merkl.d.ts +6 -4
  15. package/cjs/morphoBlue/merkl.js +17 -16
  16. package/cjs/portfolio/index.js +5 -5
  17. package/cjs/services/merkl.d.ts +6 -0
  18. package/cjs/services/merkl.js +39 -0
  19. package/cjs/types/claiming.d.ts +12 -1
  20. package/cjs/types/claiming.js +2 -0
  21. package/cjs/types/common.d.ts +4 -0
  22. package/cjs/types/common.js +5 -1
  23. package/cjs/types/fluid.d.ts +2 -0
  24. package/esm/aaveV3/merkl.js +5 -8
  25. package/esm/aaveV4/merkl.js +5 -6
  26. package/esm/claiming/aaveV3.d.ts +2 -1
  27. package/esm/claiming/aaveV3.js +12 -4
  28. package/esm/compoundV3/merkl.js +5 -7
  29. package/esm/fluid/index.d.ts +9 -1
  30. package/esm/fluid/index.js +9 -3
  31. package/esm/fluid/merkl.d.ts +7 -4
  32. package/esm/fluid/merkl.js +19 -15
  33. package/esm/helpers/fluidHelpers/index.d.ts +3 -1
  34. package/esm/helpers/fluidHelpers/index.js +38 -6
  35. package/esm/morphoBlue/index.d.ts +1 -1
  36. package/esm/morphoBlue/index.js +1 -1
  37. package/esm/morphoBlue/merkl.d.ts +6 -4
  38. package/esm/morphoBlue/merkl.js +16 -15
  39. package/esm/portfolio/index.js +6 -6
  40. package/esm/services/merkl.d.ts +6 -0
  41. package/esm/services/merkl.js +35 -0
  42. package/esm/types/claiming.d.ts +12 -1
  43. package/esm/types/claiming.js +2 -0
  44. package/esm/types/common.d.ts +4 -0
  45. package/esm/types/common.js +4 -0
  46. package/esm/types/fluid.d.ts +2 -0
  47. package/package.json +1 -1
  48. package/src/aaveV3/merkl.ts +5 -8
  49. package/src/aaveV4/merkl.ts +5 -5
  50. package/src/claiming/aaveV3.ts +19 -7
  51. package/src/compoundV3/merkl.ts +5 -6
  52. package/src/fluid/index.ts +9 -3
  53. package/src/fluid/merkl.ts +18 -13
  54. package/src/helpers/fluidHelpers/index.ts +68 -6
  55. package/src/morphoBlue/index.ts +1 -1
  56. package/src/morphoBlue/merkl.ts +16 -15
  57. package/src/portfolio/index.ts +6 -6
  58. package/src/services/merkl.ts +31 -0
  59. package/src/types/claiming.ts +13 -0
  60. package/src/types/common.ts +5 -0
  61. package/src/types/fluid.ts +3 -1
@@ -26,7 +26,7 @@ import { _getLiquityTroveInfo, getLiquityStakingData } from '../liquity';
26
26
  import { _getLiquityV2MarketData, getLiquitySAndYBold, getLiquityV2Staking } from '../liquityV2';
27
27
  import { _getAllUserEarnPositionsWithFTokens, _getUserPositionsPortfolio } from '../fluid';
28
28
  import { getUmbrellaData } from '../umbrella';
29
- import { getMeritUnclaimedRewards, getUnclaimedRewardsForAllMarkets } from '../claiming/aaveV3';
29
+ import { getMerklUnclaimedRewards, getUnclaimedRewardsForAllMarkets } from '../claiming/aaveV3';
30
30
  import { getCompoundV3Rewards } from '../claiming/compV3';
31
31
  import { fetchSparkAirdropRewards, fetchSparkRewards } from '../claiming/spark';
32
32
  import { getKingRewards } from '../claiming/king';
@@ -114,7 +114,7 @@ export function getPortfolioData(provider_1, network_1, defaultProvider_1, addre
114
114
  },
115
115
  };
116
116
  rewardsData[address.toLowerCase()] = {
117
- aaveV3merit: {},
117
+ merkl: {},
118
118
  aaveV3: {},
119
119
  compV3: {},
120
120
  spark: {},
@@ -335,12 +335,12 @@ export function getPortfolioData(provider_1, network_1, defaultProvider_1, addre
335
335
  }))).flat(),
336
336
  ...addresses.map((address) => __awaiter(this, void 0, void 0, function* () {
337
337
  try {
338
- const aaveMeritData = yield getMeritUnclaimedRewards(address, network);
339
- rewardsData[address.toLowerCase()].aaveV3merit = { error: '', data: aaveMeritData };
338
+ const merklData = yield getMerklUnclaimedRewards(address, network);
339
+ rewardsData[address.toLowerCase()].merkl = { error: '', data: merklData };
340
340
  }
341
341
  catch (error) {
342
- console.error(`Error fetching Aave V3 Merit rewards data for address ${address}:`, error);
343
- rewardsData[address.toLowerCase()].aaveV3merit = { error: `Error fetching Aave V3 Merit rewards data for address ${address}`, data: null };
342
+ console.error(`Error fetching Merkl rewards data for address ${address}:`, error);
343
+ rewardsData[address.toLowerCase()].merkl = { error: `Error fetching Merkl rewards data for address ${address}`, data: null };
344
344
  }
345
345
  })),
346
346
  ...aaveV3Markets.map(market => addresses.map((address) => __awaiter(this, void 0, void 0, function* () {
@@ -0,0 +1,6 @@
1
+ import { MerklOpportunity } from '../types';
2
+ /**
3
+ * Fetches every page matching the query. Merkl returns 20 opportunities by default, so omitting
4
+ * `items` and advancing `page` avoids imposing a client-side maximum on the result set.
5
+ */
6
+ export declare const fetchAllMerklOpportunities: (query: Record<string, string>) => Promise<MerklOpportunity[]>;
@@ -0,0 +1,35 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { LONGER_TIMEOUT } from './utils';
11
+ const MERKL_OPPORTUNITIES_URL = 'https://fe.defisaver.com/api/merkl/opportunities';
12
+ /**
13
+ * Fetches every page matching the query. Merkl returns 20 opportunities by default, so omitting
14
+ * `items` and advancing `page` avoids imposing a client-side maximum on the result set.
15
+ */
16
+ export const fetchAllMerklOpportunities = (query) => __awaiter(void 0, void 0, void 0, function* () {
17
+ const opportunities = [];
18
+ let page = 0;
19
+ let pageOpportunities;
20
+ do {
21
+ const searchParams = new URLSearchParams(Object.assign(Object.assign({}, query), { page: page.toString() }));
22
+ const res = yield fetch(`${MERKL_OPPORTUNITIES_URL}?${searchParams}`, {
23
+ signal: AbortSignal.timeout(LONGER_TIMEOUT),
24
+ });
25
+ if (!res.ok)
26
+ throw new Error(`Failed to fetch Merkl opportunities page ${page}`);
27
+ const data = yield res.json(); // eslint-disable-line no-await-in-loop
28
+ if (!Array.isArray(data))
29
+ throw new Error(`Invalid Merkl opportunities response on page ${page}`);
30
+ pageOpportunities = data;
31
+ opportunities.push(...pageOpportunities);
32
+ page += 1;
33
+ } while (pageOpportunities.length > 0);
34
+ return opportunities;
35
+ });
@@ -4,6 +4,8 @@ export declare enum ClaimType {
4
4
  AAVE_REWARDS = "AAVE_REWARDS",
5
5
  /** Merit rewards from AAVE (various tokens) */
6
6
  AAVE_MERIT_REWARDS = "AAVE_MERIT_REWARDS",
7
+ /** Rewards distributed through Merkl across supported protocols */
8
+ MERKL_REWARDS = "MERKL_REWARDS",
7
9
  /** Rewards from Compound V3 (only in COMP) */
8
10
  COMPOUND_V3_COMP = "COMPOUND_V3_COMP",
9
11
  /** Rewards from Spark (wstETH only for now) */
@@ -43,6 +45,15 @@ export type AaveMeritRewardsClaimableToken = _ClaimableTokenPartial & {
43
45
  unclaimed: string;
44
46
  };
45
47
  };
48
+ export type MerklRewardsClaimableToken = _ClaimableTokenPartial & {
49
+ claimType: ClaimType.MERKL_REWARDS;
50
+ additionalClaimFields: {
51
+ accumulated: string;
52
+ proof: string[];
53
+ decimals: string;
54
+ unclaimed: string;
55
+ };
56
+ };
46
57
  export type SparkRewardsClaimableToken = _ClaimableTokenPartial & {
47
58
  claimType: ClaimType.SPARK_REWARDS;
48
59
  additionalClaimFields: {
@@ -91,5 +102,5 @@ export type UniswapAirdropClaimableToken = _ClaimableTokenPartial & {
91
102
  proof: string[];
92
103
  };
93
104
  };
94
- export type ClaimableToken = AaveRewardsClaimableToken | AaveMeritRewardsClaimableToken | CompoundV3CompClaimableToken | SparkRewardsClaimableToken | KingRewardsClaimableToken | SparkAirdropClaimableToken | SparkWstEthRewardsClaimableToken | EthenaAirdropClaimableToken;
105
+ export type ClaimableToken = AaveRewardsClaimableToken | AaveMeritRewardsClaimableToken | MerklRewardsClaimableToken | CompoundV3CompClaimableToken | SparkRewardsClaimableToken | KingRewardsClaimableToken | SparkAirdropClaimableToken | SparkWstEthRewardsClaimableToken | EthenaAirdropClaimableToken;
95
106
  export {};
@@ -4,6 +4,8 @@ export var ClaimType;
4
4
  ClaimType["AAVE_REWARDS"] = "AAVE_REWARDS";
5
5
  /** Merit rewards from AAVE (various tokens) */
6
6
  ClaimType["AAVE_MERIT_REWARDS"] = "AAVE_MERIT_REWARDS";
7
+ /** Rewards distributed through Merkl across supported protocols */
8
+ ClaimType["MERKL_REWARDS"] = "MERKL_REWARDS";
7
9
  /** Rewards from Compound V3 (only in COMP) */
8
10
  ClaimType["COMPOUND_V3_COMP"] = "COMPOUND_V3_COMP";
9
11
  /** Rewards from Spark (wstETH only for now) */
@@ -6,6 +6,9 @@ export declare enum IncentiveSide {
6
6
  Supply = "supply",
7
7
  Borrow = "borrow"
8
8
  }
9
+ export declare enum IncentiveSource {
10
+ Merkl = "merkl"
11
+ }
9
12
  export declare enum IncentiveEligibilityId {
10
13
  AaveV3EthenaLiquidLeverage = "0x0dC599DBB180E64E42b87332FDeC3a551445ea44BORROW_BL",
11
14
  AaveV3ArbitrumEthSupply = "0x5d16261c6715a653248269861bbacf68a9774cde",
@@ -25,6 +28,7 @@ export interface IncentiveData {
25
28
  token: string;
26
29
  apy: string;
27
30
  incentiveKind?: IncentiveKind;
31
+ source?: IncentiveSource;
28
32
  description?: string;
29
33
  eligibilityId?: IncentiveEligibilityId;
30
34
  }
@@ -8,6 +8,10 @@ export var IncentiveSide;
8
8
  IncentiveSide["Supply"] = "supply";
9
9
  IncentiveSide["Borrow"] = "borrow";
10
10
  })(IncentiveSide || (IncentiveSide = {}));
11
+ export var IncentiveSource;
12
+ (function (IncentiveSource) {
13
+ IncentiveSource["Merkl"] = "merkl";
14
+ })(IncentiveSource || (IncentiveSource = {}));
11
15
  export var IncentiveEligibilityId;
12
16
  (function (IncentiveEligibilityId) {
13
17
  IncentiveEligibilityId["AaveV3EthenaLiquidLeverage"] = "0x0dC599DBB180E64E42b87332FDeC3a551445ea44BORROW_BL";
@@ -319,6 +319,8 @@ export interface FluidAggregatedVaultData {
319
319
  leftToBorrowUsd: string;
320
320
  netApy: string;
321
321
  incentiveUsd: string;
322
+ merklSupplyIncentives: IncentiveData[];
323
+ merklBorrowIncentives: IncentiveData[];
322
324
  ratio: string;
323
325
  collRatio: string;
324
326
  minRatio: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defisaver/positions-sdk",
3
- "version": "2.1.142-dev",
3
+ "version": "2.1.143",
4
4
  "description": "",
5
5
  "main": "./cjs/index.js",
6
6
  "module": "./esm/index.js",
@@ -1,5 +1,6 @@
1
1
  import { aprToApy } from '../moneymarket';
2
- import { LONGER_TIMEOUT, wethToEth } from '../services/utils';
2
+ import { fetchAllMerklOpportunities } from '../services/merkl';
3
+ import { wethToEth } from '../services/utils';
3
4
  import {
4
5
  MerkleRewardMap, MerklOpportunity, OpportunityAction, OpportunityStatus,
5
6
  EthAddress, NetworkNumber,
@@ -31,13 +32,10 @@ export const formatAaveAsset = (_symbol: string) => {
31
32
 
32
33
  export const getMerkleCampaigns = async (chainId: NetworkNumber): Promise<MerkleRewardMap> => {
33
34
  try {
34
- // status/items narrow the query because Merkl paginates at 20 items by default — an
35
- // unfiltered query silently drops live campaigns once the protocol has enough opportunities
36
- const res = await fetch('https://fe.defisaver.com/api/merkl/opportunities?mainProtocolId=aave&status=LIVE&items=100', {
37
- signal: AbortSignal.timeout(LONGER_TIMEOUT),
35
+ const opportunities = await fetchAllMerklOpportunities({
36
+ mainProtocolId: 'aave',
37
+ status: OpportunityStatus.LIVE,
38
38
  });
39
- if (!res.ok) throw new Error('Failed to fetch Merkle campaigns');
40
- const opportunities = await res.json() as MerklOpportunity[];
41
39
  const relevantOpportunities = opportunities
42
40
  .filter((o: MerklOpportunity) => o.chainId === chainId)
43
41
  .filter((o: MerklOpportunity) => o.status === OpportunityStatus.LIVE);
@@ -74,4 +72,3 @@ export const getMerkleCampaigns = async (chainId: NetworkNumber): Promise<Merkle
74
72
  return {};
75
73
  }
76
74
  };
77
-
@@ -1,5 +1,5 @@
1
1
  import { aprToApy } from '../moneymarket';
2
- import { LONGER_TIMEOUT } from '../services/utils';
2
+ import { fetchAllMerklOpportunities } from '../services/merkl';
3
3
  import {
4
4
  AaveV4MerklRewardMap,
5
5
  AaveV4ReserveAssetData,
@@ -67,11 +67,11 @@ export const buildAaveV4MerklRewardMap = (opportunities: MerklOpportunity[], cha
67
67
 
68
68
  export const getAaveV4MerkleCampaigns = async (chainId: NetworkNumber): Promise<AaveV4MerklRewardMap> => {
69
69
  try {
70
- const res = await fetch('https://fe.defisaver.com/api/merkl/opportunities?mainProtocolId=aave&type=AAVE_V4_HUB_SUPPLY,AAVE_V4_HUB_BORROW,AAVE_V4_SPOKE_SUPPLY,AAVE_V4_SPOKE_BORROW&status=LIVE&items=100', {
71
- signal: AbortSignal.timeout(LONGER_TIMEOUT),
70
+ const opportunities = await fetchAllMerklOpportunities({
71
+ mainProtocolId: 'aave',
72
+ type: 'AAVE_V4_HUB_SUPPLY,AAVE_V4_HUB_BORROW,AAVE_V4_SPOKE_SUPPLY,AAVE_V4_SPOKE_BORROW',
73
+ status: OpportunityStatus.LIVE,
72
74
  });
73
- if (!res.ok) throw new Error('Failed to fetch Aave V4 Merkle campaigns');
74
- const opportunities = await res.json() as MerklOpportunity[];
75
75
  return buildAaveV4MerklRewardMap(opportunities, chainId);
76
76
  } catch (e) {
77
77
  console.error('Failed to fetch Aave V4 Merkle campaigns', e);
@@ -1,7 +1,7 @@
1
1
  import { Client } from 'viem';
2
2
  import Dec from 'decimal.js';
3
3
  import { EthAddress, NetworkNumber } from '../types';
4
- import { ClaimableToken, ClaimType } from '../types/claiming';
4
+ import { ClaimableToken, ClaimType, MerklRewardsClaimableToken } from '../types/claiming';
5
5
  import {
6
6
  AaveIncentiveDataProviderV3ContractViem,
7
7
  AaveRewardsControllerViem,
@@ -103,18 +103,18 @@ export async function getUnclaimedRewardsForAllMarkets(
103
103
  return mapAaveRewardsToClaimableTokens(Object.values(totalUnclaimedPerRewardToken), marketAddress, walletAddress);
104
104
  }
105
105
 
106
- export async function getMeritUnclaimedRewards(account: EthAddress, network: NetworkNumber): Promise<ClaimableToken[]> {
106
+ export async function getMerklUnclaimedRewards(account: EthAddress, network: NetworkNumber): Promise<MerklRewardsClaimableToken[]> {
107
107
  let data;
108
108
  try {
109
109
  const res = await fetch(`https://fe.defisaver.com/api/merkl/get-user-rewards/${account}?chainId=${network}`,
110
110
  { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
111
111
  data = await res.json();
112
112
  } catch (error) {
113
- console.error('External API Failure: Aave Merit', error);
113
+ console.error('External API Failure: Merkl rewards', error);
114
114
  data = [];
115
115
  }
116
116
 
117
- const claimableTokens: ClaimableToken[] = [];
117
+ const claimableTokens: MerklRewardsClaimableToken[] = [];
118
118
 
119
119
  // Merkl (or our proxy, on a non-2xx) can return a non-array error body; `fetch` doesn't throw on
120
120
  // HTTP errors, so guard before iterating. Mirrors the app-side getMeritUnclaimedRewards.
@@ -137,12 +137,12 @@ export async function getMeritUnclaimedRewards(account: EthAddress, network: Net
137
137
  const unclaimed = getEthAmountForDecimals(unclaimedAmount, token.decimals);
138
138
 
139
139
  claimableTokens.push({
140
- claimType: ClaimType.AAVE_MERIT_REWARDS,
140
+ claimType: ClaimType.MERKL_REWARDS,
141
141
  amount: unclaimed,
142
142
  symbol: token.symbol,
143
143
  tokenAddress: token.address,
144
144
  walletAddress: account,
145
- label: 'AAVE Merit Rewards',
145
+ label: 'Merkl Rewards',
146
146
  underlyingSymbol: getAaveUnderlyingSymbol(token.symbol),
147
147
  additionalClaimFields: {
148
148
  accumulated: amount,
@@ -155,4 +155,16 @@ export async function getMeritUnclaimedRewards(account: EthAddress, network: Net
155
155
  });
156
156
 
157
157
  return claimableTokens;
158
- }
158
+ }
159
+
160
+ export async function getMeritUnclaimedRewards(account: EthAddress, network: NetworkNumber): Promise<ClaimableToken[]> {
161
+ const rewards = await getMerklUnclaimedRewards(account, network);
162
+
163
+ return rewards
164
+ .filter(reward => reward.symbol.startsWith('a'))
165
+ .map(reward => ({
166
+ ...reward,
167
+ claimType: ClaimType.AAVE_MERIT_REWARDS,
168
+ label: 'AAVE Merit Rewards',
169
+ }));
170
+ }
@@ -1,6 +1,7 @@
1
1
  import { getAssetInfoByAddress } from '@defisaver/tokens';
2
2
  import { aprToApy } from '../moneymarket';
3
- import { compareAddresses, LONGER_TIMEOUT, wethToEth } from '../services/utils';
3
+ import { fetchAllMerklOpportunities } from '../services/merkl';
4
+ import { compareAddresses, wethToEth } from '../services/utils';
4
5
  import {
5
6
  CompoundV3AssetData,
6
7
  IncentiveData,
@@ -22,12 +23,10 @@ import {
22
23
  */
23
24
  export const getCompoundV3MerklOpportunities = async (): Promise<MerklOpportunity[]> => {
24
25
  try {
25
- const res = await fetch('https://fe.defisaver.com/api/merkl/opportunities?mainProtocolId=compound-v3&status=LIVE&items=100', {
26
- signal: AbortSignal.timeout(LONGER_TIMEOUT),
26
+ return await fetchAllMerklOpportunities({
27
+ mainProtocolId: 'compound-v3',
28
+ status: OpportunityStatus.LIVE,
27
29
  });
28
- if (!res.ok) throw new Error('Failed to fetch Compound V3 Merkl campaigns');
29
- const data = await res.json();
30
- return Array.isArray(data) ? data as MerklOpportunity[] : [];
31
30
  } catch (e) {
32
31
  console.error('Failed to fetch Compound V3 Merkl campaigns', e);
33
32
  return [];
@@ -69,7 +69,7 @@ import { getViemProvider } from '../services/viem';
69
69
  import { attachFluidMerklIncentives, getFluidMerklCampaigns } from './merkl';
70
70
 
71
71
  export {
72
- FLUID_MERKL_OPPORTUNITY_TYPES,
72
+ FluidMerklOpportunityType,
73
73
  buildFluidMerklRewardMap,
74
74
  getFluidMerklCampaigns,
75
75
  attachFluidMerklIncentives,
@@ -1154,6 +1154,8 @@ export const EMPTY_FLUID_DATA = {
1154
1154
  minRatio: '0',
1155
1155
  netApy: '0',
1156
1156
  incentiveUsd: '0',
1157
+ merklSupplyIncentives: [],
1158
+ merklBorrowIncentives: [],
1157
1159
  totalInterestUsd: '0',
1158
1160
  isSubscribedToAutomation: false,
1159
1161
  automationResubscribeRequired: false,
@@ -1809,9 +1811,13 @@ export const _getUserPositionsPortfolio = async (provider: PublicClient, network
1809
1811
  if (!tokens.includes('ETH')) tokens.push('ETH');
1810
1812
  if (!tokens.includes('WBTC')) tokens.push('WBTC');
1811
1813
 
1812
- const tokenPrices = await getTokensPricesForPortfolio(tokens, provider, network);
1814
+ const [tokenPrices, merklCampaigns] = await Promise.all([
1815
+ getTokensPricesForPortfolio(tokens, provider, network),
1816
+ getFluidMerklCampaigns(network),
1817
+ ]);
1813
1818
 
1814
- const parsedMarketData = (await Promise.all(data[1].map(async (vaultData) => parseMarketData(provider, vaultData, network, tokenPrices))));
1819
+ const parsedMarketData = (await Promise.all(data[1].map(async (vaultData) => parseMarketData(provider, vaultData, network, tokenPrices))))
1820
+ .map(marketData => (marketData ? attachFluidMerklIncentives(marketData, merklCampaigns) : marketData));
1815
1821
 
1816
1822
  const userData = data[0].map((position, i) => (parsedMarketData[i] && { ...parseUserData(position, parsedMarketData[i]) }));
1817
1823
 
@@ -1,11 +1,13 @@
1
+ import memoize from 'memoizee';
1
2
  import { aprToApy } from '../moneymarket';
2
- import { LONGER_TIMEOUT } from '../services/utils';
3
+ import { fetchAllMerklOpportunities } from '../services/merkl';
3
4
  import {
4
5
  FluidMarketData,
5
6
  FluidMerklRewardMap,
6
7
  IncentiveData,
7
8
  IncentiveEligibilityId,
8
9
  IncentiveKind,
10
+ IncentiveSource,
9
11
  MerklOpportunity,
10
12
  NetworkNumber,
11
13
  OpportunityStatus,
@@ -25,7 +27,10 @@ import {
25
27
  * ids apply unconditionally, mirroring Aave V3. Whitelist-gated campaigns only accrue to
26
28
  * whitelisted addresses, so they are skipped rather than advertised to every user.
27
29
  */
28
- export const FLUID_MERKL_OPPORTUNITY_TYPES = ['FLUIDVAULT_COLLATERAL', 'FLUIDVAULT_BORROW'] as const;
30
+ export enum FluidMerklOpportunityType {
31
+ Collateral = 'FLUIDVAULT_COLLATERAL',
32
+ Borrow = 'FLUIDVAULT_BORROW',
33
+ }
29
34
 
30
35
  // '0x' + 20-byte vault address; conditional campaigns append a marker after it
31
36
  const VAULT_ADDRESS_LENGTH = 42;
@@ -36,6 +41,7 @@ const buildMerklIncentive = (opportunity: MerklOpportunity): IncentiveData => {
36
41
  apy: aprToApy(opportunity.apr),
37
42
  token,
38
43
  incentiveKind: IncentiveKind.Reward,
44
+ source: IncentiveSource.Merkl,
39
45
  description: `Eligible for ${token} rewards through Merkl.${opportunity.description ? `\n${opportunity.description}` : ''}`,
40
46
  eligibilityId: opportunity.identifier as IncentiveEligibilityId,
41
47
  };
@@ -47,13 +53,13 @@ export const buildFluidMerklRewardMap = (opportunities: MerklOpportunity[], chai
47
53
  opportunities
48
54
  .filter((o) => o.chainId === chainId)
49
55
  .filter((o) => o.status === OpportunityStatus.LIVE)
50
- .filter((o) => (FLUID_MERKL_OPPORTUNITY_TYPES as readonly string[]).includes(o.type))
56
+ .filter((o) => Object.values(FluidMerklOpportunityType).includes(o.type as FluidMerklOpportunityType))
51
57
  .filter((o) => !o.identifier?.includes('WHITELIST'))
52
58
  .forEach((o) => {
53
59
  const vaultAddress = o.identifier?.slice(0, VAULT_ADDRESS_LENGTH).toLowerCase();
54
60
  if (!vaultAddress || vaultAddress.length !== VAULT_ADDRESS_LENGTH) return;
55
61
  if (!result[vaultAddress]) result[vaultAddress] = { supply: [], borrow: [] };
56
- result[vaultAddress][o.type === 'FLUIDVAULT_BORROW' ? 'borrow' : 'supply'].push(buildMerklIncentive(o));
62
+ result[vaultAddress][o.type === FluidMerklOpportunityType.Borrow ? 'borrow' : 'supply'].push(buildMerklIncentive(o));
57
63
  });
58
64
 
59
65
  return result;
@@ -61,22 +67,21 @@ export const buildFluidMerklRewardMap = (opportunities: MerklOpportunity[], chai
61
67
 
62
68
  /**
63
69
  * Fetches live vault-scoped Fluid campaigns for the chain, keyed by vault address. Never throws —
64
- * on any failure it returns an empty map, so Merkl being down can't break market data. The query
65
- * is narrowed by `type=` and capped at `items=100` because Merkl paginates at 20 items by default.
70
+ * on any failure it returns an empty map, so Merkl being down can't break market data.
66
71
  */
67
- export const getFluidMerklCampaigns = async (chainId: NetworkNumber): Promise<FluidMerklRewardMap> => {
72
+ export const getFluidMerklCampaigns = memoize(async (chainId: NetworkNumber): Promise<FluidMerklRewardMap> => {
68
73
  try {
69
- const res = await fetch(`https://fe.defisaver.com/api/merkl/opportunities?mainProtocolId=fluid&type=${FLUID_MERKL_OPPORTUNITY_TYPES.join(',')}&status=LIVE&items=100`, {
70
- signal: AbortSignal.timeout(LONGER_TIMEOUT),
74
+ const opportunities = await fetchAllMerklOpportunities({
75
+ mainProtocolId: 'fluid',
76
+ type: Object.values(FluidMerklOpportunityType).join(','),
77
+ status: OpportunityStatus.LIVE,
71
78
  });
72
- if (!res.ok) throw new Error('Failed to fetch Fluid Merkl campaigns');
73
- const opportunities = await res.json();
74
- return buildFluidMerklRewardMap(Array.isArray(opportunities) ? opportunities : [], chainId);
79
+ return buildFluidMerklRewardMap(opportunities, chainId);
75
80
  } catch (e) {
76
81
  console.error('Failed to fetch Fluid Merkl campaigns', e);
77
82
  return {};
78
83
  }
79
- };
84
+ }, { promise: true, maxAge: 60 * 1000 });
80
85
 
81
86
  /**
82
87
  * Appends the vault's Merkl campaigns to its assets' incentive arrays — supply rewards on every
@@ -5,16 +5,56 @@ import {
5
5
  FluidAssetsData, FluidDexBorrowDataStructOutput, FluidDexSupplyDataStructOutput, FluidUsedAsset,
6
6
  FluidUsedAssets,
7
7
  FluidVaultType,
8
+ IncentiveData,
9
+ IncentiveSource,
8
10
  InnerFluidMarketData,
9
11
  } from '../../types';
10
12
  import {
11
13
  calcLeverageLiqPrice, getAssetsTotal, getExposure, isLeveragedPos,
12
14
  } from '../../moneymarket';
13
- import { calculateNetApy } from '../../staking';
14
- import { LeverageType, MMAssetsData } from '../../types/common';
15
+ import { calculateInterestEarned, calculateNetApy, EligibilityMapping } from '../../staking';
16
+ import { IncentiveSide, LeverageType, MMAssetsData } from '../../types/common';
15
17
  import { getEthAmountForDecimals } from '../../services/utils';
16
18
 
17
- const calculateNetApyDex = ({ marketData, suppliedUsd, borrowedUsd }: { marketData: InnerFluidMarketData, suppliedUsd: string, borrowedUsd: string }) => {
19
+ export const getUniqueFluidMerklIncentives = (assetsData: FluidAssetsData, side: IncentiveSide): IncentiveData[] => {
20
+ const field = side === IncentiveSide.Supply ? 'supplyIncentives' : 'borrowIncentives';
21
+ const incentives = Object.values(assetsData)
22
+ .flatMap(asset => asset[field])
23
+ .filter(incentive => incentive.source === IncentiveSource.Merkl);
24
+
25
+ return [...new Map(incentives.map(incentive => [
26
+ [incentive.eligibilityId, incentive.token, incentive.apy, incentive.description].join('|'),
27
+ incentive,
28
+ ])).values()];
29
+ };
30
+
31
+ const getMerklIncentiveInterest = (
32
+ incentives: IncentiveData[],
33
+ principal: string,
34
+ usedAssets: FluidUsedAssets,
35
+ ) => incentives.reduce((total, { apy, eligibilityId }) => {
36
+ const eligibilityCheck = eligibilityId ? EligibilityMapping[eligibilityId] : null;
37
+ const { isEligible, eligibleUSDAmount } = eligibilityCheck
38
+ ? eligibilityCheck(usedAssets, undefined)
39
+ : { isEligible: true, eligibleUSDAmount: principal };
40
+ const incentiveInterest = isEligible ? calculateInterestEarned(eligibleUSDAmount, apy, 'year', true) : 0;
41
+
42
+ return new Dec(total).add(incentiveInterest).toString();
43
+ }, '0');
44
+
45
+ const calculateNetApyDex = ({
46
+ marketData,
47
+ assetsData,
48
+ usedAssets,
49
+ suppliedUsd,
50
+ borrowedUsd,
51
+ }: {
52
+ marketData: InnerFluidMarketData,
53
+ assetsData: FluidAssetsData,
54
+ usedAssets: FluidUsedAssets,
55
+ suppliedUsd: string,
56
+ borrowedUsd: string,
57
+ }) => {
18
58
  const {
19
59
  borrowRate,
20
60
  supplyRate,
@@ -29,7 +69,21 @@ const calculateNetApyDex = ({ marketData, suppliedUsd, borrowedUsd }: { marketDa
29
69
 
30
70
  const borrowIncentive = new Dec(incentiveBorrowRate || '0').mul(borrowedUsd).div(100).toString();
31
71
  const supplyIncentive = new Dec(incentiveSupplyRate || '0').mul(suppliedUsd).div(100).toString();
32
- const incentiveUsd = new Dec(supplyIncentive).minus(borrowIncentive).toString();
72
+ const merklSupplyIncentive = getMerklIncentiveInterest(
73
+ getUniqueFluidMerklIncentives(assetsData, IncentiveSide.Supply),
74
+ suppliedUsd,
75
+ usedAssets,
76
+ );
77
+ const merklBorrowIncentive = getMerklIncentiveInterest(
78
+ getUniqueFluidMerklIncentives(assetsData, IncentiveSide.Borrow),
79
+ borrowedUsd,
80
+ usedAssets,
81
+ );
82
+ const incentiveUsd = new Dec(supplyIncentive)
83
+ .minus(borrowIncentive)
84
+ .add(merklSupplyIncentive)
85
+ .add(merklBorrowIncentive)
86
+ .toString();
33
87
 
34
88
  const borrowInterest = new Dec(totalBorrowRate).mul(borrowedUsd).div(100).toString();
35
89
  const supplyInterest = new Dec(totalSupplyRate).mul(suppliedUsd).div(100).toString();
@@ -66,7 +120,15 @@ borrowShares?: string,
66
120
  : new Dec(marketData.debtSharePrice!).mul(borrowShares!).toString();
67
121
 
68
122
  const isDex = [FluidVaultType.T2, FluidVaultType.T3, FluidVaultType.T4].includes(marketData.vaultType);
69
- const { netApy, incentiveUsd, totalInterestUsd } = isDex ? calculateNetApyDex({ marketData, suppliedUsd: payload.suppliedUsd, borrowedUsd: payload.borrowedUsd }) : calculateNetApy({ usedAssets, assetsData: assetsData as unknown as MMAssetsData });
123
+ payload.merklSupplyIncentives = getUniqueFluidMerklIncentives(assetsData, IncentiveSide.Supply);
124
+ payload.merklBorrowIncentives = getUniqueFluidMerklIncentives(assetsData, IncentiveSide.Borrow);
125
+ const { netApy, incentiveUsd, totalInterestUsd } = isDex ? calculateNetApyDex({
126
+ marketData,
127
+ assetsData,
128
+ usedAssets,
129
+ suppliedUsd: payload.suppliedUsd,
130
+ borrowedUsd: payload.borrowedUsd,
131
+ }) : calculateNetApy({ usedAssets, assetsData: assetsData as unknown as MMAssetsData });
70
132
  payload.netApy = netApy;
71
133
  payload.incentiveUsd = incentiveUsd;
72
134
  payload.totalInterestUsd = totalInterestUsd;
@@ -337,4 +399,4 @@ export const mergeUsedAssets = (existing: Partial<FluidUsedAsset> = {}, addition
337
399
  ...EMPTY_USED_ASSET,
338
400
  ...existing,
339
401
  ...additional,
340
- });
402
+ });
@@ -22,7 +22,7 @@ import { getViemProvider, setViemBlockNumber } from '../services/viem';
22
22
  import { addMorphoBlueMerklOpportunitiesToMarketInfo, getMorphoBlueMerklOpportunities } from './merkl';
23
23
 
24
24
  export {
25
- MORPHO_BLUE_MERKL_OPPORTUNITY_TYPES,
25
+ MorphoBlueMerklOpportunityType,
26
26
  addMorphoBlueMerklOpportunitiesToMarketInfo,
27
27
  getMorphoBlueMerklOpportunities,
28
28
  addMorphoBlueMerklRewardsToMarketInfo,
@@ -1,5 +1,5 @@
1
1
  import { aprToApy } from '../moneymarket';
2
- import { LONGER_TIMEOUT } from '../services/utils';
2
+ import { fetchAllMerklOpportunities } from '../services/merkl';
3
3
  import {
4
4
  IncentiveData,
5
5
  IncentiveKind,
@@ -23,7 +23,11 @@ import {
23
23
  * identifier (e.g. `0x54cf…c46WHITELIST_CAMPAIGN`); their rewards only accrue to whitelisted
24
24
  * addresses, so they are skipped rather than advertised to every user.
25
25
  */
26
- export const MORPHO_BLUE_MERKL_OPPORTUNITY_TYPES = ['MORPHOSUPPLY', 'MORPHOCOLLATERAL', 'MORPHOBORROW'] as const;
26
+ export enum MorphoBlueMerklOpportunityType {
27
+ Supply = 'MORPHOSUPPLY',
28
+ Collateral = 'MORPHOCOLLATERAL',
29
+ Borrow = 'MORPHOBORROW',
30
+ }
27
31
 
28
32
  const MERKL_DESCRIPTION_MARKER = 'through Merkl';
29
33
 
@@ -61,16 +65,16 @@ export const addMorphoBlueMerklOpportunitiesToMarketInfo = (
61
65
 
62
66
  const relevant = opportunities.filter((o) => o.chainId === chainId
63
67
  && o.status === OpportunityStatus.LIVE
64
- && (MORPHO_BLUE_MERKL_OPPORTUNITY_TYPES as readonly string[]).includes(o.type)
68
+ && Object.values(MorphoBlueMerklOpportunityType).includes(o.type as MorphoBlueMerklOpportunityType)
65
69
  // a suffixed identifier marks a whitelist-gated campaign — skip those
66
70
  && o.identifier?.length === MARKET_ID_PREFIX_LENGTH
67
71
  && o.identifier.toLowerCase() === marketIdPrefix);
68
72
  if (!relevant.length) return marketInfo;
69
73
 
70
- const incentivesByType = (type: string) => relevant.filter((o) => o.type === type).map(buildMerklIncentive);
71
- const collateralSupply = incentivesByType('MORPHOCOLLATERAL');
72
- const loanSupply = incentivesByType('MORPHOSUPPLY');
73
- const loanBorrow = incentivesByType('MORPHOBORROW');
74
+ const incentivesByType = (type: MorphoBlueMerklOpportunityType) => relevant.filter((o) => o.type === type).map(buildMerklIncentive);
75
+ const collateralSupply = incentivesByType(MorphoBlueMerklOpportunityType.Collateral);
76
+ const loanSupply = incentivesByType(MorphoBlueMerklOpportunityType.Supply);
77
+ const loanBorrow = incentivesByType(MorphoBlueMerklOpportunityType.Borrow);
74
78
 
75
79
  return {
76
80
  ...marketInfo,
@@ -91,18 +95,15 @@ export const addMorphoBlueMerklOpportunitiesToMarketInfo = (
91
95
 
92
96
  /**
93
97
  * Fetches live market-scoped Morpho Blue campaigns for all chains. Never throws — on any failure
94
- * it returns an empty list, so Merkl being down can't break market data. The query is narrowed by
95
- * `type=` and capped at `items=100` because Merkl paginates at 20 items by default — an
96
- * unfiltered protocol query would silently drop campaigns once Morpho has enough opportunities.
98
+ * it returns an empty list, so Merkl being down can't break market data.
97
99
  */
98
100
  export const getMorphoBlueMerklOpportunities = async (): Promise<MerklOpportunity[]> => {
99
101
  try {
100
- const res = await fetch(`https://fe.defisaver.com/api/merkl/opportunities?mainProtocolId=morpho&type=${MORPHO_BLUE_MERKL_OPPORTUNITY_TYPES.join(',')}&status=LIVE&items=100`, {
101
- signal: AbortSignal.timeout(LONGER_TIMEOUT),
102
+ return await fetchAllMerklOpportunities({
103
+ mainProtocolId: 'morpho',
104
+ type: Object.values(MorphoBlueMerklOpportunityType).join(','),
105
+ status: OpportunityStatus.LIVE,
102
106
  });
103
- if (!res.ok) throw new Error('Failed to fetch Morpho Blue Merkl campaigns');
104
- const data = await res.json();
105
- return Array.isArray(data) ? data as MerklOpportunity[] : [];
106
107
  } catch (e) {
107
108
  console.error('Failed to fetch Morpho Blue Merkl campaigns', e);
108
109
  return [];
@@ -41,7 +41,7 @@ import { _getLiquityTroveInfo, getLiquityStakingData } from '../liquity';
41
41
  import { _getLiquityV2MarketData, getLiquitySAndYBold, getLiquityV2Staking } from '../liquityV2';
42
42
  import { _getAllUserEarnPositionsWithFTokens, _getUserPositionsPortfolio } from '../fluid';
43
43
  import { getUmbrellaData } from '../umbrella';
44
- import { getMeritUnclaimedRewards, getUnclaimedRewardsForAllMarkets } from '../claiming/aaveV3';
44
+ import { getMerklUnclaimedRewards, getUnclaimedRewardsForAllMarkets } from '../claiming/aaveV3';
45
45
  import { getCompoundV3Rewards } from '../claiming/compV3';
46
46
  import { fetchSparkAirdropRewards, fetchSparkRewards } from '../claiming/spark';
47
47
  import { getKingRewards } from '../claiming/king';
@@ -143,7 +143,7 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
143
143
  };
144
144
 
145
145
  rewardsData[address.toLowerCase() as EthAddress] = {
146
- aaveV3merit: {},
146
+ merkl: {},
147
147
  aaveV3: {},
148
148
  compV3: {},
149
149
  spark: {},
@@ -352,11 +352,11 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
352
352
  })).flat(),
353
353
  ...addresses.map(async (address) => {
354
354
  try {
355
- const aaveMeritData = await getMeritUnclaimedRewards(address, network);
356
- rewardsData[address.toLowerCase() as EthAddress].aaveV3merit = { error: '', data: aaveMeritData };
355
+ const merklData = await getMerklUnclaimedRewards(address, network);
356
+ rewardsData[address.toLowerCase() as EthAddress].merkl = { error: '', data: merklData };
357
357
  } catch (error) {
358
- console.error(`Error fetching Aave V3 Merit rewards data for address ${address}:`, error);
359
- rewardsData[address.toLowerCase() as EthAddress].aaveV3merit = { error: `Error fetching Aave V3 Merit rewards data for address ${address}`, data: null };
358
+ console.error(`Error fetching Merkl rewards data for address ${address}:`, error);
359
+ rewardsData[address.toLowerCase() as EthAddress].merkl = { error: `Error fetching Merkl rewards data for address ${address}`, data: null };
360
360
  }
361
361
  }),
362
362
  ...aaveV3Markets.map(market => addresses.map(async (address) => {