@defisaver/positions-sdk 2.1.141-dev → 2.1.142-dev

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 +9 -5
  2. package/cjs/aaveV4/merkl.js +1 -1
  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 +79 -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 +29 -0
  10. package/cjs/fluid/merkl.js +97 -0
  11. package/cjs/helpers/liquityV2Helpers/index.d.ts +2 -1
  12. package/cjs/helpers/liquityV2Helpers/index.js +3 -2
  13. package/cjs/liquityV2/index.js +3 -4
  14. package/cjs/morphoBlue/index.d.ts +1 -0
  15. package/cjs/morphoBlue/index.js +16 -10
  16. package/cjs/morphoBlue/merkl.d.ts +33 -0
  17. package/cjs/morphoBlue/merkl.js +101 -0
  18. package/cjs/staking/eligibility.d.ts +4 -0
  19. package/cjs/staking/eligibility.js +11 -1
  20. package/cjs/types/common.d.ts +2 -1
  21. package/cjs/types/common.js +1 -0
  22. package/cjs/types/liquityV2.d.ts +3 -0
  23. package/cjs/types/merkl.d.ts +8 -0
  24. package/esm/aaveV3/merkl.js +9 -5
  25. package/esm/aaveV4/merkl.js +1 -1
  26. package/esm/compoundV3/index.d.ts +1 -0
  27. package/esm/compoundV3/index.js +5 -1
  28. package/esm/compoundV3/merkl.d.ts +16 -0
  29. package/esm/compoundV3/merkl.js +74 -0
  30. package/esm/fluid/index.d.ts +1 -0
  31. package/esm/fluid/index.js +23 -7
  32. package/esm/fluid/merkl.d.ts +29 -0
  33. package/esm/fluid/merkl.js +91 -0
  34. package/esm/helpers/liquityV2Helpers/index.d.ts +2 -1
  35. package/esm/helpers/liquityV2Helpers/index.js +3 -2
  36. package/esm/liquityV2/index.js +3 -4
  37. package/esm/morphoBlue/index.d.ts +1 -0
  38. package/esm/morphoBlue/index.js +11 -9
  39. package/esm/morphoBlue/merkl.d.ts +33 -0
  40. package/esm/morphoBlue/merkl.js +95 -0
  41. package/esm/staking/eligibility.d.ts +4 -0
  42. package/esm/staking/eligibility.js +9 -0
  43. package/esm/types/common.d.ts +2 -1
  44. package/esm/types/common.js +1 -0
  45. package/esm/types/liquityV2.d.ts +3 -0
  46. package/esm/types/merkl.d.ts +8 -0
  47. package/package.json +1 -1
  48. package/src/aaveV3/merkl.ts +5 -2
  49. package/src/aaveV4/merkl.ts +1 -1
  50. package/src/compoundV3/index.ts +6 -1
  51. package/src/compoundV3/merkl.ts +77 -0
  52. package/src/fluid/index.ts +29 -7
  53. package/src/fluid/merkl.ts +97 -0
  54. package/src/helpers/liquityV2Helpers/index.ts +4 -1
  55. package/src/liquityV2/index.ts +4 -3
  56. package/src/morphoBlue/index.ts +24 -9
  57. package/src/morphoBlue/merkl.ts +120 -0
  58. package/src/staking/eligibility.ts +10 -0
  59. package/src/types/common.ts +1 -0
  60. package/src/types/liquityV2.ts +3 -0
  61. package/src/types/merkl.ts +6 -0
@@ -309,15 +309,14 @@ export const _getLiquityV2TroveData = (provider_1, network_1, _a, ...args_1) =>
309
309
  const interestBatchManager = data.interestBatchManager;
310
310
  const lastInterestRateAdjTime = data.lastInterestRateAdjTime.toString();
311
311
  const hasInterestBatchManager = !compareAddresses(interestBatchManager, ZERO_ADDRESS);
312
- const liqRatio = hasInterestBatchManager ? new Dec(minCollRatio).add(batchCollRatio).toString() : minCollRatio;
312
+ const borrowLimitRatio = hasInterestBatchManager ? new Dec(minCollRatio).add(batchCollRatio).toString() : minCollRatio;
313
313
  const payload = Object.assign(Object.assign({ usedAssets,
314
314
  troveId,
315
315
  interestRate,
316
316
  interestBatchManager,
317
317
  debtInFront,
318
- lastInterestRateAdjTime,
319
- liqRatio, troveStatus: LIQUITY_V2_TROVE_STATUS_ENUM[parseInt(data.status.toString(), 10)] }, getLiquityV2AggregatedPositionData({
320
- usedAssets, assetsData, minCollRatio: liqRatio, interestRate,
318
+ lastInterestRateAdjTime, liqRatio: minCollRatio, borrowLimitRatio, troveStatus: LIQUITY_V2_TROVE_STATUS_ENUM[parseInt(data.status.toString(), 10)] }, getLiquityV2AggregatedPositionData({
319
+ usedAssets, assetsData, minCollRatio: borrowLimitRatio, interestRate, liqRatio: minCollRatio,
321
320
  })), { collRatio });
322
321
  return payload;
323
322
  });
@@ -1,6 +1,7 @@
1
1
  import { Client } from 'viem';
2
2
  import { Blockish, EthAddress, EthereumProvider, NetworkNumber, PositionBalances } from '../types/common';
3
3
  import { MorphoBlueEarnData, MorphoBlueMarketData, MorphoBlueMarketInfo, MorphoBlueMarketRewards, MorphoBluePositionData } from '../types';
