@defisaver/positions-sdk 2.1.138 → 2.1.140-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.
@@ -1,4 +1,5 @@
1
- import { AaveV4MerklRewardMap, AaveV4ReserveAssetData, NetworkNumber } from '../types';
1
+ import { AaveV4MerklRewardMap, AaveV4ReserveAssetData, MerklOpportunity, NetworkNumber } from '../types';
2
+ export declare const buildAaveV4MerklRewardMap: (opportunities: MerklOpportunity[], chainId: NetworkNumber) => AaveV4MerklRewardMap;
2
3
  export declare const getAaveV4MerkleCampaigns: (chainId: NetworkNumber) => Promise<AaveV4MerklRewardMap>;
3
4
  /**
4
5
  * Returns a copy of the asset with scope-specific incentive arrays pre-combined with the asset's
@@ -9,17 +9,20 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.attachAaveV4MerklIncentives = exports.getAaveV4MerkleCampaigns = void 0;
12
+ exports.attachAaveV4MerklIncentives = exports.getAaveV4MerkleCampaigns = exports.buildAaveV4MerklRewardMap = void 0;
13
13
  const moneymarket_1 = require("../moneymarket");
14
14
  const utils_1 = require("../services/utils");
15
15
  const types_1 = require("../types");
16
16
  /**
17
17
  * Merkl tags Aave V4 reward campaigns by scope via the `type` field:
18
- * - AAVE_V4_HUB_SUPPLY / AAVE_V4_HUB_BORROW → reward tied to a hub (matched per underlying token)
18
+ * - AAVE_V4_HUB_SUPPLY / AAVE_V4_HUB_BORROW → reward tied to a hub (matched per hub contract + underlying)
19
19
  * - AAVE_V4_SPOKE_SUPPLY / AAVE_V4_SPOKE_BORROW → reward tied to a spoke (matched per spoke contract + underlying)
20
- * Hub campaigns identify the underlying via `tokens[0]`; spoke campaigns identify the spoke via `explorerAddress`.
20
+ * Campaigns identify the underlying via `tokens[0]` and the scoping contract (the hub or spoke) via
21
+ * `explorerAddress`. The same underlying exists on several hubs (e.g. USDC on Prime and Paxos), so a
22
+ * hub campaign matched by underlying alone would leak onto every hub's reserves — a campaign whose
23
+ * `explorerAddress` isn't a contract any fetched reserve points to simply never matches.
21
24
  */
