@defisaver/positions-sdk 2.1.141 → 2.1.142

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 (55) hide show
  1. package/cjs/aaveV3/merkl.js +10 -9
  2. package/cjs/aaveV4/merkl.js +5 -6
  3. package/cjs/compoundV3/index.d.ts +1 -0
  4. package/cjs/compoundV3/index.js +8 -2
  5. package/cjs/compoundV3/merkl.d.ts +16 -0
  6. package/cjs/compoundV3/merkl.js +77 -0
  7. package/cjs/fluid/index.d.ts +1 -0
  8. package/cjs/fluid/index.js +28 -8
  9. package/cjs/fluid/merkl.d.ts +31 -0
  10. package/cjs/fluid/merkl.js +99 -0
  11. package/cjs/morphoBlue/index.d.ts +1 -0
  12. package/cjs/morphoBlue/index.js +16 -10
  13. package/cjs/morphoBlue/merkl.d.ts +35 -0
  14. package/cjs/morphoBlue/merkl.js +102 -0
  15. package/cjs/services/merkl.d.ts +6 -0
  16. package/cjs/services/merkl.js +39 -0
  17. package/cjs/staking/eligibility.d.ts +4 -0
  18. package/cjs/staking/eligibility.js +11 -1
  19. package/cjs/types/common.d.ts +2 -1
  20. package/cjs/types/common.js +1 -0
  21. package/cjs/types/merkl.d.ts +8 -0
  22. package/esm/aaveV3/merkl.js +11 -10
  23. package/esm/aaveV4/merkl.js +5 -6
  24. package/esm/compoundV3/index.d.ts +1 -0
  25. package/esm/compoundV3/index.js +5 -1
  26. package/esm/compoundV3/merkl.d.ts +16 -0
  27. package/esm/compoundV3/merkl.js +72 -0
  28. package/esm/fluid/index.d.ts +1 -0
  29. package/esm/fluid/index.js +23 -7
  30. package/esm/fluid/merkl.d.ts +31 -0
  31. package/esm/fluid/merkl.js +93 -0
  32. package/esm/morphoBlue/index.d.ts +1 -0
  33. package/esm/morphoBlue/index.js +11 -9
  34. package/esm/morphoBlue/merkl.d.ts +35 -0
  35. package/esm/morphoBlue/merkl.js +96 -0
  36. package/esm/services/merkl.d.ts +6 -0
  37. package/esm/services/merkl.js +35 -0
  38. package/esm/staking/eligibility.d.ts +4 -0
  39. package/esm/staking/eligibility.js +9 -0
  40. package/esm/types/common.d.ts +2 -1
  41. package/esm/types/common.js +1 -0
  42. package/esm/types/merkl.d.ts +8 -0
  43. package/package.json +1 -1
  44. package/src/aaveV3/merkl.ts +7 -7
  45. package/src/aaveV4/merkl.ts +5 -5
  46. package/src/compoundV3/index.ts +6 -1
  47. package/src/compoundV3/merkl.ts +76 -0
  48. package/src/fluid/index.ts +29 -7
  49. package/src/fluid/merkl.ts +99 -0
  50. package/src/morphoBlue/index.ts +24 -9
  51. package/src/morphoBlue/merkl.ts +121 -0
  52. package/src/services/merkl.ts +31 -0
  53. package/src/staking/eligibility.ts +10 -0
  54. package/src/types/common.ts +1 -0
  55. package/src/types/merkl.ts +6 -0