4
+ export { MORPHO_BLUE_MERKL_OPPORTUNITY_TYPES, addMorphoBlueMerklOpportunitiesToMarketInfo, getMorphoBlueMerklOpportunities, addMorphoBlueMerklRewardsToMarketInfo, } from './merkl';
4
5
  export declare const addMorphoBlueRewardsToMarketInfo: (marketInfo: MorphoBlueMarketInfo, rewards: MorphoBlueMarketRewards) => MorphoBlueMarketInfo;
5
6
  export declare function _getMorphoBlueMarketData(provider: Client, network: NetworkNumber, selectedMarket: MorphoBlueMarketData): Promise<MorphoBlueMarketInfo>;
6
7
  export declare function _getMorphoBluePortfolioMarketData(provider: Client, network: NetworkNumber, selectedMarket: MorphoBlueMarketData): Promise<MorphoBlueMarketInfo>;
@@ -17,6 +17,8 @@ import { isMainnetNetwork, wethToEth } from '../services/utils';
17
17
  import { getBorrowRate, getMorphoBlueAggregatedPositionData, getRewardsForMarket, getSupplyRate, } from '../helpers/morphoBlueHelpers';
18
18
  import { getChainlinkAssetAddress } from '../services/priceService';
19
19
  import { getViemProvider, setViemBlockNumber } from '../services/viem';
20
+ import { addMorphoBlueMerklOpportunitiesToMarketInfo, getMorphoBlueMerklOpportunities } from './merkl';
21
+ export { MORPHO_BLUE_MERKL_OPPORTUNITY_TYPES, addMorphoBlueMerklOpportunitiesToMarketInfo, getMorphoBlueMerklOpportunities, addMorphoBlueMerklRewardsToMarketInfo, } from './merkl';
20
22
  const HARDCODED_USD_STABLE_PRICE = '100000000'; // $1 with 8 decimals