22
- const spokeKey = (spokeAddress, underlying) => `${spokeAddress.toLowerCase()}_${underlying.toLowerCase()}`;
25
+ const scopeKey = (scopeAddress, underlying) => `${scopeAddress.toLowerCase()}_${underlying.toLowerCase()}`;
23
26
  const buildIncentive = (opportunity) => {
24
27
  var _a, _b, _c, _d, _e;
25
28
  const rewardToken = (_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;
@@ -31,8 +34,38 @@ const buildIncentive = (opportunity) => {
31
34
  description: `Eligible for ${token} rewards through Merkl.${opportunity.description ? `\n${opportunity.description}` : ''}`,
32
35
  };
33
36
  };
34
- const getAaveV4MerkleCampaigns = (chainId) => __awaiter(void 0, void 0, void 0, function* () {
37
+ const buildAaveV4MerklRewardMap = (opportunities, chainId) => {
35
38
  const result = { hub: {}, spoke: {} };
39
+ opportunities
40
+ .filter((o) => o.chainId === chainId)
41
+ .filter((o) => o.status === types_1.OpportunityStatus.LIVE)
42
+ .filter((o) => typeof o.type === 'string' && o.type.startsWith('AAVE_V4_'))
43
+ .forEach((o) => {
44
+ var _a, _b, _c, _d;
45
+ const underlying = (_c = (_b = (_a = o.tokens) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.address) === null || _c === void 0 ? void 0 : _c.toLowerCase();
46
+ if (!underlying)
47
+ return;
48
+ const scopeAddress = (_d = o.explorerAddress) === null || _d === void 0 ? void 0 : _d.toLowerCase();
49
+ if (!scopeAddress)
50
+ return;
51
+ const side = o.action === types_1.OpportunityAction.BORROW ? types_1.IncentiveSide.Borrow : types_1.IncentiveSide.Supply;
52
+ const incentive = buildIncentive(o);
53
+ const key = scopeKey(scopeAddress, underlying);
54
+ if (o.type.includes('HUB')) {
55
+ if (!result.hub[key])
56
+ result.hub[key] = {};
57
+ result.hub[key][side] = incentive;
58
+ }
59
+ else if (o.type.includes('SPOKE')) {
60
+ if (!result.spoke[key])
61
+ result.spoke[key] = {};
62
+ result.spoke[key][side] = incentive;
63
+ }
64
+ });
65
+ return result;
66
+ };
67
+ exports.buildAaveV4MerklRewardMap = buildAaveV4MerklRewardMap;
68
+ const getAaveV4MerkleCampaigns = (chainId) => __awaiter(void 0, void 0, void 0, function* () {
36
69
  try {
37
70
  const res = yield 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', {
38
71
  signal: AbortSignal.timeout(utils_1.LONGER_TIMEOUT),
@@ -40,37 +73,11 @@ const getAaveV4MerkleCampaigns = (chainId) => __awaiter(void 0, void 0, void 0,
40
73
  if (!res.ok)
41
74
  throw new Error('Failed to fetch Aave V4 Merkle campaigns');
42
75
  const opportunities = yield res.json();
43
- opportunities
44
- .filter((o) => o.chainId === chainId)
45
- .filter((o) => o.status === types_1.OpportunityStatus.LIVE)
46
- .filter((o) => typeof o.type === 'string' && o.type.startsWith('AAVE_V4_'))
47
- .forEach((o) => {
48
- var _a, _b, _c, _d;
49
- const underlying = (_c = (_b = (_a = o.tokens) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.address) === null || _c === void 0 ? void 0 : _c.toLowerCase();
50
- if (!underlying)
51
- return;
52
- const side = o.action === types_1.OpportunityAction.BORROW ? 'borrow' : 'supply';
53
- const incentive = buildIncentive(o);
54
- if (o.type.includes('HUB')) {
55
- if (!result.hub[underlying])
56
- result.hub[underlying] = {};
57
- result.hub[underlying][side] = incentive;
58
- }
59
- else if (o.type.includes('SPOKE')) {
60
- const spokeAddress = (_d = o.explorerAddress) === null || _d === void 0 ? void 0 : _d.toLowerCase();
61
- if (!spokeAddress)
62
- return;
63
- const key = spokeKey(spokeAddress, underlying);
64
- if (!result.spoke[key])
65
- result.spoke[key] = {};
66
- result.spoke[key][side] = incentive;
67
- }
68
- });
69
- return result;
76
+ return (0, exports.buildAaveV4MerklRewardMap)(opportunities, chainId);
70
77
  }
71
78
  catch (e) {
72
79
  console.error('Failed to fetch Aave V4 Merkle campaigns', e);
73
- return result;
80
+ return { hub: {}, spoke: {} };
74
81
  }
75
82
  });
76
83
  exports.getAaveV4MerkleCampaigns = getAaveV4MerkleCampaigns;
@@ -83,8 +90,8 @@ const attachAaveV4MerklIncentives = (asset, spokeAddress, campaigns) => {
83
90
  const underlying = (_a = asset.underlying) === null || _a === void 0 ? void 0 : _a.toLowerCase();
84
91
  const baseSupply = asset.supplyIncentives || [];
85
92
  const baseBorrow = asset.borrowIncentives || [];
86
- const spokeScoped = (spokeAddress && underlying) ? campaigns.spoke[spokeKey(spokeAddress, underlying)] : undefined;
87
- const hubScoped = underlying ? campaigns.hub[underlying] : undefined;
93
+ const spokeScoped = (spokeAddress && underlying) ? campaigns.spoke[scopeKey(spokeAddress, underlying)] : undefined;
94
+ const hubScoped = (asset.hub && underlying) ? campaigns.hub[scopeKey(asset.hub, underlying)] : undefined;
88
95
  return Object.assign(Object.assign({}, asset), { spokeSupplyIncentives: (spokeScoped === null || spokeScoped === void 0 ? void 0 : spokeScoped.supply) ? [...baseSupply, spokeScoped.supply] : baseSupply, spokeBorrowIncentives: (spokeScoped === null || spokeScoped === void 0 ? void 0 : spokeScoped.borrow) ? [...baseBorrow, spokeScoped.borrow] : baseBorrow, hubSupplyIncentives: (hubScoped === null || hubScoped === void 0 ? void 0 : hubScoped.supply) ? [...baseSupply, hubScoped.supply] : baseSupply, hubBorrowIncentives: (hubScoped === null || hubScoped === void 0 ? void 0 : hubScoped.borrow) ? [...baseBorrow, hubScoped.borrow] : baseBorrow });
89
96
  };
90
97
  exports.attachAaveV4MerklIncentives = attachAaveV4MerklIncentives;
@@ -1,5 +1,11 @@
1
- import { AaveV4AggregatedPositionData, AaveV4AssetsData, AaveV4ReserveAssetData, AaveV4SpokeInfo, AaveV4UsedReserveAsset, AaveV4UsedReserveAssets, EthereumProvider, LeverageType, NetworkNumber } from '../../types';
1
+ import { AaveV4AggregatedPositionData, AaveV4AssetsData, AaveV4ReserveAssetData, AaveV4SpokeInfo, AaveV4UsedReserveAsset, AaveV4UsedReserveAssets, EthereumProvider, IncentiveData, IncentiveSide, LeverageType, NetworkNumber } from '../../types';
2
2
  export declare const calcUserRiskPremiumBps: (usedAssets: AaveV4UsedReserveAssets, assetsData: AaveV4AssetsData) => number;
3
+ /**
4
+ * The incentives that actually accrue to a position on this reserve for the given side: the
5
+ * intrinsic (staking) incentives plus the single applicable Merkl reward. Display surfaces should
6
+ * use this rather than picking a scoped list directly, so badges always match the net APY math.
7
+ */
8
+ export declare const getAaveV4ApplicableIncentives: (assetData: AaveV4ReserveAssetData, side: IncentiveSide) => IncentiveData[];
3
9
  export declare const calculateNetApyAaveV4: ({ usedAssets, assetsData, }: {
4
10
  usedAssets: AaveV4UsedReserveAssets;
5
11
  assetsData: AaveV4AssetsData;
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.getAaveV4ApyAfterValuesEstimation = exports.aaveV4GetAggregatedPositionData = exports.isLeveragedPosAaveV4 = exports.aaveV4GetCollateralFactor = exports.calculateNetApyAaveV4 = exports.calcUserRiskPremiumBps = void 0;
15
+ exports.getAaveV4ApyAfterValuesEstimation = exports.aaveV4GetAggregatedPositionData = exports.isLeveragedPosAaveV4 = exports.aaveV4GetCollateralFactor = exports.calculateNetApyAaveV4 = exports.getAaveV4ApplicableIncentives = exports.calcUserRiskPremiumBps = void 0;
16
16
  const decimal_js_1 = __importDefault(require("decimal.js"));
17
17
  const tokens_1 = require("@defisaver/tokens");
18
18
  const moneymarket_1 = require("../../moneymarket");
@@ -76,14 +76,26 @@ const calcUserRiskPremiumBps = (usedAssets, assetsData) => {
76
76
  exports.calcUserRiskPremiumBps = calcUserRiskPremiumBps;
77
77
  /**
78
78
  * `spokeXIncentives`/`hubXIncentives` are each the intrinsic `base` list with at most one Merkl
79
- * reward appended (see attachAaveV4MerklIncentives). Both scopes can apply to the same position at
80
- * once (spoke-specific + hub-wide), so the full applicable set is base + whatever each scope appended.
79
+ * reward appended (see attachAaveV4MerklIncentives). Merkl regularly publishes the same reward
80
+ * stream at both scopes (a spoke campaign and a hub campaign covering the same borrows, e.g. USDC
81
+ * borrowed from the Prime Hub via the Bluechip Spoke), so the scopes must never be summed — the
82
+ * more specific spoke reward wins and the hub reward only applies when no spoke campaign exists,
83
+ * which is also what every per-asset APY badge (and Aave's own UI) shows.
81
84
  */
82
- const mergeScopedIncentives = (base = [], spokeScoped, hubScoped) => [
83
- ...base,
84
- ...(spokeScoped ? spokeScoped.slice(base.length) : []),
85
- ...(hubScoped ? hubScoped.slice(base.length) : []),
86
- ];
85
+ const mergeScopedIncentives = (base = [], spokeScoped, hubScoped) => {
86
+ const spokeExtras = spokeScoped ? spokeScoped.slice(base.length) : [];
87
+ const hubExtras = hubScoped ? hubScoped.slice(base.length) : [];
88
+ return [...base, ...(spokeExtras.length ? spokeExtras : hubExtras)];
89
+ };
90
+ /**
91
+ * The incentives that actually accrue to a position on this reserve for the given side: the
92
+ * intrinsic (staking) incentives plus the single applicable Merkl reward. Display surfaces should
93
+ * use this rather than picking a scoped list directly, so badges always match the net APY math.
94
+ */
95
+ const getAaveV4ApplicableIncentives = (assetData, side) => (side === types_1.IncentiveSide.Supply
96
+ ? mergeScopedIncentives(assetData.supplyIncentives, assetData.spokeSupplyIncentives, assetData.hubSupplyIncentives)
97
+ : mergeScopedIncentives(assetData.borrowIncentives, assetData.spokeBorrowIncentives, assetData.hubBorrowIncentives));
98
+ exports.getAaveV4ApplicableIncentives = getAaveV4ApplicableIncentives;
87
99
  const calculateNetApyAaveV4 = ({ usedAssets, assetsData, }) => {
88
100
  const riskPremiumBps = (0, exports.calcUserRiskPremiumBps)(usedAssets, assetsData);
89
101
  const riskPremiumFraction = new decimal_js_1.default(riskPremiumBps).div(10000);
@@ -97,7 +109,7 @@ const calculateNetApyAaveV4 = ({ usedAssets, assetsData, }) => {
97
109
  acc.suppliedUsd = new decimal_js_1.default(acc.suppliedUsd).add(amount).toString();
98
110
  const supplyInterest = (0, staking_1.calculateInterestEarned)(amount, assetData.supplyRate, 'year', true);
99
111
  acc.supplyInterest = new decimal_js_1.default(acc.supplyInterest).add(supplyInterest.toString()).toString();
100
- const supplyIncentives = mergeScopedIncentives(assetData.supplyIncentives, assetData.spokeSupplyIncentives, assetData.hubSupplyIncentives);
112
+ const supplyIncentives = (0, exports.getAaveV4ApplicableIncentives)(assetData, types_1.IncentiveSide.Supply);
101
113
  for (const supplyIncentive of supplyIncentives) {
102
114
  const incentiveInterest = (0, staking_1.calculateInterestEarned)(amount, supplyIncentive.apy, 'year', true);
103
115
  acc.incentiveUsd = new decimal_js_1.default(acc.incentiveUsd).add(incentiveInterest).toString();
@@ -113,7 +125,7 @@ const calculateNetApyAaveV4 = ({ usedAssets, assetsData, }) => {
113
125
  const userBorrowRate = (0, moneymarket_1.aprToApy)(userBorrowApr.toString());
114
126
  const borrowInterest = (0, staking_1.calculateInterestEarned)(amount, userBorrowRate, 'year', true);
115
127
  acc.borrowInterest = new decimal_js_1.default(acc.borrowInterest).sub(borrowInterest.toString()).toString();
116
- const borrowIncentives = mergeScopedIncentives(assetData.borrowIncentives, assetData.spokeBorrowIncentives, assetData.hubBorrowIncentives);
128
+ const borrowIncentives = (0, exports.getAaveV4ApplicableIncentives)(assetData, types_1.IncentiveSide.Borrow);
117
129
  for (const borrowIncentive of borrowIncentives) {
118
130
  const incentiveInterest = (0, staking_1.calculateInterestEarned)(amount, borrowIncentive.apy, 'year', true);
119
131
  acc.incentiveUsd = new decimal_js_1.default(acc.incentiveUsd).add(incentiveInterest).toString();
@@ -4,9 +4,10 @@ export declare const calculateNetApyLiquityV2: (usedAssets: LiquityV2UsedAssets,
4
4
  totalInterestUsd: string;
5
5
  incentiveUsd: string;
6
6
  };
7
- export declare const getLiquityV2AggregatedPositionData: ({ usedAssets, assetsData, minCollRatio, interestRate, }: {
7
+ export declare const getLiquityV2AggregatedPositionData: ({ usedAssets, assetsData, minCollRatio, interestRate, liqRatio, }: {
8
8
  usedAssets: LiquityV2UsedAssets;
9
9
  assetsData: LiquityV2AssetsData;
10
10
  minCollRatio: string;
11
11
  interestRate: string;
12
+ liqRatio?: string;
12
13
  }) => LiquityV2AggregatedTroveData;
@@ -38,11 +38,12 @@ const calculateNetApyLiquityV2 = (usedAssets, assetsData, interestRate) => {
38
38
  return { netApy, totalInterestUsd, incentiveUsd };
39
39
  };
40
40
  exports.calculateNetApyLiquityV2 = calculateNetApyLiquityV2;
41
- const getLiquityV2AggregatedPositionData = ({ usedAssets, assetsData, minCollRatio, interestRate, }) => {
41
+ const getLiquityV2AggregatedPositionData = ({ usedAssets, assetsData, minCollRatio, interestRate, liqRatio, }) => {
42
42
  const payload = {};
43
43
  payload.suppliedUsd = (0, moneymarket_1.getAssetsTotal)(usedAssets, (usedAsset) => usedAsset, ({ suppliedUsd }) => suppliedUsd);
44
44
  payload.borrowedUsd = (0, moneymarket_1.getAssetsTotal)(usedAssets, (usedAsset) => usedAsset, ({ borrowedUsd }) => borrowedUsd);
45
45
  payload.borrowLimitUsd = new decimal_js_1.default(payload.suppliedUsd).div(minCollRatio).mul(100).toString();
46
+ payload.liquidationLimitUsd = new decimal_js_1.default(payload.suppliedUsd).div(liqRatio !== null && liqRatio !== void 0 ? liqRatio : minCollRatio).mul(100).toString();
46
47
  const leftToBorrowUsd = new decimal_js_1.default(payload.borrowLimitUsd).sub(payload.borrowedUsd);
47
48
  payload.leftToBorrowUsd = leftToBorrowUsd.lte('0') ? '0' : leftToBorrowUsd.toString();
48
49
  payload.ratio = (+payload.suppliedUsd && +payload.borrowedUsd) ? new decimal_js_1.default(payload.borrowLimitUsd).div(payload.borrowedUsd).mul(100).toString() : '0';
@@ -57,7 +58,7 @@ const getLiquityV2AggregatedPositionData = ({ usedAssets, assetsData, minCollRat
57
58
  payload.liquidationPrice = '';
58
59
  if (leveragedType !== '') {
59
60
  const assetPrice = assetsData[leveragedAsset].price;
60
- payload.liquidationPrice = (0, moneymarket_1.calcLeverageLiqPrice)(leveragedType, assetPrice, payload.borrowedUsd, payload.borrowLimitUsd);
61
+ payload.liquidationPrice = (0, moneymarket_1.calcLeverageLiqPrice)(leveragedType, assetPrice, payload.borrowedUsd, payload.liquidationLimitUsd);
61
62
  }
62
63
  payload.exposure = (0, moneymarket_1.getExposure)(payload.borrowedUsd, payload.suppliedUsd);
63
64
  return payload;
@@ -322,15 +322,14 @@ const _getLiquityV2TroveData = (provider_1, network_1, _a, ...args_1) => __await
322
322
  const interestBatchManager = data.interestBatchManager;
323
323
  const lastInterestRateAdjTime = data.lastInterestRateAdjTime.toString();
324
324
  const hasInterestBatchManager = !(0, utils_1.compareAddresses)(interestBatchManager, constants_1.ZERO_ADDRESS);
325
- const liqRatio = hasInterestBatchManager ? new decimal_js_1.default(minCollRatio).add(batchCollRatio).toString() : minCollRatio;
325
+ const borrowLimitRatio = hasInterestBatchManager ? new decimal_js_1.default(minCollRatio).add(batchCollRatio).toString() : minCollRatio;
326
326
  const payload = Object.assign(Object.assign({ usedAssets,
327
327
  troveId,
328
328
  interestRate,
329
329
  interestBatchManager,
330
330
  debtInFront,
331
- lastInterestRateAdjTime,
332
- liqRatio, troveStatus: types_1.LIQUITY_V2_TROVE_STATUS_ENUM[parseInt(data.status.toString(), 10)] }, (0, liquityV2Helpers_1.getLiquityV2AggregatedPositionData)({
333
- usedAssets, assetsData, minCollRatio: liqRatio, interestRate,
331
+ lastInterestRateAdjTime, liqRatio: minCollRatio, borrowLimitRatio, troveStatus: types_1.LIQUITY_V2_TROVE_STATUS_ENUM[parseInt(data.status.toString(), 10)] }, (0, liquityV2Helpers_1.getLiquityV2AggregatedPositionData)({
332
+ usedAssets, assetsData, minCollRatio: borrowLimitRatio, interestRate, liqRatio: minCollRatio,
334
333
  })), { collRatio });
335
334
  return payload;
336
335
  });
@@ -2,6 +2,10 @@ export declare enum IncentiveKind {
2
2
  Staking = "staking",
3
3
  Reward = "reward"
4
4
  }
5
+ export declare enum IncentiveSide {
6
+ Supply = "supply",
7
+ Borrow = "borrow"
8
+ }
5
9
  export declare enum IncentiveEligibilityId {
6
10
  AaveV3EthenaLiquidLeverage = "0x0dC599DBB180E64E42b87332FDeC3a551445ea44BORROW_BL",
7
11
  AaveV3ArbitrumEthSupply = "0x5d16261c6715a653248269861bbacf68a9774cde",
@@ -1,11 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NetworkNumber = exports.LeverageType = exports.IncentiveEligibilityId = exports.IncentiveKind = void 0;
3
+ exports.NetworkNumber = exports.LeverageType = exports.IncentiveEligibilityId = exports.IncentiveSide = exports.IncentiveKind = void 0;
4
4
  var IncentiveKind;
5
5
  (function (IncentiveKind) {
6
6
  IncentiveKind["Staking"] = "staking";
7
7
  IncentiveKind["Reward"] = "reward";
8
8
  })(IncentiveKind || (exports.IncentiveKind = IncentiveKind = {}));
9
+ var IncentiveSide;
10
+ (function (IncentiveSide) {
11
+ IncentiveSide["Supply"] = "supply";
12
+ IncentiveSide["Borrow"] = "borrow";
13
+ })(IncentiveSide || (exports.IncentiveSide = IncentiveSide = {}));
9
14
  var IncentiveEligibilityId;
10
15
  (function (IncentiveEligibilityId) {
11
16
  IncentiveEligibilityId["AaveV3EthenaLiquidLeverage"] = "0x0dC599DBB180E64E42b87332FDeC3a551445ea44BORROW_BL";
@@ -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;
@@ -1,4 +1,4 @@
1
- import { EthAddress, IncentiveData } from './common';
1
+ import { EthAddress, IncentiveData, IncentiveSide } from './common';
2
2
  export declare enum OpportunityAction {
3
3
  LEND = "LEND",
4
4
  BORROW = "BORROW"
@@ -74,12 +74,11 @@ export type MerkleRewardMap = Record<EthAddress, {
74
74
  borrow?: MerkleRewardInfo;
75
75
  }>;
76
76
  export type AaveV4MerklScopedReward = {
77
- supply?: IncentiveData;
78
- borrow?: IncentiveData;
77
+ [side in IncentiveSide]?: IncentiveData;
79
78
  };
80
79
  /**
81
80
  * Aave V4 Merkl reward campaigns split by scope:
82
- * - `hub`: keyed by underlying token address (lowercase) — rewards for supplying to a hub
81
+ * - `hub`: keyed by `${hubAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing via a hub
83
82
  * - `spoke`: keyed by `${spokeAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing on a spoke
84
83
  */
85
84
  export type AaveV4MerklRewardMap = {
@@ -1,4 +1,5 @@
1
- import { AaveV4MerklRewardMap, AaveV4ReserveAssetData, NetworkNumber } from '../types';
1
+ import { AaveV4MerklRewardMap, AaveV4ReserveAssetData, MerklOpportunity, NetworkNumber } from '../types';
2
+ export declare const buildAaveV4MerklRewardMap: (opportunities: MerklOpportunity[], chainId: NetworkNumber) => AaveV4MerklRewardMap;
2
3
  export declare const getAaveV4MerkleCampaigns: (chainId: NetworkNumber) => Promise<AaveV4MerklRewardMap>;
3
4
  /**
4
5
  * Returns a copy of the asset with scope-specific incentive arrays pre-combined with the asset's
@@ -9,14 +9,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { aprToApy } from '../moneymarket';
11
11
  import { LONGER_TIMEOUT } from '../services/utils';
12
- import { IncentiveKind, OpportunityAction, OpportunityStatus, } from '../types';
12
+ import { IncentiveKind, IncentiveSide, OpportunityAction, OpportunityStatus, } from '../types';
13
13
  /**
14
14
  * Merkl tags Aave V4 reward campaigns by scope via the `type` field:
15
- * - AAVE_V4_HUB_SUPPLY / AAVE_V4_HUB_BORROW → reward tied to a hub (matched per underlying token)
15
+ * - AAVE_V4_HUB_SUPPLY / AAVE_V4_HUB_BORROW → reward tied to a hub (matched per hub contract + underlying)
16
16
  * - AAVE_V4_SPOKE_SUPPLY / AAVE_V4_SPOKE_BORROW → reward tied to a spoke (matched per spoke contract + underlying)
17
- * Hub campaigns identify the underlying via `tokens[0]`; spoke campaigns identify the spoke via `explorerAddress`.
17
+ * Campaigns identify the underlying via `tokens[0]` and the scoping contract (the hub or spoke) via
18
+ * `explorerAddress`. The same underlying exists on several hubs (e.g. USDC on Prime and Paxos), so a
19
+ * hub campaign matched by underlying alone would leak onto every hub's reserves — a campaign whose
20
+ * `explorerAddress` isn't a contract any fetched reserve points to simply never matches.
18
21
  */
19
- const spokeKey = (spokeAddress, underlying) => `${spokeAddress.toLowerCase()}_${underlying.toLowerCase()}`;
22
+ const scopeKey = (scopeAddress, underlying) => `${scopeAddress.toLowerCase()}_${underlying.toLowerCase()}`;
20
23
  const buildIncentive = (opportunity) => {
21
24
  var _a, _b, _c, _d, _e;
22
25
  const rewardToken = (_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;
@@ -28,8 +31,37 @@ const buildIncentive = (opportunity) => {
28
31
  description: `Eligible for ${token} rewards through Merkl.${opportunity.description ? `\n${opportunity.description}` : ''}`,
29
32
  };
30
33
  };
31
- export const getAaveV4MerkleCampaigns = (chainId) => __awaiter(void 0, void 0, void 0, function* () {
34
+ export const buildAaveV4MerklRewardMap = (opportunities, chainId) => {
32
35
  const result = { hub: {}, spoke: {} };
36
+ opportunities
37
+ .filter((o) => o.chainId === chainId)
38
+ .filter((o) => o.status === OpportunityStatus.LIVE)
39
+ .filter((o) => typeof o.type === 'string' && o.type.startsWith('AAVE_V4_'))
40
+ .forEach((o) => {
41
+ var _a, _b, _c, _d;
42
+ const underlying = (_c = (_b = (_a = o.tokens) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.address) === null || _c === void 0 ? void 0 : _c.toLowerCase();
43
+ if (!underlying)
44
+ return;
45
+ const scopeAddress = (_d = o.explorerAddress) === null || _d === void 0 ? void 0 : _d.toLowerCase();
46
+ if (!scopeAddress)
47
+ return;
48
+ const side = o.action === OpportunityAction.BORROW ? IncentiveSide.Borrow : IncentiveSide.Supply;
49
+ const incentive = buildIncentive(o);
50
+ const key = scopeKey(scopeAddress, underlying);
51
+ if (o.type.includes('HUB')) {
52
+ if (!result.hub[key])
53
+ result.hub[key] = {};
54
+ result.hub[key][side] = incentive;
55
+ }
56
+ else if (o.type.includes('SPOKE')) {
57
+ if (!result.spoke[key])
58
+ result.spoke[key] = {};
59
+ result.spoke[key][side] = incentive;
60
+ }
61
+ });
62
+ return result;
63
+ };
64
+ export const getAaveV4MerkleCampaigns = (chainId) => __awaiter(void 0, void 0, void 0, function* () {
33
65
  try {
34
66
  const res = yield 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', {
35
67
  signal: AbortSignal.timeout(LONGER_TIMEOUT),
@@ -37,37 +69,11 @@ export const getAaveV4MerkleCampaigns = (chainId) => __awaiter(void 0, void 0, v
37
69
  if (!res.ok)
38
70
  throw new Error('Failed to fetch Aave V4 Merkle campaigns');
39
71
  const opportunities = yield res.json();
40
- opportunities
41
- .filter((o) => o.chainId === chainId)
42
- .filter((o) => o.status === OpportunityStatus.LIVE)
43
- .filter((o) => typeof o.type === 'string' && o.type.startsWith('AAVE_V4_'))
44
- .forEach((o) => {
45
- var _a, _b, _c, _d;
46
- const underlying = (_c = (_b = (_a = o.tokens) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.address) === null || _c === void 0 ? void 0 : _c.toLowerCase();
47
- if (!underlying)
48
- return;
49
- const side = o.action === OpportunityAction.BORROW ? 'borrow' : 'supply';
50
- const incentive = buildIncentive(o);
51
- if (o.type.includes('HUB')) {
52
- if (!result.hub[underlying])
53
- result.hub[underlying] = {};
54
- result.hub[underlying][side] = incentive;
55
- }
56
- else if (o.type.includes('SPOKE')) {
57
- const spokeAddress = (_d = o.explorerAddress) === null || _d === void 0 ? void 0 : _d.toLowerCase();
58
- if (!spokeAddress)
59
- return;
60
- const key = spokeKey(spokeAddress, underlying);
61
- if (!result.spoke[key])
62
- result.spoke[key] = {};
63
- result.spoke[key][side] = incentive;
64
- }
65
- });
66
- return result;
72
+ return buildAaveV4MerklRewardMap(opportunities, chainId);
67
73
  }
68
74
  catch (e) {
69
75
  console.error('Failed to fetch Aave V4 Merkle campaigns', e);
70
- return result;
76
+ return { hub: {}, spoke: {} };
71
77
  }
72
78
  });
73
79
  /**
@@ -79,7 +85,7 @@ export const attachAaveV4MerklIncentives = (asset, spokeAddress, campaigns) => {
79
85
  const underlying = (_a = asset.underlying) === null || _a === void 0 ? void 0 : _a.toLowerCase();
80
86
  const baseSupply = asset.supplyIncentives || [];
81
87
  const baseBorrow = asset.borrowIncentives || [];
82
- const spokeScoped = (spokeAddress && underlying) ? campaigns.spoke[spokeKey(spokeAddress, underlying)] : undefined;
83
- const hubScoped = underlying ? campaigns.hub[underlying] : undefined;
88
+ const spokeScoped = (spokeAddress && underlying) ? campaigns.spoke[scopeKey(spokeAddress, underlying)] : undefined;
89
+ const hubScoped = (asset.hub && underlying) ? campaigns.hub[scopeKey(asset.hub, underlying)] : undefined;
84
90
  return Object.assign(Object.assign({}, asset), { spokeSupplyIncentives: (spokeScoped === null || spokeScoped === void 0 ? void 0 : spokeScoped.supply) ? [...baseSupply, spokeScoped.supply] : baseSupply, spokeBorrowIncentives: (spokeScoped === null || spokeScoped === void 0 ? void 0 : spokeScoped.borrow) ? [...baseBorrow, spokeScoped.borrow] : baseBorrow, hubSupplyIncentives: (hubScoped === null || hubScoped === void 0 ? void 0 : hubScoped.supply) ? [...baseSupply, hubScoped.supply] : baseSupply, hubBorrowIncentives: (hubScoped === null || hubScoped === void 0 ? void 0 : hubScoped.borrow) ? [...baseBorrow, hubScoped.borrow] : baseBorrow });
85
91
  };
@@ -1,5 +1,11 @@
1
- import { AaveV4AggregatedPositionData, AaveV4AssetsData, AaveV4ReserveAssetData, AaveV4SpokeInfo, AaveV4UsedReserveAsset, AaveV4UsedReserveAssets, EthereumProvider, LeverageType, NetworkNumber } from '../../types';
1
+ import { AaveV4AggregatedPositionData, AaveV4AssetsData, AaveV4ReserveAssetData, AaveV4SpokeInfo, AaveV4UsedReserveAsset, AaveV4UsedReserveAssets, EthereumProvider, IncentiveData, IncentiveSide, LeverageType, NetworkNumber } from '../../types';
2
2
  export declare const calcUserRiskPremiumBps: (usedAssets: AaveV4UsedReserveAssets, assetsData: AaveV4AssetsData) => number;
3
+ /**
4
+ * The incentives that actually accrue to a position on this reserve for the given side: the
5
+ * intrinsic (staking) incentives plus the single applicable Merkl reward. Display surfaces should
6
+ * use this rather than picking a scoped list directly, so badges always match the net APY math.
7
+ */
8
+ export declare const getAaveV4ApplicableIncentives: (assetData: AaveV4ReserveAssetData, side: IncentiveSide) => IncentiveData[];
3
9
  export declare const calculateNetApyAaveV4: ({ usedAssets, assetsData, }: {
4
10
  usedAssets: AaveV4UsedReserveAssets;
5
11
  assetsData: AaveV4AssetsData;
@@ -11,7 +11,7 @@ import Dec from 'decimal.js';
11
11
  import { assetAmountInWei } from '@defisaver/tokens';
12
12
  import { aprToApy, calcLeverageLiqPrice, getAssetsTotal, STABLE_ASSETS, } from '../../moneymarket';
13
13
  import { calculateInterestEarned } from '../../staking';
14
- import { LeverageType, } from '../../types';
14
+ import { IncentiveSide, LeverageType, } from '../../types';
15
15
  import { borrowOperations } from '../../constants';
16
16
  import { AaveV4ViewContractViem } from '../../contracts';
17
17
  import { getViemProvider } from '../../services/viem';
@@ -69,14 +69,25 @@ export const calcUserRiskPremiumBps = (usedAssets, assetsData) => {
69
69
  };
70
70
  /**
71
71
  * `spokeXIncentives`/`hubXIncentives` are each the intrinsic `base` list with at most one Merkl
72
- * reward appended (see attachAaveV4MerklIncentives). Both scopes can apply to the same position at
73
- * once (spoke-specific + hub-wide), so the full applicable set is base + whatever each scope appended.
72
+ * reward appended (see attachAaveV4MerklIncentives). Merkl regularly publishes the same reward
73
+ * stream at both scopes (a spoke campaign and a hub campaign covering the same borrows, e.g. USDC
74
+ * borrowed from the Prime Hub via the Bluechip Spoke), so the scopes must never be summed — the
75
+ * more specific spoke reward wins and the hub reward only applies when no spoke campaign exists,
76
+ * which is also what every per-asset APY badge (and Aave's own UI) shows.
74
77
  */
75
- const mergeScopedIncentives = (base = [], spokeScoped, hubScoped) => [
76
- ...base,
77
- ...(spokeScoped ? spokeScoped.slice(base.length) : []),
78
- ...(hubScoped ? hubScoped.slice(base.length) : []),
79
- ];
78
+ const mergeScopedIncentives = (base = [], spokeScoped, hubScoped) => {
79
+ const spokeExtras = spokeScoped ? spokeScoped.slice(base.length) : [];
80
+ const hubExtras = hubScoped ? hubScoped.slice(base.length) : [];
81
+ return [...base, ...(spokeExtras.length ? spokeExtras : hubExtras)];
82
+ };
83
+ /**
84
+ * The incentives that actually accrue to a position on this reserve for the given side: the
85
+ * intrinsic (staking) incentives plus the single applicable Merkl reward. Display surfaces should
86
+ * use this rather than picking a scoped list directly, so badges always match the net APY math.
87
+ */
88
+ export const getAaveV4ApplicableIncentives = (assetData, side) => (side === IncentiveSide.Supply
89
+ ? mergeScopedIncentives(assetData.supplyIncentives, assetData.spokeSupplyIncentives, assetData.hubSupplyIncentives)
90
+ : mergeScopedIncentives(assetData.borrowIncentives, assetData.spokeBorrowIncentives, assetData.hubBorrowIncentives));
80
91
  export const calculateNetApyAaveV4 = ({ usedAssets, assetsData, }) => {
81
92
  const riskPremiumBps = calcUserRiskPremiumBps(usedAssets, assetsData);
82
93
  const riskPremiumFraction = new Dec(riskPremiumBps).div(10000);
@@ -90,7 +101,7 @@ export const calculateNetApyAaveV4 = ({ usedAssets, assetsData, }) => {
90
101
  acc.suppliedUsd = new Dec(acc.suppliedUsd).add(amount).toString();
91
102
  const supplyInterest = calculateInterestEarned(amount, assetData.supplyRate, 'year', true);
92
103
  acc.supplyInterest = new Dec(acc.supplyInterest).add(supplyInterest.toString()).toString();
93
- const supplyIncentives = mergeScopedIncentives(assetData.supplyIncentives, assetData.spokeSupplyIncentives, assetData.hubSupplyIncentives);
104
+ const supplyIncentives = getAaveV4ApplicableIncentives(assetData, IncentiveSide.Supply);
94
105
  for (const supplyIncentive of supplyIncentives) {
95
106
  const incentiveInterest = calculateInterestEarned(amount, supplyIncentive.apy, 'year', true);
96
107
  acc.incentiveUsd = new Dec(acc.incentiveUsd).add(incentiveInterest).toString();
@@ -106,7 +117,7 @@ export const calculateNetApyAaveV4 = ({ usedAssets, assetsData, }) => {
106
117
  const userBorrowRate = aprToApy(userBorrowApr.toString());
107
118
  const borrowInterest = calculateInterestEarned(amount, userBorrowRate, 'year', true);
108
119
  acc.borrowInterest = new Dec(acc.borrowInterest).sub(borrowInterest.toString()).toString();
109
- const borrowIncentives = mergeScopedIncentives(assetData.borrowIncentives, assetData.spokeBorrowIncentives, assetData.hubBorrowIncentives);
120
+ const borrowIncentives = getAaveV4ApplicableIncentives(assetData, IncentiveSide.Borrow);
110
121
  for (const borrowIncentive of borrowIncentives) {
111
122
  const incentiveInterest = calculateInterestEarned(amount, borrowIncentive.apy, 'year', true);
112
123
  acc.incentiveUsd = new Dec(acc.incentiveUsd).add(incentiveInterest).toString();
@@ -4,9 +4,10 @@ export declare const calculateNetApyLiquityV2: (usedAssets: LiquityV2UsedAssets,
4
4
  totalInterestUsd: string;
5
5
  incentiveUsd: string;
6
6
  };
7
- export declare const getLiquityV2AggregatedPositionData: ({ usedAssets, assetsData, minCollRatio, interestRate, }: {
7
+ export declare const getLiquityV2AggregatedPositionData: ({ usedAssets, assetsData, minCollRatio, interestRate, liqRatio, }: {
8
8
  usedAssets: LiquityV2UsedAssets;
9
9
  assetsData: LiquityV2AssetsData;
10
10
  minCollRatio: string;
11
11
  interestRate: string;
12
+ liqRatio?: string;
12
13
  }) => LiquityV2AggregatedTroveData;
@@ -31,11 +31,12 @@ export const calculateNetApyLiquityV2 = (usedAssets, assetsData, interestRate) =
31
31
  const netApy = new Dec(totalInterestUsd).div(balance).times(100).toString();
32
32
  return { netApy, totalInterestUsd, incentiveUsd };
33
33
  };
34
- export const getLiquityV2AggregatedPositionData = ({ usedAssets, assetsData, minCollRatio, interestRate, }) => {
34
+ export const getLiquityV2AggregatedPositionData = ({ usedAssets, assetsData, minCollRatio, interestRate, liqRatio, }) => {
35
35
  const payload = {};
36
36
  payload.suppliedUsd = getAssetsTotal(usedAssets, (usedAsset) => usedAsset, ({ suppliedUsd }) => suppliedUsd);
37
37
  payload.borrowedUsd = getAssetsTotal(usedAssets, (usedAsset) => usedAsset, ({ borrowedUsd }) => borrowedUsd);
38
38
  payload.borrowLimitUsd = new Dec(payload.suppliedUsd).div(minCollRatio).mul(100).toString();
39
+ payload.liquidationLimitUsd = new Dec(payload.suppliedUsd).div(liqRatio !== null && liqRatio !== void 0 ? liqRatio : minCollRatio).mul(100).toString();
39
40
  const leftToBorrowUsd = new Dec(payload.borrowLimitUsd).sub(payload.borrowedUsd);
40
41
  payload.leftToBorrowUsd = leftToBorrowUsd.lte('0') ? '0' : leftToBorrowUsd.toString();
41
42
  payload.ratio = (+payload.suppliedUsd && +payload.borrowedUsd) ? new Dec(payload.borrowLimitUsd).div(payload.borrowedUsd).mul(100).toString() : '0';
@@ -50,7 +51,7 @@ export const getLiquityV2AggregatedPositionData = ({ usedAssets, assetsData, min
50
51
  payload.liquidationPrice = '';
51
52
  if (leveragedType !== '') {
52
53
  const assetPrice = assetsData[leveragedAsset].price;
53
- payload.liquidationPrice = calcLeverageLiqPrice(leveragedType, assetPrice, payload.borrowedUsd, payload.borrowLimitUsd);
54
+ payload.liquidationPrice = calcLeverageLiqPrice(leveragedType, assetPrice, payload.borrowedUsd, payload.liquidationLimitUsd);
54
55
  }
55
56
  payload.exposure = getExposure(payload.borrowedUsd, payload.suppliedUsd);
56
57
  return payload;
@@ -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
  });
@@ -2,6 +2,10 @@ export declare enum IncentiveKind {
2
2
  Staking = "staking",
3
3
  Reward = "reward"
4
4
  }
5
+ export declare enum IncentiveSide {
6
+ Supply = "supply",
7
+ Borrow = "borrow"
8
+ }
5
9
  export declare enum IncentiveEligibilityId {
6
10
  AaveV3EthenaLiquidLeverage = "0x0dC599DBB180E64E42b87332FDeC3a551445ea44BORROW_BL",
7
11
  AaveV3ArbitrumEthSupply = "0x5d16261c6715a653248269861bbacf68a9774cde",
@@ -3,6 +3,11 @@ export var IncentiveKind;
3
3
  IncentiveKind["Staking"] = "staking";
4
4
  IncentiveKind["Reward"] = "reward";
5
5
  })(IncentiveKind || (IncentiveKind = {}));
6
+ export var IncentiveSide;
7
+ (function (IncentiveSide) {
8
+ IncentiveSide["Supply"] = "supply";
9
+ IncentiveSide["Borrow"] = "borrow";
10
+ })(IncentiveSide || (IncentiveSide = {}));
6
11
  export var IncentiveEligibilityId;
7
12
  (function (IncentiveEligibilityId) {
8
13
  IncentiveEligibilityId["AaveV3EthenaLiquidLeverage"] = "0x0dC599DBB180E64E42b87332FDeC3a551445ea44BORROW_BL";
@@ -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;
@@ -1,4 +1,4 @@
1
- import { EthAddress, IncentiveData } from './common';
1
+ import { EthAddress, IncentiveData, IncentiveSide } from './common';
2
2
  export declare enum OpportunityAction {
3
3
  LEND = "LEND",
4
4
  BORROW = "BORROW"
@@ -74,12 +74,11 @@ export type MerkleRewardMap = Record<EthAddress, {
74
74
  borrow?: MerkleRewardInfo;
75
75
  }>;
76
76
  export type AaveV4MerklScopedReward = {
77
- supply?: IncentiveData;
78
- borrow?: IncentiveData;
77
+ [side in IncentiveSide]?: IncentiveData;
79
78
  };
80
79
  /**
81
80
  * Aave V4 Merkl reward campaigns split by scope:
82
- * - `hub`: keyed by underlying token address (lowercase) — rewards for supplying to a hub
81
+ * - `hub`: keyed by `${hubAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing via a hub
83
82
  * - `spoke`: keyed by `${spokeAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing on a spoke
84
83
  */
85
84
  export type AaveV4MerklRewardMap = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defisaver/positions-sdk",
3
- "version": "2.1.138",
3
+ "version": "2.1.140-dev",
4
4
  "description": "",
5
5
  "main": "./cjs/index.js",
6
6
  "module": "./esm/index.js",
@@ -5,6 +5,7 @@ import {
5
5
  AaveV4ReserveAssetData,
6
6
  IncentiveData,
7
7
  IncentiveKind,
8
+ IncentiveSide,
8
9
  MerklOpportunity,
9
10
  OpportunityAction,
10
11
  OpportunityStatus,
@@ -13,12 +14,15 @@ import {
13
14
 
14
15
  /**
15
16
  * Merkl tags Aave V4 reward campaigns by scope via the `type` field:
16
- * - AAVE_V4_HUB_SUPPLY / AAVE_V4_HUB_BORROW → reward tied to a hub (matched per underlying token)
17
+ * - AAVE_V4_HUB_SUPPLY / AAVE_V4_HUB_BORROW → reward tied to a hub (matched per hub contract + underlying)
17
18
  * - AAVE_V4_SPOKE_SUPPLY / AAVE_V4_SPOKE_BORROW → reward tied to a spoke (matched per spoke contract + underlying)
18
- * Hub campaigns identify the underlying via `tokens[0]`; spoke campaigns identify the spoke via `explorerAddress`.
19
+ * Campaigns identify the underlying via `tokens[0]` and the scoping contract (the hub or spoke) via
20
+ * `explorerAddress`. The same underlying exists on several hubs (e.g. USDC on Prime and Paxos), so a
21
+ * hub campaign matched by underlying alone would leak onto every hub's reserves — a campaign whose
22
+ * `explorerAddress` isn't a contract any fetched reserve points to simply never matches.
19
23
  */
20
24
 
21
- const spokeKey = (spokeAddress: string, underlying: string) => `${spokeAddress.toLowerCase()}_${underlying.toLowerCase()}`;
25
+ const scopeKey = (scopeAddress: string, underlying: string) => `${scopeAddress.toLowerCase()}_${underlying.toLowerCase()}`;
22
26
 
23
27
  const buildIncentive = (opportunity: MerklOpportunity): IncentiveData => {
24
28
  const rewardToken = opportunity.rewardsRecord?.breakdowns?.[0]?.token;
@@ -31,42 +35,47 @@ const buildIncentive = (opportunity: MerklOpportunity): IncentiveData => {
31
35
  };
32
36
  };
33
37
 
34
- export const getAaveV4MerkleCampaigns = async (chainId: NetworkNumber): Promise<AaveV4MerklRewardMap> => {
38
+ export const buildAaveV4MerklRewardMap = (opportunities: MerklOpportunity[], chainId: NetworkNumber): AaveV4MerklRewardMap => {
35
39
  const result: AaveV4MerklRewardMap = { hub: {}, spoke: {} };
40
+
41
+ opportunities
42
+ .filter((o) => o.chainId === chainId)
43
+ .filter((o) => o.status === OpportunityStatus.LIVE)
44
+ .filter((o) => typeof o.type === 'string' && o.type.startsWith('AAVE_V4_'))
45
+ .forEach((o) => {
46
+ const underlying = o.tokens?.[0]?.address?.toLowerCase();
47
+ if (!underlying) return;
48
+
49
+ const scopeAddress = o.explorerAddress?.toLowerCase();
50
+ if (!scopeAddress) return;
51
+
52
+ const side = o.action === OpportunityAction.BORROW ? IncentiveSide.Borrow : IncentiveSide.Supply;
53
+ const incentive = buildIncentive(o);
54
+ const key = scopeKey(scopeAddress, underlying);
55
+
56
+ if (o.type.includes('HUB')) {
57
+ if (!result.hub[key]) result.hub[key] = {};
58
+ result.hub[key][side] = incentive;
59
+ } else if (o.type.includes('SPOKE')) {
60
+ if (!result.spoke[key]) result.spoke[key] = {};
61
+ result.spoke[key][side] = incentive;
62
+ }
63
+ });
64
+
65
+ return result;
66
+ };
67
+
68
+ export const getAaveV4MerkleCampaigns = async (chainId: NetworkNumber): Promise<AaveV4MerklRewardMap> => {
36
69
  try {
37
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', {
38
71
  signal: AbortSignal.timeout(LONGER_TIMEOUT),
39
72
  });
40
73
  if (!res.ok) throw new Error('Failed to fetch Aave V4 Merkle campaigns');
41
74
  const opportunities = await res.json() as MerklOpportunity[];
42
-
43
- opportunities
44
- .filter((o) => o.chainId === chainId)
45
- .filter((o) => o.status === OpportunityStatus.LIVE)
46
- .filter((o) => typeof o.type === 'string' && o.type.startsWith('AAVE_V4_'))
47
- .forEach((o) => {
48
- const underlying = o.tokens?.[0]?.address?.toLowerCase();
49
- if (!underlying) return;
50
-
51
- const side: 'supply' | 'borrow' = o.action === OpportunityAction.BORROW ? 'borrow' : 'supply';
52
- const incentive = buildIncentive(o);
53
-
54
- if (o.type.includes('HUB')) {
55
- if (!result.hub[underlying]) result.hub[underlying] = {};
56
- result.hub[underlying][side] = incentive;
57
- } else if (o.type.includes('SPOKE')) {
58
- const spokeAddress = o.explorerAddress?.toLowerCase();
59
- if (!spokeAddress) return;
60
- const key = spokeKey(spokeAddress, underlying);
61
- if (!result.spoke[key]) result.spoke[key] = {};
62
- result.spoke[key][side] = incentive;
63
- }
64
- });
65
-
66
- return result;
75
+ return buildAaveV4MerklRewardMap(opportunities, chainId);
67
76
  } catch (e) {
68
77
  console.error('Failed to fetch Aave V4 Merkle campaigns', e);
69
- return result;
78
+ return { hub: {}, spoke: {} };
70
79
  }
71
80
  };
72
81
 
@@ -79,8 +88,8 @@ export const attachAaveV4MerklIncentives = (asset: AaveV4ReserveAssetData, spoke
79
88
  const baseSupply = asset.supplyIncentives || [];
80
89
  const baseBorrow = asset.borrowIncentives || [];
81
90
 
82
- const spokeScoped = (spokeAddress && underlying) ? campaigns.spoke[spokeKey(spokeAddress, underlying)] : undefined;
83
- const hubScoped = underlying ? campaigns.hub[underlying] : undefined;
91
+ const spokeScoped = (spokeAddress && underlying) ? campaigns.spoke[scopeKey(spokeAddress, underlying)] : undefined;
92
+ const hubScoped = (asset.hub && underlying) ? campaigns.hub[scopeKey(asset.hub, underlying)] : undefined;
84
93
 
85
94
  return {
86
95
  ...asset,
@@ -17,6 +17,7 @@ import {
17
17
  AaveV4UsedReserveAssets,
18
18
  EthereumProvider,
19
19
  IncentiveData,
20
+ IncentiveSide,
20
21
  LeverageType,
21
22
  NetworkNumber,
22
23
  } from '../../types';
@@ -93,14 +94,26 @@ export const calcUserRiskPremiumBps = (usedAssets: AaveV4UsedReserveAssets, asse
93
94
 
94
95
  /**
95
96
  * `spokeXIncentives`/`hubXIncentives` are each the intrinsic `base` list with at most one Merkl
96
- * reward appended (see attachAaveV4MerklIncentives). Both scopes can apply to the same position at
97
- * once (spoke-specific + hub-wide), so the full applicable set is base + whatever each scope appended.
97
+ * reward appended (see attachAaveV4MerklIncentives). Merkl regularly publishes the same reward
98
+ * stream at both scopes (a spoke campaign and a hub campaign covering the same borrows, e.g. USDC
99
+ * borrowed from the Prime Hub via the Bluechip Spoke), so the scopes must never be summed — the
100
+ * more specific spoke reward wins and the hub reward only applies when no spoke campaign exists,
101
+ * which is also what every per-asset APY badge (and Aave's own UI) shows.
98
102
  */
99
- const mergeScopedIncentives = (base: IncentiveData[] = [], spokeScoped?: IncentiveData[], hubScoped?: IncentiveData[]): IncentiveData[] => [
100
- ...base,
101
- ...(spokeScoped ? spokeScoped.slice(base.length) : []),
102
- ...(hubScoped ? hubScoped.slice(base.length) : []),
103
- ];
103
+ const mergeScopedIncentives = (base: IncentiveData[] = [], spokeScoped?: IncentiveData[], hubScoped?: IncentiveData[]): IncentiveData[] => {
104
+ const spokeExtras = spokeScoped ? spokeScoped.slice(base.length) : [];
105
+ const hubExtras = hubScoped ? hubScoped.slice(base.length) : [];
106
+ return [...base, ...(spokeExtras.length ? spokeExtras : hubExtras)];
107
+ };
108
+
109
+ /**
110
+ * The incentives that actually accrue to a position on this reserve for the given side: the
111
+ * intrinsic (staking) incentives plus the single applicable Merkl reward. Display surfaces should
112
+ * use this rather than picking a scoped list directly, so badges always match the net APY math.
113
+ */
114
+ export const getAaveV4ApplicableIncentives = (assetData: AaveV4ReserveAssetData, side: IncentiveSide): IncentiveData[] => (side === IncentiveSide.Supply
115
+ ? mergeScopedIncentives(assetData.supplyIncentives, assetData.spokeSupplyIncentives, assetData.hubSupplyIncentives)
116
+ : mergeScopedIncentives(assetData.borrowIncentives, assetData.spokeBorrowIncentives, assetData.hubBorrowIncentives));
104
117
 
105
118
  export const calculateNetApyAaveV4 = ({
106
119
  usedAssets,
@@ -123,7 +136,7 @@ export const calculateNetApyAaveV4 = ({
123
136
  const supplyInterest = calculateInterestEarned(amount, assetData.supplyRate, 'year', true);
124
137
  acc.supplyInterest = new Dec(acc.supplyInterest).add(supplyInterest.toString()).toString();
125
138
 
126
- const supplyIncentives = mergeScopedIncentives(assetData.supplyIncentives, assetData.spokeSupplyIncentives, assetData.hubSupplyIncentives);
139
+ const supplyIncentives = getAaveV4ApplicableIncentives(assetData, IncentiveSide.Supply);
127
140
  for (const supplyIncentive of supplyIncentives) {
128
141
  const incentiveInterest = calculateInterestEarned(amount, supplyIncentive.apy, 'year', true);
129
142
  acc.incentiveUsd = new Dec(acc.incentiveUsd).add(incentiveInterest).toString();
@@ -141,7 +154,7 @@ export const calculateNetApyAaveV4 = ({
141
154
  const borrowInterest = calculateInterestEarned(amount, userBorrowRate, 'year', true);
142
155
  acc.borrowInterest = new Dec(acc.borrowInterest).sub(borrowInterest.toString()).toString();
143
156
 
144
- const borrowIncentives = mergeScopedIncentives(assetData.borrowIncentives, assetData.spokeBorrowIncentives, assetData.hubBorrowIncentives);
157
+ const borrowIncentives = getAaveV4ApplicableIncentives(assetData, IncentiveSide.Borrow);
145
158
  for (const borrowIncentive of borrowIncentives) {
146
159
  const incentiveInterest = calculateInterestEarned(amount, borrowIncentive.apy, 'year', true);
147
160
  acc.incentiveUsd = new Dec(acc.incentiveUsd).add(incentiveInterest).toString();
@@ -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
 
@@ -425,7 +425,7 @@ export const _getLiquityV2TroveData = async (
425
425
  const lastInterestRateAdjTime = data.lastInterestRateAdjTime.toString();
426
426
 
427
427
  const hasInterestBatchManager = !compareAddresses(interestBatchManager, ZERO_ADDRESS);
428
- const liqRatio = hasInterestBatchManager ? new Dec(minCollRatio).add(batchCollRatio).toString() : minCollRatio;
428
+ const borrowLimitRatio = hasInterestBatchManager ? new Dec(minCollRatio).add(batchCollRatio).toString() : minCollRatio;
429
429
 
430
430
  const payload: LiquityV2TroveData = {
431
431
  usedAssets,
@@ -434,10 +434,11 @@ export const _getLiquityV2TroveData = async (
434
434
  interestBatchManager,
435
435
  debtInFront,
436
436
  lastInterestRateAdjTime,
437
- liqRatio,
437
+ liqRatio: minCollRatio,
438
+ borrowLimitRatio,
438
439
  troveStatus: LIQUITY_V2_TROVE_STATUS_ENUM[parseInt(data.status.toString(), 10)],
439
440
  ...getLiquityV2AggregatedPositionData({
440
- usedAssets, assetsData, minCollRatio: liqRatio, interestRate,
441
+ usedAssets, assetsData, minCollRatio: borrowLimitRatio, interestRate, liqRatio: minCollRatio,
441
442
  }),
442
443
  collRatio,
443
444
  };
@@ -3,6 +3,11 @@ export enum IncentiveKind {
3
3
  Reward = 'reward',
4
4
  }
5
5
 
6
+ export enum IncentiveSide {
7
+ Supply = 'supply',
8
+ Borrow = 'borrow',
9
+ }
10
+
6
11
  export enum IncentiveEligibilityId {
7
12
  AaveV3EthenaLiquidLeverage = '0x0dC599DBB180E64E42b87332FDeC3a551445ea44BORROW_BL',
8
13
  AaveV3ArbitrumEthSupply = '0x5d16261c6715a653248269861bbacf68a9774cde',
@@ -94,6 +94,7 @@ export interface LiquityV2AggregatedTroveData {
94
94
  suppliedUsd: string,
95
95
  borrowedUsd: string,
96
96
  borrowLimitUsd: string,
97
+ liquidationLimitUsd: string,
97
98
  leftToBorrowUsd: string,
98
99
  netApy: string,
99
100
  incentiveUsd: string,
@@ -112,9 +113,11 @@ export interface LiquityV2TroveData {
112
113
  ratio: string,
113
114
  collRatio: string,
114
115
  liqRatio: string,
116
+ borrowLimitRatio: string,
115
117
  interestRate: string,
116
118
  leftToBorrowUsd: string,
117
119
  borrowLimitUsd: string,
120
+ liquidationLimitUsd: string,
118
121
  suppliedUsd: string,
119
122
  borrowedUsd: string,
120
123
  netApy: string,
@@ -1,4 +1,4 @@
1
- import { EthAddress, IncentiveData } from './common';
1
+ import { EthAddress, IncentiveData, IncentiveSide } from './common';
2
2
 
3
3
  export enum OpportunityAction {
4
4
  LEND = 'LEND',
@@ -70,11 +70,11 @@ export type MerklOpportunity = {
70
70
  export type MerkleRewardInfo = { apy: string; rewardTokenSymbol: string, description: string, identifier: string };
71
71
  export type MerkleRewardMap = Record<EthAddress, { supply?: MerkleRewardInfo; borrow?: MerkleRewardInfo }>;
72
72
 
73
- export type AaveV4MerklScopedReward = { supply?: IncentiveData; borrow?: IncentiveData };
73
+ export type AaveV4MerklScopedReward = { [side in IncentiveSide]?: IncentiveData };
74
74
 
75
75
  /**
76
76
  * Aave V4 Merkl reward campaigns split by scope:
77
- * - `hub`: keyed by underlying token address (lowercase) — rewards for supplying to a hub
77
+ * - `hub`: keyed by `${hubAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing via a hub
78
78
  * - `spoke`: keyed by `${spokeAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing on a spoke
79
79
  */
80
80
  export type AaveV4MerklRewardMap = {