@@ -19,6 +19,15 @@ import {
19
19
  import { getChainlinkAssetAddress } from '../services/priceService';
20
20
  import { getViemProvider, setViemBlockNumber } from '../services/viem';
21
21
 
22
+ import { addMorphoBlueMerklOpportunitiesToMarketInfo, getMorphoBlueMerklOpportunities } from './merkl';
23
+
24
+ export {
25
+ MorphoBlueMerklOpportunityType,
26
+ addMorphoBlueMerklOpportunitiesToMarketInfo,
27
+ getMorphoBlueMerklOpportunities,
28
+ addMorphoBlueMerklRewardsToMarketInfo,
29
+ } from './merkl';
30
+
22
31
  const HARDCODED_USD_STABLE_PRICE = '100000000'; // $1 with 8 decimals
23
32
 
24
33
  const getMorphoRewardIncentives = (apy: string) => [{
@@ -151,15 +160,21 @@ async function getMorphoBlueMarketDataInternal(
151
160
  }
152
161
 
153
162
  export async function _getMorphoBlueMarketData(provider: Client, network: NetworkNumber, selectedMarket: MorphoBlueMarketData): Promise<MorphoBlueMarketInfo> {
154
- const marketInfo = await getMorphoBlueMarketDataInternal(provider, network, selectedMarket);
155
-
156
- try {
157
- const rewards = await getRewardsForMarket(selectedMarket.marketId, network);
158
- return addMorphoBlueRewardsToMarketInfo(marketInfo, rewards);
159
- } catch (error) {
160
- console.error(error);
161
- return addMorphoBlueRewardsToMarketInfo(marketInfo, { supplyApy: '0', borrowApy: '0' });
162
- }
163
+ const [marketInfo, rewards, merklOpportunities] = await Promise.all([
164
+ getMorphoBlueMarketDataInternal(provider, network, selectedMarket),
165
+ getRewardsForMarket(selectedMarket.marketId, network).catch((error) => {
166
+ console.error(error);
167
+ return { supplyApy: '0', borrowApy: '0' };
168
+ }),
169
+ getMorphoBlueMerklOpportunities(),
170
+ ]);
171
+
172
+ return addMorphoBlueMerklOpportunitiesToMarketInfo(
173
+ addMorphoBlueRewardsToMarketInfo(marketInfo, rewards),
174
+ selectedMarket,
175
+ network,
176
+ merklOpportunities,
177
+ );
163
178
  }
164
179
 
165
180
  export function _getMorphoBluePortfolioMarketData(provider: Client, network: NetworkNumber, selectedMarket: MorphoBlueMarketData): Promise<MorphoBlueMarketInfo> {
@@ -0,0 +1,121 @@
1
+ import { aprToApy } from '../moneymarket';
2
+ import { fetchAllMerklOpportunities } from '../services/merkl';
3
+ import {
4
+ IncentiveData,
5
+ IncentiveKind,
6
+ MerklOpportunity,
7
+ MorphoBlueMarketData,
8
+ MorphoBlueMarketInfo,
9
+ NetworkNumber,
10
+ OpportunityStatus,
11
+ } from '../types';
12
+
13
+ /**
14
+ * Merkl tags Morpho Blue market-level reward campaigns via the opportunity `type` field:
15
+ * - MORPHOSUPPLY → reward for supplying the market's loan asset
16
+ * - MORPHOCOLLATERAL → reward for depositing the market's collateral asset
17
+ * - MORPHOBORROW → reward for borrowing the market's loan asset
18
+ * (MORPHOVAULT/ERC20LOGPROCESSOR are MetaMorpho vault campaigns and MORPHOSUPPLY_SINGLETOKEN is
19
+ * token-wide rather than market-scoped, so none of them belong on per-market data.)
20
+ *
21
+ * A campaign identifies its market via `identifier` — the first 20 bytes of the 32-byte Morpho
22
+ * market id, formatted as a checksummed address. Whitelist-gated campaigns append a marker to that
23
+ * identifier (e.g. `0x54cf…c46WHITELIST_CAMPAIGN`); their rewards only accrue to whitelisted
24
+ * addresses, so they are skipped rather than advertised to every user.
25
+ */
26
+ export enum MorphoBlueMerklOpportunityType {
27
+ Supply = 'MORPHOSUPPLY',
28
+ Collateral = 'MORPHOCOLLATERAL',
29
+ Borrow = 'MORPHOBORROW',
30
+ }
31
+
32
+ const MERKL_DESCRIPTION_MARKER = 'through Merkl';
33
+
34
+ // '0x' + first 20 bytes of the market id
35
+ const MARKET_ID_PREFIX_LENGTH = 42;
36
+
37
+ const buildMerklIncentive = (opportunity: MerklOpportunity): IncentiveData => {
38
+ const token = opportunity.rewardsRecord?.breakdowns?.[0]?.token?.symbol || opportunity.tokens?.[0]?.symbol || '';
39
+ return {
40
+ apy: aprToApy(opportunity.apr),
41
+ token,
42
+ incentiveKind: IncentiveKind.Reward,
43
+ description: `Eligible for ${token} rewards through Merkl.${opportunity.description ? `\n${opportunity.description}` : ''}`,
44
+ };
45
+ };
46
+
47
+ const withoutMerklIncentives = (incentives: IncentiveData[] = []) => incentives
48
+ .filter(({ description }) => !description?.includes(MERKL_DESCRIPTION_MARKER));
49
+
50
+ /**
51
+ * Appends the given live Merkl campaigns for the market to its `assetsData` incentive arrays
52
+ * (collateral supply / loan supply / loan borrow). Existing Merkl incentives are replaced rather
53
+ * than duplicated, so re-applying on refreshed data is safe.
54
+ */
55
+ export const addMorphoBlueMerklOpportunitiesToMarketInfo = (
56
+ marketInfo: MorphoBlueMarketInfo,
57
+ selectedMarket: MorphoBlueMarketData,
58
+ chainId: NetworkNumber,
59
+ opportunities: MerklOpportunity[],
60
+ ): MorphoBlueMarketInfo => {
61
+ const marketIdPrefix = (selectedMarket.marketId || '').slice(0, MARKET_ID_PREFIX_LENGTH).toLowerCase();
62
+ const collateralAsset = marketInfo.assetsData[marketInfo.collateralToken];
63
+ const loanAsset = marketInfo.assetsData[marketInfo.loanToken];
64
+ if (marketIdPrefix.length !== MARKET_ID_PREFIX_LENGTH || !collateralAsset || !loanAsset) return marketInfo;
65
+
66
+ const relevant = opportunities.filter((o) => o.chainId === chainId
67
+ && o.status === OpportunityStatus.LIVE
68
+ && Object.values(MorphoBlueMerklOpportunityType).includes(o.type as MorphoBlueMerklOpportunityType)
69
+ // a suffixed identifier marks a whitelist-gated campaign — skip those
70
+ && o.identifier?.length === MARKET_ID_PREFIX_LENGTH
71
+ && o.identifier.toLowerCase() === marketIdPrefix);
72
+ if (!relevant.length) return marketInfo;
73
+
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);
78
+
79
+ return {
80
+ ...marketInfo,
81
+ assetsData: {
82
+ ...marketInfo.assetsData,
83
+ [marketInfo.collateralToken]: {
84
+ ...collateralAsset,
85
+ supplyIncentives: [...withoutMerklIncentives(collateralAsset.supplyIncentives), ...collateralSupply],
86
+ },
87
+ [marketInfo.loanToken]: {
88
+ ...loanAsset,
89
+ supplyIncentives: [...withoutMerklIncentives(loanAsset.supplyIncentives), ...loanSupply],
90
+ borrowIncentives: [...withoutMerklIncentives(loanAsset.borrowIncentives), ...loanBorrow],
91
+ },
92
+ },
93
+ };
94
+ };
95
+
96
+ /**
97
+ * Fetches live market-scoped Morpho Blue campaigns for all chains. Never throws — on any failure
98
+ * it returns an empty list, so Merkl being down can't break market data.
99
+ */
100
+ export const getMorphoBlueMerklOpportunities = async (): Promise<MerklOpportunity[]> => {
101
+ try {
102
+ return await fetchAllMerklOpportunities({
103
+ mainProtocolId: 'morpho',
104
+ type: Object.values(MorphoBlueMerklOpportunityType).join(','),
105
+ status: OpportunityStatus.LIVE,
106
+ });
107
+ } catch (e) {
108
+ console.error('Failed to fetch Morpho Blue Merkl campaigns', e);
109
+ return [];
110
+ }
111
+ };
112
+
113
+ /**
114
+ * Fetches live Merkl campaigns and appends the ones for the market to its `assetsData` incentive
115
+ * arrays. Never throws — on any failure the market info is returned unchanged.
116
+ */
117
+ export const addMorphoBlueMerklRewardsToMarketInfo = async (
118
+ marketInfo: MorphoBlueMarketInfo,
119
+ selectedMarket: MorphoBlueMarketData,
120
+ network: NetworkNumber,
121
+ ): Promise<MorphoBlueMarketInfo> => addMorphoBlueMerklOpportunitiesToMarketInfo(marketInfo, selectedMarket, network, await getMorphoBlueMerklOpportunities());
@@ -0,0 +1,31 @@
1
+ import { MerklOpportunity } from '../types';
2
+ import { LONGER_TIMEOUT } from './utils';
3
+
4
+ const MERKL_OPPORTUNITIES_URL = 'https://fe.defisaver.com/api/merkl/opportunities';
5
+
6
+ /**
7
+ * Fetches every page matching the query. Merkl returns 20 opportunities by default, so omitting
8
+ * `items` and advancing `page` avoids imposing a client-side maximum on the result set.
9
+ */
10
+ export const fetchAllMerklOpportunities = async (query: Record<string, string>): Promise<MerklOpportunity[]> => {
11
+ const opportunities: MerklOpportunity[] = [];
12
+ let page = 0;
13
+ let pageOpportunities: MerklOpportunity[];
14
+
15
+ do {
16
+ const searchParams = new URLSearchParams({ ...query, page: page.toString() });
17
+ const res = await fetch(`${MERKL_OPPORTUNITIES_URL}?${searchParams}`, { // eslint-disable-line no-await-in-loop
18
+ signal: AbortSignal.timeout(LONGER_TIMEOUT),
19
+ });
20
+ if (!res.ok) throw new Error(`Failed to fetch Merkl opportunities page ${page}`);
21
+
22
+ const data = await res.json(); // eslint-disable-line no-await-in-loop
23
+ if (!Array.isArray(data)) throw new Error(`Invalid Merkl opportunities response on page ${page}`);
24
+
25
+ pageOpportunities = data as MerklOpportunity[];
26
+ opportunities.push(...pageOpportunities);
27
+ page += 1;
28
+ } while (pageOpportunities.length > 0);
29
+
30
+ return opportunities;
31
+ };
@@ -97,10 +97,20 @@ export const isEligibleForAaveV3ArbitrumETHLSBorrow = (usedAssets: MMUsedAssets)
97
97
  return { isEligible: true, eligibleUSDAmount: ETHAmountBorrowed };
98
98
  };
99
99
 
100
+ // "Borrow WETH with pufETH collateral" — rewards are calculated as min(collateral * 80% LTV, borrow amount)
101
+ export const isEligibleForCompoundV3PufEthWethBorrow = (usedAssets: MMUsedAssets) => {
102
+ const collateralUsd = usedAssets.pufETH?.suppliedUsd || '0';
103
+ const borrowedUsd = usedAssets.ETH?.borrowedUsd || '0';
104
+ const eligibleUSDAmount = Dec.min(new Dec(collateralUsd).mul('0.8'), borrowedUsd).toString();
105
+
106
+ return { isEligible: new Dec(eligibleUSDAmount).gt(0), eligibleUSDAmount };
107
+ };
108
+
100
109
  export const EligibilityMapping: { [key in IncentiveEligibilityId]: (usedAssets: MMUsedAssets, optionalData: any) => { isEligible: boolean; eligibleUSDAmount: string } } = {
101
110
  [IncentiveEligibilityId.AaveV3EthenaLiquidLeverage]: isEligibleForEthenaUSDeRewards,
102
111
  [IncentiveEligibilityId.AaveV3ArbitrumEthSupply]: isEligibleForAaveV3ArbitrumEthSupply,
103
112
  [IncentiveEligibilityId.AaveV3ArbitrumETHLSBorrow]: isEligibleForAaveV3ArbitrumETHLSBorrow,
104
113
  [IncentiveEligibilityId.AaveV3EthenaLiquidLeveragePlasma]: isEligibleForEthenaUSDeRewards,
105
114
  [IncentiveEligibilityId.AaveV3EthenaLiquidLeveragePlasmaGHO]: isEligibleForEthenaGHORewards,
115
+ [IncentiveEligibilityId.CompoundV3PufEthWethBorrow]: isEligibleForCompoundV3PufEthWethBorrow,
106
116
  };
@@ -14,6 +14,7 @@ export enum IncentiveEligibilityId {
14
14
  AaveV3ArbitrumETHLSBorrow = '0x0c84331e39d6658Cd6e6b9ba04736cC4c4734351',
15
15
  AaveV3EthenaLiquidLeveragePlasma = '0x2E8f1FA9c73F9d975B46BDFe40C92b6dDEFA3f31BORROW_BL',
16
16
  AaveV3EthenaLiquidLeveragePlasmaGHO = '2b2f6fefcd3df634',
17
+ CompoundV3PufEthWethBorrow = 'e6e49b79e83041f9',
17
18
  }
18
19
 
19
20
  export enum LeverageType {
@@ -72,6 +72,12 @@ export type MerkleRewardMap = Record<EthAddress, { supply?: MerkleRewardInfo; bo
72
72
 
73
73
  export type AaveV4MerklScopedReward = { [side in IncentiveSide]?: IncentiveData };
74
74
 
75
+ /**
76
+ * Fluid vault-scoped Merkl campaigns keyed by lowercase vault address — `supply` rewards apply to
77
+ * the vault's collateral side, `borrow` rewards to its debt side.
78
+ */
79
+ export type FluidMerklRewardMap = Record<string, { supply: IncentiveData[], borrow: IncentiveData[] }>;
80
+
75
81
  /**
76
82
  * Aave V4 Merkl reward campaigns split by scope:
77
83
  * - `hub`: keyed by `${hubAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing via a hub