21
23
  const getMorphoRewardIncentives = (apy) => [{
22
24
  token: 'MORPHO',
@@ -119,15 +121,15 @@ function getMorphoBlueMarketDataInternal(provider, network, selectedMarket) {
119
121
  }
120
122
  export function _getMorphoBlueMarketData(provider, network, selectedMarket) {
121
123
  return __awaiter(this, void 0, void 0, function* () {
122
- const marketInfo = yield getMorphoBlueMarketDataInternal(provider, network, selectedMarket);
123
- try {
124
- const rewards = yield getRewardsForMarket(selectedMarket.marketId, network);
125
- return addMorphoBlueRewardsToMarketInfo(marketInfo, rewards);
126
- }
127
- catch (error) {
128
- console.error(error);
129
- return addMorphoBlueRewardsToMarketInfo(marketInfo, { supplyApy: '0', borrowApy: '0' });
130
- }
124
+ const [marketInfo, rewards, merklOpportunities] = yield Promise.all([
125
+ getMorphoBlueMarketDataInternal(provider, network, selectedMarket),
126
+ getRewardsForMarket(selectedMarket.marketId, network).catch((error) => {
127
+ console.error(error);
128
+ return { supplyApy: '0', borrowApy: '0' };
129
+ }),
130
+ getMorphoBlueMerklOpportunities(),
131
+ ]);
132
+ return addMorphoBlueMerklOpportunitiesToMarketInfo(addMorphoBlueRewardsToMarketInfo(marketInfo, rewards), selectedMarket, network, merklOpportunities);
131
133
  });
132
134
  }
133
135
  export function _getMorphoBluePortfolioMarketData(provider, network, selectedMarket) {
@@ -0,0 +1,33 @@
1
+ import { MerklOpportunity, MorphoBlueMarketData, MorphoBlueMarketInfo, NetworkNumber } from '../types';
2
+ /**
3
+ * Merkl tags Morpho Blue market-level reward campaigns via the opportunity `type` field:
4
+ * - MORPHOSUPPLY → reward for supplying the market's loan asset
5
+ * - MORPHOCOLLATERAL → reward for depositing the market's collateral asset
6
+ * - MORPHOBORROW → reward for borrowing the market's loan asset
7
+ * (MORPHOVAULT/ERC20LOGPROCESSOR are MetaMorpho vault campaigns and MORPHOSUPPLY_SINGLETOKEN is
8
+ * token-wide rather than market-scoped, so none of them belong on per-market data.)
9
+ *
10
+ * A campaign identifies its market via `identifier` — the first 20 bytes of the 32-byte Morpho
11
+ * market id, formatted as a checksummed address. Whitelist-gated campaigns append a marker to that
12
+ * identifier (e.g. `0x54cf…c46WHITELIST_CAMPAIGN`); their rewards only accrue to whitelisted
13
+ * addresses, so they are skipped rather than advertised to every user.
14
+ */
15
+ export declare const MORPHO_BLUE_MERKL_OPPORTUNITY_TYPES: readonly ["MORPHOSUPPLY", "MORPHOCOLLATERAL", "MORPHOBORROW"];
16
+ /**
17
+ * Appends the given live Merkl campaigns for the market to its `assetsData` incentive arrays
18
+ * (collateral supply / loan supply / loan borrow). Existing Merkl incentives are replaced rather
19
+ * than duplicated, so re-applying on refreshed data is safe.
20
+ */
21
+ export declare const addMorphoBlueMerklOpportunitiesToMarketInfo: (marketInfo: MorphoBlueMarketInfo, selectedMarket: MorphoBlueMarketData, chainId: NetworkNumber, opportunities: MerklOpportunity[]) => MorphoBlueMarketInfo;
22
+ /**
23
+ * Fetches live market-scoped Morpho Blue campaigns for all chains. Never throws — on any failure
24
+ * it returns an empty list, so Merkl being down can't break market data. The query is narrowed by
25
+ * `type=` and capped at `items=100` because Merkl paginates at 20 items by default — an
26
+ * unfiltered protocol query would silently drop campaigns once Morpho has enough opportunities.
27
+ */
28
+ export declare const getMorphoBlueMerklOpportunities: () => Promise<MerklOpportunity[]>;
29
+ /**
30
+ * Fetches live Merkl campaigns and appends the ones for the market to its `assetsData` incentive
31
+ * arrays. Never throws — on any failure the market info is returned unchanged.
32
+ */
33
+ export declare const addMorphoBlueMerklRewardsToMarketInfo: (marketInfo: MorphoBlueMarketInfo, selectedMarket: MorphoBlueMarketData, network: NetworkNumber) => Promise<MorphoBlueMarketInfo>;
@@ -0,0 +1,95 @@
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 { aprToApy } from '../moneymarket';
11
+ import { LONGER_TIMEOUT } from '../services/utils';
12
+ import { IncentiveKind, OpportunityStatus, } from '../types';
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 const MORPHO_BLUE_MERKL_OPPORTUNITY_TYPES = ['MORPHOSUPPLY', 'MORPHOCOLLATERAL', 'MORPHOBORROW'];
27
+ const MERKL_DESCRIPTION_MARKER = 'through Merkl';
28
+ // '0x' + first 20 bytes of the market id
29
+ const MARKET_ID_PREFIX_LENGTH = 42;
30
+ const buildMerklIncentive = (opportunity) => {
31
+ var _a, _b, _c, _d, _e, _f;
32
+ const token = ((_d = (_c = (_b = (_a = opportunity.rewardsRecord) === null || _a === void 0 ? void 0 : _a.breakdowns) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.token) === null || _d === void 0 ? void 0 : _d.symbol) || ((_f = (_e = opportunity.tokens) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.symbol) || '';
33
+ return {
34
+ apy: aprToApy(opportunity.apr),
35
+ token,
36
+ incentiveKind: IncentiveKind.Reward,
37
+ description: `Eligible for ${token} rewards through Merkl.${opportunity.description ? `\n${opportunity.description}` : ''}`,
38
+ };
39
+ };
40
+ const withoutMerklIncentives = (incentives = []) => incentives
41
+ .filter(({ description }) => !(description === null || description === void 0 ? void 0 : description.includes(MERKL_DESCRIPTION_MARKER)));
42
+ /**
43
+ * Appends the given live Merkl campaigns for the market to its `assetsData` incentive arrays
44
+ * (collateral supply / loan supply / loan borrow). Existing Merkl incentives are replaced rather
45
+ * than duplicated, so re-applying on refreshed data is safe.
46
+ */
47
+ export const addMorphoBlueMerklOpportunitiesToMarketInfo = (marketInfo, selectedMarket, chainId, opportunities) => {
48
+ const marketIdPrefix = (selectedMarket.marketId || '').slice(0, MARKET_ID_PREFIX_LENGTH).toLowerCase();
49
+ const collateralAsset = marketInfo.assetsData[marketInfo.collateralToken];
50
+ const loanAsset = marketInfo.assetsData[marketInfo.loanToken];
51
+ if (marketIdPrefix.length !== MARKET_ID_PREFIX_LENGTH || !collateralAsset || !loanAsset)
52
+ return marketInfo;
53
+ const relevant = opportunities.filter((o) => {
54
+ var _a;
55
+ return o.chainId === chainId
56
+ && o.status === OpportunityStatus.LIVE
57
+ && MORPHO_BLUE_MERKL_OPPORTUNITY_TYPES.includes(o.type)
58
+ // a suffixed identifier marks a whitelist-gated campaign — skip those
59
+ && ((_a = o.identifier) === null || _a === void 0 ? void 0 : _a.length) === MARKET_ID_PREFIX_LENGTH
60
+ && o.identifier.toLowerCase() === marketIdPrefix;
61
+ });
62
+ if (!relevant.length)
63
+ return marketInfo;
64
+ const incentivesByType = (type) => relevant.filter((o) => o.type === type).map(buildMerklIncentive);
65
+ const collateralSupply = incentivesByType('MORPHOCOLLATERAL');
66
+ const loanSupply = incentivesByType('MORPHOSUPPLY');
67
+ const loanBorrow = incentivesByType('MORPHOBORROW');
68
+ return Object.assign(Object.assign({}, marketInfo), { assetsData: Object.assign(Object.assign({}, marketInfo.assetsData), { [marketInfo.collateralToken]: Object.assign(Object.assign({}, collateralAsset), { supplyIncentives: [...withoutMerklIncentives(collateralAsset.supplyIncentives), ...collateralSupply] }), [marketInfo.loanToken]: Object.assign(Object.assign({}, loanAsset), { supplyIncentives: [...withoutMerklIncentives(loanAsset.supplyIncentives), ...loanSupply], borrowIncentives: [...withoutMerklIncentives(loanAsset.borrowIncentives), ...loanBorrow] }) }) });
69
+ };
70
+ /**
71
+ * Fetches live market-scoped Morpho Blue campaigns for all chains. Never throws — on any failure
72
+ * it returns an empty list, so Merkl being down can't break market data. The query is narrowed by
73
+ * `type=` and capped at `items=100` because Merkl paginates at 20 items by default — an
74
+ * unfiltered protocol query would silently drop campaigns once Morpho has enough opportunities.
75
+ */
76
+ export const getMorphoBlueMerklOpportunities = () => __awaiter(void 0, void 0, void 0, function* () {
77
+ try {
78
+ const res = yield fetch(`https://fe.defisaver.com/api/merkl/opportunities?mainProtocolId=morpho&type=${MORPHO_BLUE_MERKL_OPPORTUNITY_TYPES.join(',')}&status=LIVE&items=100`, {
79
+ signal: AbortSignal.timeout(LONGER_TIMEOUT),
80
+ });
81
+ if (!res.ok)
82
+ throw new Error('Failed to fetch Morpho Blue Merkl campaigns');
83
+ const data = yield res.json();
84
+ return Array.isArray(data) ? data : [];
85
+ }
86
+ catch (e) {
87
+ console.error('Failed to fetch Morpho Blue Merkl campaigns', e);
88
+ return [];
89
+ }
90
+ });
91
+ /**
92
+ * Fetches live Merkl campaigns and appends the ones for the market to its `assetsData` incentive
93
+ * arrays. Never throws — on any failure the market info is returned unchanged.
94
+ */
95
+ export const addMorphoBlueMerklRewardsToMarketInfo = (marketInfo, selectedMarket, network) => __awaiter(void 0, void 0, void 0, function* () { return addMorphoBlueMerklOpportunitiesToMarketInfo(marketInfo, selectedMarket, network, yield getMorphoBlueMerklOpportunities()); });
@@ -19,6 +19,10 @@ export declare const isEligibleForAaveV3ArbitrumETHLSBorrow: (usedAssets: MMUsed
19
19
  isEligible: boolean;
20
20
  eligibleUSDAmount: string;
21
21
  };
22
+ export declare const isEligibleForCompoundV3PufEthWethBorrow: (usedAssets: MMUsedAssets) => {
23
+ isEligible: boolean;
24
+ eligibleUSDAmount: string;
25
+ };
22
26
  export declare const EligibilityMapping: {
23
27
  [key in IncentiveEligibilityId]: (usedAssets: MMUsedAssets, optionalData: any) => {
24
28
  isEligible: boolean;
@@ -60,10 +60,19 @@ export const isEligibleForAaveV3ArbitrumETHLSBorrow = (usedAssets) => {
60
60
  const ETHAmountBorrowed = ((_a = usedAssets.ETH) === null || _a === void 0 ? void 0 : _a.borrowedUsd) || '0';
61
61
  return { isEligible: true, eligibleUSDAmount: ETHAmountBorrowed };
62
62
  };
63
+ // "Borrow WETH with pufETH collateral" — rewards are calculated as min(collateral * 80% LTV, borrow amount)
64
+ export const isEligibleForCompoundV3PufEthWethBorrow = (usedAssets) => {
65
+ var _a, _b;
66
+ const collateralUsd = ((_a = usedAssets.pufETH) === null || _a === void 0 ? void 0 : _a.suppliedUsd) || '0';
67
+ const borrowedUsd = ((_b = usedAssets.ETH) === null || _b === void 0 ? void 0 : _b.borrowedUsd) || '0';
68
+ const eligibleUSDAmount = Dec.min(new Dec(collateralUsd).mul('0.8'), borrowedUsd).toString();
69
+ return { isEligible: new Dec(eligibleUSDAmount).gt(0), eligibleUSDAmount };
70
+ };
63
71
  export const EligibilityMapping = {
64
72
  [IncentiveEligibilityId.AaveV3EthenaLiquidLeverage]: isEligibleForEthenaUSDeRewards,
65
73
  [IncentiveEligibilityId.AaveV3ArbitrumEthSupply]: isEligibleForAaveV3ArbitrumEthSupply,
66
74
  [IncentiveEligibilityId.AaveV3ArbitrumETHLSBorrow]: isEligibleForAaveV3ArbitrumETHLSBorrow,
67
75
  [IncentiveEligibilityId.AaveV3EthenaLiquidLeveragePlasma]: isEligibleForEthenaUSDeRewards,
68
76
  [IncentiveEligibilityId.AaveV3EthenaLiquidLeveragePlasmaGHO]: isEligibleForEthenaGHORewards,
77
+ [IncentiveEligibilityId.CompoundV3PufEthWethBorrow]: isEligibleForCompoundV3PufEthWethBorrow,
69
78
  };
@@ -11,7 +11,8 @@ export declare enum IncentiveEligibilityId {
11
11
  AaveV3ArbitrumEthSupply = "0x5d16261c6715a653248269861bbacf68a9774cde",
12
12
  AaveV3ArbitrumETHLSBorrow = "0x0c84331e39d6658Cd6e6b9ba04736cC4c4734351",
13
13
  AaveV3EthenaLiquidLeveragePlasma = "0x2E8f1FA9c73F9d975B46BDFe40C92b6dDEFA3f31BORROW_BL",
14
- AaveV3EthenaLiquidLeveragePlasmaGHO = "2b2f6fefcd3df634"
14
+ AaveV3EthenaLiquidLeveragePlasmaGHO = "2b2f6fefcd3df634",
15
+ CompoundV3PufEthWethBorrow = "e6e49b79e83041f9"
15
16
  }
16
17
  export declare enum LeverageType {
17
18
  Long = "long",
@@ -15,6 +15,7 @@ export var IncentiveEligibilityId;
15
15
  IncentiveEligibilityId["AaveV3ArbitrumETHLSBorrow"] = "0x0c84331e39d6658Cd6e6b9ba04736cC4c4734351";
16
16
  IncentiveEligibilityId["AaveV3EthenaLiquidLeveragePlasma"] = "0x2E8f1FA9c73F9d975B46BDFe40C92b6dDEFA3f31BORROW_BL";
17
17
  IncentiveEligibilityId["AaveV3EthenaLiquidLeveragePlasmaGHO"] = "2b2f6fefcd3df634";
18
+ IncentiveEligibilityId["CompoundV3PufEthWethBorrow"] = "e6e49b79e83041f9";
18
19
  })(IncentiveEligibilityId || (IncentiveEligibilityId = {}));
19
20
  export var LeverageType;
20
21
  (function (LeverageType) {
@@ -84,6 +84,7 @@ export interface LiquityV2AggregatedTroveData {
84
84
  suppliedUsd: string;
85
85
  borrowedUsd: string;
86
86
  borrowLimitUsd: string;
87
+ liquidationLimitUsd: string;
87
88
  leftToBorrowUsd: string;
88
89
  netApy: string;
89
90
  incentiveUsd: string;
@@ -101,9 +102,11 @@ export interface LiquityV2TroveData {
101
102
  ratio: string;
102
103
  collRatio: string;
103
104
  liqRatio: string;
105
+ borrowLimitRatio: string;
104
106
  interestRate: string;
105
107
  leftToBorrowUsd: string;
106
108
  borrowLimitUsd: string;
109
+ liquidationLimitUsd: string;
107
110
  suppliedUsd: string;
108
111
  borrowedUsd: string;
109
112
  netApy: string;
@@ -76,6 +76,14 @@ export type MerkleRewardMap = Record<EthAddress, {
76
76
  export type AaveV4MerklScopedReward = {
77
77
  [side in IncentiveSide]?: IncentiveData;
78
78
  };
79
+ /**
80
+ * Fluid vault-scoped Merkl campaigns keyed by lowercase vault address — `supply` rewards apply to
81
+ * the vault's collateral side, `borrow` rewards to its debt side.
82
+ */
83
+ export type FluidMerklRewardMap = Record<string, {
84
+ supply: IncentiveData[];
85
+ borrow: IncentiveData[];
86
+ }>;
79
87
  /**
80
88
  * Aave V4 Merkl reward campaigns split by scope:
81
89
  * - `hub`: keyed by `${hubAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing via a hub
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defisaver/positions-sdk",
3
- "version": "2.1.141-dev",
3
+ "version": "2.1.142-dev",
4
4
  "description": "",
5
5
  "main": "./cjs/index.js",
6
6
  "module": "./esm/index.js",
@@ -31,7 +31,9 @@ export const formatAaveAsset = (_symbol: string) => {
31
31
 
32
32
  export const getMerkleCampaigns = async (chainId: NetworkNumber): Promise<MerkleRewardMap> => {
33
33
  try {
34
- const res = await fetch('https://fe.defisaver.com/api/merkl/opportunities?mainProtocolId=aave', {
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', {
35
37
  signal: AbortSignal.timeout(LONGER_TIMEOUT),
36
38
  });
37
39
  if (!res.ok) throw new Error('Failed to fetch Merkle campaigns');
@@ -40,7 +42,8 @@ export const getMerkleCampaigns = async (chainId: NetworkNumber): Promise<Merkle
40
42
  .filter((o: MerklOpportunity) => o.chainId === chainId)
41
43
  .filter((o: MerklOpportunity) => o.status === OpportunityStatus.LIVE);
42
44
  return relevantOpportunities.reduce((acc, opportunity) => {
43
- const rewardToken = opportunity.rewardsRecord.breakdowns[0].token;
45
+ const rewardToken = opportunity.rewardsRecord?.breakdowns?.[0]?.token;
46
+ if (!rewardToken) return acc;
44
47
  const description = `Eligible for ${formatAaveAsset(rewardToken.symbol)} rewards through Merkl. ${opportunity.description ? `\n${opportunity.description}` : ''}`;
45
48
  if (opportunity.action === OpportunityAction.LEND && opportunity.explorerAddress) {
46
49
  const supplyAToken = opportunity.explorerAddress?.toLowerCase() as EthAddress;
@@ -67,7 +67,7 @@ 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', {
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
71
  signal: AbortSignal.timeout(LONGER_TIMEOUT),
72
72
  });
73
73
  if (!res.ok) throw new Error('Failed to fetch Aave V4 Merkle campaigns');
@@ -24,6 +24,9 @@ import {
24
24
  getEthPrice, getCompPrice, getUSDCPrice, getWstETHPrice,
25
25
  } from '../services/priceService';
26
26
  import { getViemProvider, setViemBlockNumber } from '../services/viem';
27
+ import { attachCompoundV3MerklIncentives, getCompoundV3MerklOpportunities } from './merkl';
28
+
29
+ export { getCompoundV3MerklOpportunities, attachCompoundV3MerklIncentives } from './merkl';
27
30
 
28
31
  const getSupportedAssetsAddressesForMarket = (selectedMarket: CompoundMarketData, network: NetworkNumber) => selectedMarket.collAssets.map(asset => getAssetInfo(ethToWeth(asset), network)).map(addr => addr.address.toLowerCase());
29
32
 
@@ -41,12 +44,13 @@ const getBaseAssetPriceFunction = (asset: string) => {
41
44
  export const _getCompoundV3MarketsData = async (provider: Client, network: NetworkNumber, selectedMarket: CompoundMarketData, defaultProvider: Client): Promise<CompoundV3MarketsData> => {
42
45
  const contract = CompV3ViewContractViem(provider, network);
43
46
 
44
- const [baseAssetPrice, compPrice, baseTokenInfo, collInfos, govInfo] = await Promise.all([
47
+ const [baseAssetPrice, compPrice, baseTokenInfo, collInfos, govInfo, merklOpportunities] = await Promise.all([
45
48
  getBaseAssetPriceFunction(selectedMarket.baseAsset)(defaultProvider),
46
49
  getCompPrice(defaultProvider),
47
50
  contract.read.getFullBaseTokenInfo([selectedMarket.baseMarketAddress]),
48
51
  contract.read.getFullCollInfos([selectedMarket.baseMarketAddress]),
49
52
  contract.read.getGovernanceInfoFull([selectedMarket.baseMarketAddress]),
53
+ getCompoundV3MerklOpportunities(),
50
54
  ]);
51
55
 
52
56
  const { isSupplyPaused, isWithdrawPaused } = govInfo;
@@ -72,6 +76,7 @@ export const _getCompoundV3MarketsData = async (provider: Client, network: Netwo
72
76
  const payload: CompoundV3AssetsData = {};
73
77
 
74
78
  const baseObj = { ...base, ...(await getIncentiveApys(base, compPrice)) };
79
+ attachCompoundV3MerklIncentives(baseObj, colls, selectedMarket.baseMarketAddress, network, merklOpportunities);
75
80
  const allAssets = [baseObj, ...colls];
76
81
 
77
82
  allAssets
@@ -0,0 +1,77 @@
1
+ import { getAssetInfoByAddress } from '@defisaver/tokens';
2
+ import { aprToApy } from '../moneymarket';
3
+ import { compareAddresses, LONGER_TIMEOUT, wethToEth } from '../services/utils';
4
+ import {
5
+ CompoundV3AssetData,
6
+ IncentiveData,
7
+ IncentiveEligibilityId,
8
+ IncentiveKind,
9
+ MerklOpportunity,
10
+ NetworkNumber,
11
+ OpportunityAction,
12
+ OpportunityStatus,
13
+ } from '../types';
14
+
15
+ /**
16
+ * Merkl tags Compound V3 reward campaigns with COMPOUND_V3_* opportunity types (e.g.
17
+ * COMPOUND_V3_BORROW_FROM_COLLATERAL) and identifies the comet via `explorerAddress`. Campaign
18
+ * identifiers are opaque hashes, passed through as `eligibilityId` so conditional campaigns —
19
+ * like borrow-from-collateral ones, whose reward is min(collateral * LTV, borrow amount) — can
20
+ * be gated and sized by a curated check in EligibilityMapping; unknown ids apply
21
+ * unconditionally, mirroring Aave V3.
22
+ */
23
+ export const getCompoundV3MerklOpportunities = async (): Promise<MerklOpportunity[]> => {
24
+ 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),
27
+ });
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
+ } catch (e) {
32
+ console.error('Failed to fetch Compound V3 Merkl campaigns', e);
33
+ return [];
34
+ }
35
+ };
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
+ eligibilityId: opportunity.identifier as IncentiveEligibilityId,
45
+ };
46
+ };
47
+
48
+ /**
49
+ * Appends the comet's Merkl campaigns to its assets' incentive arrays (mutating them, like the
50
+ * staking incentives are attached). BORROW campaigns land on the base asset — the only borrowable
51
+ * one — while LEND campaigns land on the asset the campaign's first token resolves to.
52
+ */
53
+ export const attachCompoundV3MerklIncentives = (
54
+ baseAsset: CompoundV3AssetData,
55
+ collAssets: CompoundV3AssetData[],
56
+ baseMarketAddress: string,
57
+ chainId: NetworkNumber,
58
+ opportunities: MerklOpportunity[],
59
+ ): void => {
60
+ opportunities
61
+ .filter((o) => o.chainId === chainId
62
+ && o.status === OpportunityStatus.LIVE
63
+ && typeof o.type === 'string' && o.type.startsWith('COMPOUND_V3')
64
+ && o.explorerAddress && compareAddresses(o.explorerAddress, baseMarketAddress))
65
+ .forEach((o) => {
66
+ const incentive = buildMerklIncentive(o);
67
+ if (o.action === OpportunityAction.BORROW) {
68
+ baseAsset.borrowIncentives.push(incentive);
69
+ } else if (o.action === OpportunityAction.LEND) {
70
+ const campaignTokenAddress = o.tokens?.[0]?.address;
71
+ if (!campaignTokenAddress) return;
72
+ const campaignSymbol = wethToEth(getAssetInfoByAddress(campaignTokenAddress, chainId).symbol);
73
+ const target = [baseAsset, ...collAssets].find((asset) => asset.symbol === campaignSymbol);
74
+ target?.supplyIncentives.push(incentive);
75
+ }
76
+ });
77
+ };
@@ -66,6 +66,14 @@ import {
66
66
  } from '../services/priceService';
67
67
  import { getStakingApy, STAKING_ASSETS } from '../staking';
68
68
  import { getViemProvider } from '../services/viem';
69
+ import { attachFluidMerklIncentives, getFluidMerklCampaigns } from './merkl';
70
+
71
+ export {
72
+ FLUID_MERKL_OPPORTUNITY_TYPES,
73
+ buildFluidMerklRewardMap,
74
+ getFluidMerklCampaigns,
75
+ attachFluidMerklIncentives,
76
+ } from './merkl';
69
77
 
70
78
  export const EMPTY_USED_ASSET = {
71
79
  isSupplied: false,
@@ -1442,9 +1450,13 @@ const parseUserData = (userPositionData: FluidUserPositionStructOutputStruct, va
1442
1450
  export const _getFluidMarketData = async (provider: PublicClient, network: NetworkNumber, market: FluidMarketInfo) => {
1443
1451
  const view = FluidViewContractViem(provider, network);
1444
1452
 
1445
- const data = await view.read.getVaultData([market.marketAddress]);
1453
+ const [data, merklCampaigns] = await Promise.all([
1454
+ view.read.getVaultData([market.marketAddress]),
1455
+ getFluidMerklCampaigns(network),
1456
+ ]);
1446
1457
 
1447
- return parseMarketData(provider, data, network);
1458
+ const marketData = await parseMarketData(provider, data, network);
1459
+ return marketData ? attachFluidMerklIncentives(marketData, merklCampaigns) : marketData;
1448
1460
  };
1449
1461
 
1450
1462
  export const getFluidMarketData = async (
@@ -1500,11 +1512,15 @@ export const getFluidPosition = async (
1500
1512
 
1501
1513
  export const _getFluidPositionWithMarket = async (provider: PublicClient, network: NetworkNumber, vaultId: string) => {
1502
1514
  const view = FluidViewContractViem(provider, network);
1503
- const data = await view.read.getPositionByNftId([BigInt(vaultId)]);
1504
- const marketData = await parseMarketData(provider, data[1], network);
1505
- if (!marketData) {
1515
+ const [data, merklCampaigns] = await Promise.all([
1516
+ view.read.getPositionByNftId([BigInt(vaultId)]),
1517
+ getFluidMerklCampaigns(network),
1518
+ ]);
1519
+ const parsedMarketData = await parseMarketData(provider, data[1], network);
1520
+ if (!parsedMarketData) {
1506
1521
  return;
1507
1522
  }
1523
+ const marketData = attachFluidMerklIncentives(parsedMarketData, merklCampaigns);
1508
1524
  const userData = parseUserData(data[0], marketData);
1509
1525
 
1510
1526
  return {
@@ -1522,8 +1538,14 @@ export const getFluidPositionWithMarket = async (
1522
1538
  export const _getAllFluidMarketDataChunked = async (network: NetworkNumber, provider: PublicClient) => {
1523
1539
  const versions = getFluidVersionsDataForNetwork(network);
1524
1540
  const view = FluidViewContractViem(provider, network);
1525
- const data = await Promise.all(versions.map((version) => view.read.getVaultData([version.marketAddress])));
1526
- return Promise.all(data.map(async (item, i) => parseMarketData(provider, item, network)));
1541
+ const [data, merklCampaigns] = await Promise.all([
1542
+ Promise.all(versions.map((version) => view.read.getVaultData([version.marketAddress]))),
1543
+ getFluidMerklCampaigns(network),
1544
+ ]);
1545
+ return Promise.all(data.map(async (item) => {
1546
+ const marketData = await parseMarketData(provider, item, network);
1547
+ return marketData ? attachFluidMerklIncentives(marketData, merklCampaigns) : marketData;
1548
+ }));
1527
1549
  };
1528
1550
 
1529
1551
  export const getAllFluidMarketDataChunked = async (
@@ -0,0 +1,97 @@
1
+ import { aprToApy } from '../moneymarket';
2
+ import { LONGER_TIMEOUT } from '../services/utils';
3
+ import {
4
+ FluidMarketData,
5
+ FluidMerklRewardMap,
6
+ IncentiveData,
7
+ IncentiveEligibilityId,
8
+ IncentiveKind,
9
+ MerklOpportunity,
10
+ NetworkNumber,
11
+ OpportunityStatus,
12
+ } from '../types';
13
+
14
+ /**
15
+ * Merkl tags Fluid vault-scoped reward campaigns via the opportunity `type` field:
16
+ * - FLUIDVAULT_COLLATERAL → reward for supplying collateral to the vault
17
+ * - FLUIDVAULT_BORROW → reward for borrowing from the vault
18
+ * (ERC20LOGPROCESSOR campaigns target fToken earn positions rather than vaults, so they don't
19
+ * belong on vault market data.)
20
+ *
21
+ * A campaign identifies its vault via `identifier` — the vault address. Conditional campaigns
22
+ * append a marker to it (e.g. `0x5668…eeaBORROW_BL`, excluding holders who borrow the reward
23
+ * asset elsewhere); those are still attached, with the full identifier passed through as
24
+ * `eligibilityId` so a curated check in EligibilityMapping can gate them in net APY — unknown
25
+ * ids apply unconditionally, mirroring Aave V3. Whitelist-gated campaigns only accrue to
26
+ * whitelisted addresses, so they are skipped rather than advertised to every user.
27
+ */
28
+ export const FLUID_MERKL_OPPORTUNITY_TYPES = ['FLUIDVAULT_COLLATERAL', 'FLUIDVAULT_BORROW'] as const;
29
+
30
+ // '0x' + 20-byte vault address; conditional campaigns append a marker after it
31
+ const VAULT_ADDRESS_LENGTH = 42;
32
+
33
+ const buildMerklIncentive = (opportunity: MerklOpportunity): IncentiveData => {
34
+ const token = opportunity.rewardsRecord?.breakdowns?.[0]?.token?.symbol || opportunity.tokens?.[0]?.symbol || '';
35
+ return {
36
+ apy: aprToApy(opportunity.apr),
37
+ token,
38
+ incentiveKind: IncentiveKind.Reward,
39
+ description: `Eligible for ${token} rewards through Merkl.${opportunity.description ? `\n${opportunity.description}` : ''}`,
40
+ eligibilityId: opportunity.identifier as IncentiveEligibilityId,
41
+ };
42
+ };
43
+
44
+ export const buildFluidMerklRewardMap = (opportunities: MerklOpportunity[], chainId: NetworkNumber): FluidMerklRewardMap => {
45
+ const result: FluidMerklRewardMap = {};
46
+
47
+ opportunities
48
+ .filter((o) => o.chainId === chainId)
49
+ .filter((o) => o.status === OpportunityStatus.LIVE)
50
+ .filter((o) => (FLUID_MERKL_OPPORTUNITY_TYPES as readonly string[]).includes(o.type))
51
+ .filter((o) => !o.identifier?.includes('WHITELIST'))
52
+ .forEach((o) => {
53
+ const vaultAddress = o.identifier?.slice(0, VAULT_ADDRESS_LENGTH).toLowerCase();
54
+ if (!vaultAddress || vaultAddress.length !== VAULT_ADDRESS_LENGTH) return;
55
+ if (!result[vaultAddress]) result[vaultAddress] = { supply: [], borrow: [] };
56
+ result[vaultAddress][o.type === 'FLUIDVAULT_BORROW' ? 'borrow' : 'supply'].push(buildMerklIncentive(o));
57
+ });
58
+
59
+ return result;
60
+ };
61
+
62
+ /**
63
+ * 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.
66
+ */
67
+ export const getFluidMerklCampaigns = async (chainId: NetworkNumber): Promise<FluidMerklRewardMap> => {
68
+ 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),
71
+ });
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);
75
+ } catch (e) {
76
+ console.error('Failed to fetch Fluid Merkl campaigns', e);
77
+ return {};
78
+ }
79
+ };
80
+
81
+ /**
82
+ * Appends the vault's Merkl campaigns to its assets' incentive arrays — supply rewards on every
83
+ * suppliable asset and borrow rewards on every borrowable one. Smart collateral/debt vaults hold
84
+ * the position across both pair tokens, so the pro-rata reward APY applies to each side's value.
85
+ */
86
+ export const attachFluidMerklIncentives = (marketData: FluidMarketData, campaigns: FluidMerklRewardMap): FluidMarketData => {
87
+ const vaultCampaigns = campaigns[marketData.marketData.marketAddress?.toLowerCase()];
88
+ if (!vaultCampaigns || (!vaultCampaigns.supply.length && !vaultCampaigns.borrow.length)) return marketData;
89
+
90
+ const assetsData = Object.fromEntries(Object.entries(marketData.assetsData).map(([symbol, asset]) => [symbol, {
91
+ ...asset,
92
+ supplyIncentives: asset.canBeSupplied ? [...asset.supplyIncentives, ...vaultCampaigns.supply] : asset.supplyIncentives,
93
+ borrowIncentives: asset.canBeBorrowed ? [...asset.borrowIncentives, ...vaultCampaigns.borrow] : asset.borrowIncentives,
94
+ }]));
95
+
96
+ return { ...marketData, assetsData };
97
+ };
@@ -52,16 +52,19 @@ export const getLiquityV2AggregatedPositionData = ({
52
52
  assetsData,
53
53
  minCollRatio,
54
54
  interestRate,
55
+ liqRatio,
55
56
  }: {
56
57
  usedAssets: LiquityV2UsedAssets
57
58
  assetsData: LiquityV2AssetsData
58
59
  minCollRatio: string
59
60
  interestRate: string
61
+ liqRatio?: string // liquidation threshold (MCR), when different from minCollRatio (MCR + BCR for troves in a batch)
60
62
  }): LiquityV2AggregatedTroveData => {
61
63
  const payload = {} as LiquityV2AggregatedTroveData;
62
64
  payload.suppliedUsd = getAssetsTotal(usedAssets, (usedAsset: LiquityV2UsedAsset) => usedAsset, ({ suppliedUsd }: { suppliedUsd: string }) => suppliedUsd);
63
65
  payload.borrowedUsd = getAssetsTotal(usedAssets, (usedAsset: LiquityV2UsedAsset) => usedAsset, ({ borrowedUsd }: { borrowedUsd: string }) => borrowedUsd);
64
66
  payload.borrowLimitUsd = new Dec(payload.suppliedUsd).div(minCollRatio).mul(100).toString();
67
+ payload.liquidationLimitUsd = new Dec(payload.suppliedUsd).div(liqRatio ?? minCollRatio).mul(100).toString();
65
68
  const leftToBorrowUsd = new Dec(payload.borrowLimitUsd).sub(payload.borrowedUsd);
66
69
  payload.leftToBorrowUsd = leftToBorrowUsd.lte('0') ? '0' : leftToBorrowUsd.toString();
67
70
  payload.ratio = (+payload.suppliedUsd && +payload.borrowedUsd) ? new Dec(payload.borrowLimitUsd).div(payload.borrowedUsd).mul(100).toString() : '0';
@@ -77,7 +80,7 @@ export const getLiquityV2AggregatedPositionData = ({
77
80
  payload.liquidationPrice = '';
78
81
  if (leveragedType !== '') {
79
82
  const assetPrice = assetsData[leveragedAsset].price;
80
- payload.liquidationPrice = calcLeverageLiqPrice(leveragedType, assetPrice, payload.borrowedUsd, payload.borrowLimitUsd);
83
+ payload.liquidationPrice = calcLeverageLiqPrice(leveragedType, assetPrice, payload.borrowedUsd, payload.liquidationLimitUsd);
81
84
  }
82
85
  payload.exposure = getExposure(payload.borrowedUsd, payload.suppliedUsd);
83
86