@defisaver/positions-sdk 2.1.154 → 2.1.155

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.
@@ -15,14 +15,15 @@ const merkl_1 = require("../services/merkl");
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 hub contract + underlying)
19
- * - AAVE_V4_SPOKE_SUPPLY / AAVE_V4_SPOKE_BORROW → reward tied to a spoke (matched per spoke contract + underlying)
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.
18
+ * - AAVE_V4_HUB_SUPPLY / AAVE_V4_HUB_BORROW → reward tied to a hub asset
19
+ * - AAVE_V4_SPOKE_SUPPLY / AAVE_V4_SPOKE_BORROW → reward tied to a spoke reserve
20
+ * Embedded campaign params provide the exact on-chain identifiers. Token addresses cannot safely
21
+ * identify Aave V4 rewards because one spoke can expose the same underlying from multiple hubs.
22
+ * Each stored reward is also tagged with campaign identity (`campaignIds`/`parentCampaignIds`) so
23
+ * a hub (parent) campaign's child re-listing on a spoke can be told apart from a genuinely
24
+ * distinct spoke campaign — the former replaces the hub reward, the latter combines with it.
24
25
  */
25
- const scopeKey = (scopeAddress, underlying) => `${scopeAddress.toLowerCase()}_${underlying.toLowerCase()}`;
26
+ const scopeKey = (scopeAddress, id) => `${scopeAddress.toLowerCase()}_${id.toString()}`;
26
27
  const buildIncentive = (opportunity) => {
27
28
  var _a, _b, _c, _d, _e;
28
29
  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;
@@ -41,25 +42,45 @@ const buildAaveV4MerklRewardMap = (opportunities, chainId) => {
41
42
  .filter((o) => o.status === types_1.OpportunityStatus.LIVE)
42
43
  .filter((o) => typeof o.type === 'string' && o.type.startsWith('AAVE_V4_'))
43
44
  .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;
45
+ var _a, _b;
51
46
  const side = o.action === types_1.OpportunityAction.BORROW ? types_1.IncentiveSide.Borrow : types_1.IncentiveSide.Supply;
52
47
  const incentive = buildIncentive(o);
53
- const key = scopeKey(scopeAddress, underlying);
48
+ // one opportunity can span several campaigns (e.g. renewed periods), so campaign identity is
49
+ // collected per scope key before the reward entries are written
50
+ const idsByKey = {};
51
+ const collect = (key, campaign) => {
52
+ if (!idsByKey[key])
53
+ idsByKey[key] = { campaignIds: new Set(), parentCampaignIds: new Set() };
54
+ if (campaign.id)
55
+ idsByKey[key].campaignIds.add(campaign.id);
56
+ if (campaign.parentCampaignId)
57
+ idsByKey[key].parentCampaignIds.add(campaign.parentCampaignId);
58
+ };
54
59
  if (o.type.includes('HUB')) {
55
- if (!result.hub[key])
56
- result.hub[key] = {};
57
- result.hub[key][side] = incentive;
60
+ (_a = o.campaigns) === null || _a === void 0 ? void 0 : _a.forEach((c) => {
61
+ var _a;
62
+ if (!((_a = c.params) === null || _a === void 0 ? void 0 : _a.hubAddress) || c.params.assetId === undefined || c.params.assetId === null)
63
+ return;
64
+ collect(scopeKey(c.params.hubAddress, c.params.assetId), c);
65
+ });
66
+ Object.entries(idsByKey).forEach(([key, ids]) => {
67
+ if (!result.hub[key])
68
+ result.hub[key] = {};
69
+ result.hub[key][side] = [...(result.hub[key][side] || []), Object.assign(Object.assign({}, incentive), { campaignIds: [...ids.campaignIds] })];
70
+ });
58
71
  }
59
72
  else if (o.type.includes('SPOKE')) {
60
- if (!result.spoke[key])
61
- result.spoke[key] = {};
62
- result.spoke[key][side] = incentive;
73
+ (_b = o.campaigns) === null || _b === void 0 ? void 0 : _b.forEach((c) => {
74
+ var _a;
75
+ if (!((_a = c.params) === null || _a === void 0 ? void 0 : _a.spokeAddress) || c.params.reserveId === undefined || c.params.reserveId === null)
76
+ return;
77
+ collect(scopeKey(c.params.spokeAddress, c.params.reserveId), c);
78
+ });
79
+ Object.entries(idsByKey).forEach(([key, ids]) => {
80
+ if (!result.spoke[key])
81
+ result.spoke[key] = {};
82
+ result.spoke[key][side] = [...(result.spoke[key][side] || []), Object.assign(Object.assign({}, incentive), { campaignIds: [...ids.campaignIds], parentCampaignIds: [...ids.parentCampaignIds] })];
83
+ });
63
84
  }
64
85
  });
65
86
  return result;
@@ -71,6 +92,7 @@ const getAaveV4MerkleCampaigns = (chainId) => __awaiter(void 0, void 0, void 0,
71
92
  mainProtocolId: 'aave',
72
93
  type: 'AAVE_V4_HUB_SUPPLY,AAVE_V4_HUB_BORROW,AAVE_V4_SPOKE_SUPPLY,AAVE_V4_SPOKE_BORROW',
73
94
  status: types_1.OpportunityStatus.LIVE,
95
+ campaigns: 'true',
74
96
  });
75
97
  return (0, exports.buildAaveV4MerklRewardMap)(opportunities, chainId);
76
98
  }
@@ -85,12 +107,11 @@ exports.getAaveV4MerkleCampaigns = getAaveV4MerkleCampaigns;
85
107
  * intrinsic (staking) incentives, so each surface can render base yield + the rewards that apply to it.
86
108
  */
87
109
  const attachAaveV4MerklIncentives = (asset, spokeAddress, campaigns) => {
88
- var _a;
89
- const underlying = (_a = asset.underlying) === null || _a === void 0 ? void 0 : _a.toLowerCase();
110
+ var _a, _b, _c, _d;
90
111
  const baseSupply = asset.supplyIncentives || [];
91
112
  const baseBorrow = asset.borrowIncentives || [];
92
- const spokeScoped = (spokeAddress && underlying) ? campaigns.spoke[scopeKey(spokeAddress, underlying)] : undefined;
93
- const hubScoped = (asset.hub && underlying) ? campaigns.hub[scopeKey(asset.hub, underlying)] : undefined;
94
- 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 });
113
+ const spokeScoped = spokeAddress ? campaigns.spoke[scopeKey(spokeAddress, asset.reserveId)] : undefined;
114
+ const hubScoped = asset.hub ? campaigns.hub[scopeKey(asset.hub, asset.assetId)] : undefined;
115
+ return Object.assign(Object.assign({}, asset), { spokeSupplyIncentives: ((_a = spokeScoped === null || spokeScoped === void 0 ? void 0 : spokeScoped.supply) === null || _a === void 0 ? void 0 : _a.length) ? [...baseSupply, ...spokeScoped.supply] : baseSupply, spokeBorrowIncentives: ((_b = spokeScoped === null || spokeScoped === void 0 ? void 0 : spokeScoped.borrow) === null || _b === void 0 ? void 0 : _b.length) ? [...baseBorrow, ...spokeScoped.borrow] : baseBorrow, hubSupplyIncentives: ((_c = hubScoped === null || hubScoped === void 0 ? void 0 : hubScoped.supply) === null || _c === void 0 ? void 0 : _c.length) ? [...baseSupply, ...hubScoped.supply] : baseSupply, hubBorrowIncentives: ((_d = hubScoped === null || hubScoped === void 0 ? void 0 : hubScoped.borrow) === null || _d === void 0 ? void 0 : _d.length) ? [...baseBorrow, ...hubScoped.borrow] : baseBorrow });
95
116
  };
96
117
  exports.attachAaveV4MerklIncentives = attachAaveV4MerklIncentives;
@@ -2,7 +2,7 @@ import { AaveV4AggregatedPositionData, AaveV4AssetsData, AaveV4ReserveAssetData,
2
2
  export declare const calcUserRiskPremiumBps: (usedAssets: AaveV4UsedReserveAssets, assetsData: AaveV4AssetsData) => number;
3
3
  /**
4
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
5
+ * intrinsic (staking) incentives plus the applicable Merkl rewards. Display surfaces should
6
6
  * use this rather than picking a scoped list directly, so badges always match the net APY math.
7
7
  */
8
8
  export declare const getAaveV4ApplicableIncentives: (assetData: AaveV4ReserveAssetData, side: IncentiveSide) => IncentiveData[];
@@ -75,21 +75,31 @@ const calcUserRiskPremiumBps = (usedAssets, assetsData) => {
75
75
  };
76
76
  exports.calcUserRiskPremiumBps = calcUserRiskPremiumBps;
77
77
  /**
78
- * `spokeXIncentives`/`hubXIncentives` are each the intrinsic `base` list with at most one Merkl
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.
78
+ * `spokeXIncentives`/`hubXIncentives` are each the intrinsic `base` list with the Merkl rewards
79
+ * of that scope appended (see attachAaveV4MerklIncentives). Merkl publishes a hub reward stream at both
80
+ * scopes the hub (parent) campaign plus a child campaign per covered spoke (e.g. USDC borrowed
81
+ * from the Prime Hub via the Bluechip Spoke) so a child must never be summed with its parent:
82
+ * the more specific spoke listing wins. A hub reward the spoke reward is not a child of comes from
83
+ * a distinct campaign, and both genuinely accrue, so they are shown together — which is also what
84
+ * every per-asset APY badge (and Aave's own UI) shows.
84
85
  */
85
86
  const mergeScopedIncentives = (base = [], spokeScoped, hubScoped) => {
86
87
  const spokeExtras = spokeScoped ? spokeScoped.slice(base.length) : [];
87
88
  const hubExtras = hubScoped ? hubScoped.slice(base.length) : [];
88
- return [...base, ...(spokeExtras.length ? spokeExtras : hubExtras)];
89
+ const parentIds = new Set();
90
+ spokeExtras.forEach((extra) => { var _a; return (_a = extra.parentCampaignIds) === null || _a === void 0 ? void 0 : _a.forEach((id) => parentIds.add(id)); });
91
+ const distinctHubExtras = hubExtras.filter((extra) => {
92
+ var _a;
93
+ // without campaign identity the parent/child relation is unknowable — fail closed to spoke-wins
94
+ if (!((_a = extra.campaignIds) === null || _a === void 0 ? void 0 : _a.length))
95
+ return !spokeExtras.length;
96
+ return !extra.campaignIds.some((id) => parentIds.has(id));
97
+ });
98
+ return [...base, ...spokeExtras, ...distinctHubExtras];
89
99
  };
90
100
  /**
91
101
  * 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
102
+ * intrinsic (staking) incentives plus the applicable Merkl rewards. Display surfaces should
93
103
  * use this rather than picking a scoped list directly, so badges always match the net APY math.
94
104
  */
95
105
  const getAaveV4ApplicableIncentives = (assetData, side) => (side === types_1.IncentiveSide.Supply
@@ -8,6 +8,24 @@ export declare enum OpportunityStatus {
8
8
  PAST = "PAST",
9
9
  UPCOMING = "UPCOMING"
10
10
  }
11
+ export type MerklCampaign = {
12
+ id: string;
13
+ campaignId: string;
14
+ /**
15
+ * Merkl-internal `id` of the parent campaign — set on child campaigns, which re-publish a hub
16
+ * (parent) campaign's reward scoped to a single spoke reserve. Absent on standalone campaigns,
17
+ * whose `params` still carry `hubAddress`/`hubAssetId`, so only this field tells the two apart.
18
+ */
19
+ parentCampaignId?: string;
20
+ childCampaignIds?: string[];
21
+ params?: {
22
+ reserveId?: string | number;
23
+ spokeAddress?: EthAddress;
24
+ hubAddress?: EthAddress;
25
+ hubAssetId?: string | number;
26
+ assetId?: string | number;
27
+ };
28
+ };
11
29
  export type MerklOpportunity = {
12
30
  chainId: number;
13
31
  type: string;
@@ -22,6 +40,7 @@ export type MerklOpportunity = {
22
40
  id: string;
23
41
  explorerAddress?: EthAddress;
24
42
  description?: string;
43
+ campaigns?: MerklCampaign[];
25
44
  tokens: {
26
45
  id: string;
27
46
  name: string;
@@ -73,8 +92,19 @@ export type MerkleRewardMap = Record<EthAddress, {
73
92
  supply?: MerkleRewardInfo;
74
93
  borrow?: MerkleRewardInfo;
75
94
  }>;
95
+ /**
96
+ * A scoped Aave V4 Merkl reward tagged with the campaign identity needed to resolve parent/child
97
+ * listings downstream: `campaignIds` are the Merkl-internal ids of the campaigns behind this
98
+ * entry, `parentCampaignIds` (spoke entries only) the ids of the hub campaigns those are children
99
+ * of. A hub reward is dropped only for a spoke reward that is its own child re-listing — a spoke
100
+ * reward from a distinct campaign combines with it instead.
101
+ */
102
+ export type AaveV4MerklIncentive = IncentiveData & {
103
+ campaignIds?: string[];
104
+ parentCampaignIds?: string[];
105
+ };
76
106
  export type AaveV4MerklScopedReward = {
77
- [side in IncentiveSide]?: IncentiveData;
107
+ [side in IncentiveSide]?: AaveV4MerklIncentive[];
78
108
  };
79
109
  /**
80
110
  * Fluid vault-scoped Merkl campaigns keyed by lowercase vault address — `supply` rewards apply to
@@ -86,8 +116,8 @@ export type FluidMerklRewardMap = Record<string, {
86
116
  }>;
87
117
  /**
88
118
  * Aave V4 Merkl reward campaigns split by scope:
89
- * - `hub`: keyed by `${hubAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing via a hub
90
- * - `spoke`: keyed by `${spokeAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing on a spoke
119
+ * - `hub`: keyed by `${hubAddress}_${assetId}` — rewards for supplying/borrowing via a hub asset
120
+ * - `spoke`: keyed by `${spokeAddress}_${reserveId}` — rewards for supplying/borrowing on a spoke reserve
91
121
  */
92
122
  export type AaveV4MerklRewardMap = {
93
123
  hub: Record<string, AaveV4MerklScopedReward>;
@@ -12,14 +12,15 @@ import { fetchAllMerklOpportunities } from '../services/merkl';
12
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 hub contract + underlying)
16
- * - AAVE_V4_SPOKE_SUPPLY / AAVE_V4_SPOKE_BORROW → reward tied to a spoke (matched per spoke contract + underlying)
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.
15
+ * - AAVE_V4_HUB_SUPPLY / AAVE_V4_HUB_BORROW → reward tied to a hub asset
16
+ * - AAVE_V4_SPOKE_SUPPLY / AAVE_V4_SPOKE_BORROW → reward tied to a spoke reserve
17
+ * Embedded campaign params provide the exact on-chain identifiers. Token addresses cannot safely
18
+ * identify Aave V4 rewards because one spoke can expose the same underlying from multiple hubs.
19
+ * Each stored reward is also tagged with campaign identity (`campaignIds`/`parentCampaignIds`) so
20
+ * a hub (parent) campaign's child re-listing on a spoke can be told apart from a genuinely
21
+ * distinct spoke campaign — the former replaces the hub reward, the latter combines with it.
21
22
  */
22
- const scopeKey = (scopeAddress, underlying) => `${scopeAddress.toLowerCase()}_${underlying.toLowerCase()}`;
23
+ const scopeKey = (scopeAddress, id) => `${scopeAddress.toLowerCase()}_${id.toString()}`;
23
24
  const buildIncentive = (opportunity) => {
24
25
  var _a, _b, _c, _d, _e;
25
26
  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;
@@ -38,25 +39,45 @@ export const buildAaveV4MerklRewardMap = (opportunities, chainId) => {
38
39
  .filter((o) => o.status === OpportunityStatus.LIVE)
39
40
  .filter((o) => typeof o.type === 'string' && o.type.startsWith('AAVE_V4_'))
40
41
  .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;
42
+ var _a, _b;
48
43
  const side = o.action === OpportunityAction.BORROW ? IncentiveSide.Borrow : IncentiveSide.Supply;
49
44
  const incentive = buildIncentive(o);
50
- const key = scopeKey(scopeAddress, underlying);
45
+ // one opportunity can span several campaigns (e.g. renewed periods), so campaign identity is
46
+ // collected per scope key before the reward entries are written
47
+ const idsByKey = {};
48
+ const collect = (key, campaign) => {
49
+ if (!idsByKey[key])
50
+ idsByKey[key] = { campaignIds: new Set(), parentCampaignIds: new Set() };
51
+ if (campaign.id)
52
+ idsByKey[key].campaignIds.add(campaign.id);
53
+ if (campaign.parentCampaignId)
54
+ idsByKey[key].parentCampaignIds.add(campaign.parentCampaignId);
55
+ };
51
56
  if (o.type.includes('HUB')) {
52
- if (!result.hub[key])
53
- result.hub[key] = {};
54
- result.hub[key][side] = incentive;
57
+ (_a = o.campaigns) === null || _a === void 0 ? void 0 : _a.forEach((c) => {
58
+ var _a;
59
+ if (!((_a = c.params) === null || _a === void 0 ? void 0 : _a.hubAddress) || c.params.assetId === undefined || c.params.assetId === null)
60
+ return;
61
+ collect(scopeKey(c.params.hubAddress, c.params.assetId), c);
62
+ });
63
+ Object.entries(idsByKey).forEach(([key, ids]) => {
64
+ if (!result.hub[key])
65
+ result.hub[key] = {};
66
+ result.hub[key][side] = [...(result.hub[key][side] || []), Object.assign(Object.assign({}, incentive), { campaignIds: [...ids.campaignIds] })];
67
+ });
55
68
  }
56
69
  else if (o.type.includes('SPOKE')) {
57
- if (!result.spoke[key])
58
- result.spoke[key] = {};
59
- result.spoke[key][side] = incentive;
70
+ (_b = o.campaigns) === null || _b === void 0 ? void 0 : _b.forEach((c) => {
71
+ var _a;
72
+ if (!((_a = c.params) === null || _a === void 0 ? void 0 : _a.spokeAddress) || c.params.reserveId === undefined || c.params.reserveId === null)
73
+ return;
74
+ collect(scopeKey(c.params.spokeAddress, c.params.reserveId), c);
75
+ });
76
+ Object.entries(idsByKey).forEach(([key, ids]) => {
77
+ if (!result.spoke[key])
78
+ result.spoke[key] = {};
79
+ result.spoke[key][side] = [...(result.spoke[key][side] || []), Object.assign(Object.assign({}, incentive), { campaignIds: [...ids.campaignIds], parentCampaignIds: [...ids.parentCampaignIds] })];
80
+ });
60
81
  }
61
82
  });
62
83
  return result;
@@ -67,6 +88,7 @@ export const getAaveV4MerkleCampaigns = (chainId) => __awaiter(void 0, void 0, v
67
88
  mainProtocolId: 'aave',
68
89
  type: 'AAVE_V4_HUB_SUPPLY,AAVE_V4_HUB_BORROW,AAVE_V4_SPOKE_SUPPLY,AAVE_V4_SPOKE_BORROW',
69
90
  status: OpportunityStatus.LIVE,
91
+ campaigns: 'true',
70
92
  });
71
93
  return buildAaveV4MerklRewardMap(opportunities, chainId);
72
94
  }
@@ -80,11 +102,10 @@ export const getAaveV4MerkleCampaigns = (chainId) => __awaiter(void 0, void 0, v
80
102
  * intrinsic (staking) incentives, so each surface can render base yield + the rewards that apply to it.
81
103
  */
82
104
  export const attachAaveV4MerklIncentives = (asset, spokeAddress, campaigns) => {
83
- var _a;
84
- const underlying = (_a = asset.underlying) === null || _a === void 0 ? void 0 : _a.toLowerCase();
105
+ var _a, _b, _c, _d;
85
106
  const baseSupply = asset.supplyIncentives || [];
86
107
  const baseBorrow = asset.borrowIncentives || [];
87
- const spokeScoped = (spokeAddress && underlying) ? campaigns.spoke[scopeKey(spokeAddress, underlying)] : undefined;
88
- const hubScoped = (asset.hub && underlying) ? campaigns.hub[scopeKey(asset.hub, underlying)] : undefined;
89
- 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 });
108
+ const spokeScoped = spokeAddress ? campaigns.spoke[scopeKey(spokeAddress, asset.reserveId)] : undefined;
109
+ const hubScoped = asset.hub ? campaigns.hub[scopeKey(asset.hub, asset.assetId)] : undefined;
110
+ return Object.assign(Object.assign({}, asset), { spokeSupplyIncentives: ((_a = spokeScoped === null || spokeScoped === void 0 ? void 0 : spokeScoped.supply) === null || _a === void 0 ? void 0 : _a.length) ? [...baseSupply, ...spokeScoped.supply] : baseSupply, spokeBorrowIncentives: ((_b = spokeScoped === null || spokeScoped === void 0 ? void 0 : spokeScoped.borrow) === null || _b === void 0 ? void 0 : _b.length) ? [...baseBorrow, ...spokeScoped.borrow] : baseBorrow, hubSupplyIncentives: ((_c = hubScoped === null || hubScoped === void 0 ? void 0 : hubScoped.supply) === null || _c === void 0 ? void 0 : _c.length) ? [...baseSupply, ...hubScoped.supply] : baseSupply, hubBorrowIncentives: ((_d = hubScoped === null || hubScoped === void 0 ? void 0 : hubScoped.borrow) === null || _d === void 0 ? void 0 : _d.length) ? [...baseBorrow, ...hubScoped.borrow] : baseBorrow });
90
111
  };
@@ -2,7 +2,7 @@ import { AaveV4AggregatedPositionData, AaveV4AssetsData, AaveV4ReserveAssetData,
2
2
  export declare const calcUserRiskPremiumBps: (usedAssets: AaveV4UsedReserveAssets, assetsData: AaveV4AssetsData) => number;
3
3
  /**
4
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
5
+ * intrinsic (staking) incentives plus the applicable Merkl rewards. Display surfaces should
6
6
  * use this rather than picking a scoped list directly, so badges always match the net APY math.
7
7
  */
8
8
  export declare const getAaveV4ApplicableIncentives: (assetData: AaveV4ReserveAssetData, side: IncentiveSide) => IncentiveData[];
@@ -68,21 +68,31 @@ export const calcUserRiskPremiumBps = (usedAssets, assetsData) => {
68
68
  return riskPremiumBps.toNumber();
69
69
  };
70
70
  /**
71
- * `spokeXIncentives`/`hubXIncentives` are each the intrinsic `base` list with at most one Merkl
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.
71
+ * `spokeXIncentives`/`hubXIncentives` are each the intrinsic `base` list with the Merkl rewards
72
+ * of that scope appended (see attachAaveV4MerklIncentives). Merkl publishes a hub reward stream at both
73
+ * scopes the hub (parent) campaign plus a child campaign per covered spoke (e.g. USDC borrowed
74
+ * from the Prime Hub via the Bluechip Spoke) so a child must never be summed with its parent:
75
+ * the more specific spoke listing wins. A hub reward the spoke reward is not a child of comes from
76
+ * a distinct campaign, and both genuinely accrue, so they are shown together — which is also what
77
+ * every per-asset APY badge (and Aave's own UI) shows.
77
78
  */
78
79
  const mergeScopedIncentives = (base = [], spokeScoped, hubScoped) => {
79
80
  const spokeExtras = spokeScoped ? spokeScoped.slice(base.length) : [];
80
81
  const hubExtras = hubScoped ? hubScoped.slice(base.length) : [];
81
- return [...base, ...(spokeExtras.length ? spokeExtras : hubExtras)];
82
+ const parentIds = new Set();
83
+ spokeExtras.forEach((extra) => { var _a; return (_a = extra.parentCampaignIds) === null || _a === void 0 ? void 0 : _a.forEach((id) => parentIds.add(id)); });
84
+ const distinctHubExtras = hubExtras.filter((extra) => {
85
+ var _a;
86
+ // without campaign identity the parent/child relation is unknowable — fail closed to spoke-wins
87
+ if (!((_a = extra.campaignIds) === null || _a === void 0 ? void 0 : _a.length))
88
+ return !spokeExtras.length;
89
+ return !extra.campaignIds.some((id) => parentIds.has(id));
90
+ });
91
+ return [...base, ...spokeExtras, ...distinctHubExtras];
82
92
  };
83
93
  /**
84
94
  * 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
95
+ * intrinsic (staking) incentives plus the applicable Merkl rewards. Display surfaces should
86
96
  * use this rather than picking a scoped list directly, so badges always match the net APY math.
87
97
  */
88
98
  export const getAaveV4ApplicableIncentives = (assetData, side) => (side === IncentiveSide.Supply
@@ -8,6 +8,24 @@ export declare enum OpportunityStatus {
8
8
  PAST = "PAST",
9
9
  UPCOMING = "UPCOMING"
10
10
  }
11
+ export type MerklCampaign = {
12
+ id: string;
13
+ campaignId: string;
14
+ /**
15
+ * Merkl-internal `id` of the parent campaign — set on child campaigns, which re-publish a hub
16
+ * (parent) campaign's reward scoped to a single spoke reserve. Absent on standalone campaigns,
17
+ * whose `params` still carry `hubAddress`/`hubAssetId`, so only this field tells the two apart.
18
+ */
19
+ parentCampaignId?: string;
20
+ childCampaignIds?: string[];
21
+ params?: {
22
+ reserveId?: string | number;
23
+ spokeAddress?: EthAddress;
24
+ hubAddress?: EthAddress;
25
+ hubAssetId?: string | number;
26
+ assetId?: string | number;
27
+ };
28
+ };
11
29
  export type MerklOpportunity = {
12
30
  chainId: number;
13
31
  type: string;
@@ -22,6 +40,7 @@ export type MerklOpportunity = {
22
40
  id: string;
23
41
  explorerAddress?: EthAddress;
24
42
  description?: string;
43
+ campaigns?: MerklCampaign[];
25
44
  tokens: {
26
45
  id: string;
27
46
  name: string;
@@ -73,8 +92,19 @@ export type MerkleRewardMap = Record<EthAddress, {
73
92
  supply?: MerkleRewardInfo;
74
93
  borrow?: MerkleRewardInfo;
75
94
  }>;
95
+ /**
96
+ * A scoped Aave V4 Merkl reward tagged with the campaign identity needed to resolve parent/child
97
+ * listings downstream: `campaignIds` are the Merkl-internal ids of the campaigns behind this
98
+ * entry, `parentCampaignIds` (spoke entries only) the ids of the hub campaigns those are children
99
+ * of. A hub reward is dropped only for a spoke reward that is its own child re-listing — a spoke
100
+ * reward from a distinct campaign combines with it instead.
101
+ */
102
+ export type AaveV4MerklIncentive = IncentiveData & {
103
+ campaignIds?: string[];
104
+ parentCampaignIds?: string[];
105
+ };
76
106
  export type AaveV4MerklScopedReward = {
77
- [side in IncentiveSide]?: IncentiveData;
107
+ [side in IncentiveSide]?: AaveV4MerklIncentive[];
78
108
  };
79
109
  /**
80
110
  * Fluid vault-scoped Merkl campaigns keyed by lowercase vault address — `supply` rewards apply to
@@ -86,8 +116,8 @@ export type FluidMerklRewardMap = Record<string, {
86
116
  }>;
87
117
  /**
88
118
  * Aave V4 Merkl reward campaigns split by scope:
89
- * - `hub`: keyed by `${hubAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing via a hub
90
- * - `spoke`: keyed by `${spokeAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing on a spoke
119
+ * - `hub`: keyed by `${hubAddress}_${assetId}` — rewards for supplying/borrowing via a hub asset
120
+ * - `spoke`: keyed by `${spokeAddress}_${reserveId}` — rewards for supplying/borrowing on a spoke reserve
91
121
  */
92
122
  export type AaveV4MerklRewardMap = {
93
123
  hub: Record<string, AaveV4MerklScopedReward>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defisaver/positions-sdk",
3
- "version": "2.1.154",
3
+ "version": "2.1.155",
4
4
  "description": "",
5
5
  "main": "./cjs/index.js",
6
6
  "module": "./esm/index.js",
@@ -6,6 +6,7 @@ import {
6
6
  IncentiveData,
7
7
  IncentiveKind,
8
8
  IncentiveSide,
9
+ MerklCampaign,
9
10
  MerklOpportunity,
10
11
  OpportunityAction,
11
12
  OpportunityStatus,
@@ -14,15 +15,16 @@ import {
14
15
 
15
16
  /**
16
17
  * Merkl tags Aave V4 reward campaigns by scope via the `type` field:
17
- * - AAVE_V4_HUB_SUPPLY / AAVE_V4_HUB_BORROW → reward tied to a hub (matched per hub contract + underlying)
18
- * - AAVE_V4_SPOKE_SUPPLY / AAVE_V4_SPOKE_BORROW → reward tied to a spoke (matched per spoke contract + underlying)
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.
18
+ * - AAVE_V4_HUB_SUPPLY / AAVE_V4_HUB_BORROW → reward tied to a hub asset
19
+ * - AAVE_V4_SPOKE_SUPPLY / AAVE_V4_SPOKE_BORROW → reward tied to a spoke reserve
20
+ * Embedded campaign params provide the exact on-chain identifiers. Token addresses cannot safely
21
+ * identify Aave V4 rewards because one spoke can expose the same underlying from multiple hubs.
22
+ * Each stored reward is also tagged with campaign identity (`campaignIds`/`parentCampaignIds`) so
23
+ * a hub (parent) campaign's child re-listing on a spoke can be told apart from a genuinely
24
+ * distinct spoke campaign — the former replaces the hub reward, the latter combines with it.
23
25
  */
24
26
 
25
- const scopeKey = (scopeAddress: string, underlying: string) => `${scopeAddress.toLowerCase()}_${underlying.toLowerCase()}`;
27
+ const scopeKey = (scopeAddress: string, id: string | number) => `${scopeAddress.toLowerCase()}_${id.toString()}`;
26
28
 
27
29
  const buildIncentive = (opportunity: MerklOpportunity): IncentiveData => {
28
30
  const rewardToken = opportunity.rewardsRecord?.breakdowns?.[0]?.token;
@@ -43,22 +45,35 @@ export const buildAaveV4MerklRewardMap = (opportunities: MerklOpportunity[], cha
43
45
  .filter((o) => o.status === OpportunityStatus.LIVE)
44
46
  .filter((o) => typeof o.type === 'string' && o.type.startsWith('AAVE_V4_'))
45
47
  .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
48
  const side = o.action === OpportunityAction.BORROW ? IncentiveSide.Borrow : IncentiveSide.Supply;
53
49
  const incentive = buildIncentive(o);
54
- const key = scopeKey(scopeAddress, underlying);
50
+ // one opportunity can span several campaigns (e.g. renewed periods), so campaign identity is
51
+ // collected per scope key before the reward entries are written
52
+ const idsByKey: Record<string, { campaignIds: Set<string>, parentCampaignIds: Set<string> }> = {};
53
+ const collect = (key: string, campaign: MerklCampaign) => {
54
+ if (!idsByKey[key]) idsByKey[key] = { campaignIds: new Set(), parentCampaignIds: new Set() };
55
+ if (campaign.id) idsByKey[key].campaignIds.add(campaign.id);
56
+ if (campaign.parentCampaignId) idsByKey[key].parentCampaignIds.add(campaign.parentCampaignId);
57
+ };
55
58
 
56
59
  if (o.type.includes('HUB')) {
57
- if (!result.hub[key]) result.hub[key] = {};
58
- result.hub[key][side] = incentive;
60
+ o.campaigns?.forEach((c) => {
61
+ if (!c.params?.hubAddress || c.params.assetId === undefined || c.params.assetId === null) return;
62
+ collect(scopeKey(c.params.hubAddress, c.params.assetId), c);
63
+ });
64
+ Object.entries(idsByKey).forEach(([key, ids]) => {
65
+ if (!result.hub[key]) result.hub[key] = {};
66
+ result.hub[key][side] = [...(result.hub[key][side] || []), { ...incentive, campaignIds: [...ids.campaignIds] }];
67
+ });
59
68
  } else if (o.type.includes('SPOKE')) {
60
- if (!result.spoke[key]) result.spoke[key] = {};
61
- result.spoke[key][side] = incentive;
69
+ o.campaigns?.forEach((c) => {
70
+ if (!c.params?.spokeAddress || c.params.reserveId === undefined || c.params.reserveId === null) return;
71
+ collect(scopeKey(c.params.spokeAddress, c.params.reserveId), c);
72
+ });
73
+ Object.entries(idsByKey).forEach(([key, ids]) => {
74
+ if (!result.spoke[key]) result.spoke[key] = {};
75
+ result.spoke[key][side] = [...(result.spoke[key][side] || []), { ...incentive, campaignIds: [...ids.campaignIds], parentCampaignIds: [...ids.parentCampaignIds] }];
76
+ });
62
77
  }
63
78
  });
64
79
 
@@ -71,6 +86,7 @@ export const getAaveV4MerkleCampaigns = async (chainId: NetworkNumber): Promise<
71
86
  mainProtocolId: 'aave',
72
87
  type: 'AAVE_V4_HUB_SUPPLY,AAVE_V4_HUB_BORROW,AAVE_V4_SPOKE_SUPPLY,AAVE_V4_SPOKE_BORROW',
73
88
  status: OpportunityStatus.LIVE,
89
+ campaigns: 'true',
74
90
  });
75
91
  return buildAaveV4MerklRewardMap(opportunities, chainId);
76
92
  } catch (e) {
@@ -84,18 +100,17 @@ export const getAaveV4MerkleCampaigns = async (chainId: NetworkNumber): Promise<
84
100
  * intrinsic (staking) incentives, so each surface can render base yield + the rewards that apply to it.
85
101
  */
86
102
  export const attachAaveV4MerklIncentives = (asset: AaveV4ReserveAssetData, spokeAddress: string, campaigns: AaveV4MerklRewardMap): AaveV4ReserveAssetData => {
87
- const underlying = asset.underlying?.toLowerCase();
88
103
  const baseSupply = asset.supplyIncentives || [];
89
104
  const baseBorrow = asset.borrowIncentives || [];
90
105
 
91
- const spokeScoped = (spokeAddress && underlying) ? campaigns.spoke[scopeKey(spokeAddress, underlying)] : undefined;
92
- const hubScoped = (asset.hub && underlying) ? campaigns.hub[scopeKey(asset.hub, underlying)] : undefined;
106
+ const spokeScoped = spokeAddress ? campaigns.spoke[scopeKey(spokeAddress, asset.reserveId)] : undefined;
107
+ const hubScoped = asset.hub ? campaigns.hub[scopeKey(asset.hub, asset.assetId)] : undefined;
93
108
 
94
109
  return {
95
110
  ...asset,
96
- spokeSupplyIncentives: spokeScoped?.supply ? [...baseSupply, spokeScoped.supply] : baseSupply,
97
- spokeBorrowIncentives: spokeScoped?.borrow ? [...baseBorrow, spokeScoped.borrow] : baseBorrow,
98
- hubSupplyIncentives: hubScoped?.supply ? [...baseSupply, hubScoped.supply] : baseSupply,
99
- hubBorrowIncentives: hubScoped?.borrow ? [...baseBorrow, hubScoped.borrow] : baseBorrow,
111
+ spokeSupplyIncentives: spokeScoped?.supply?.length ? [...baseSupply, ...spokeScoped.supply] : baseSupply,
112
+ spokeBorrowIncentives: spokeScoped?.borrow?.length ? [...baseBorrow, ...spokeScoped.borrow] : baseBorrow,
113
+ hubSupplyIncentives: hubScoped?.supply?.length ? [...baseSupply, ...hubScoped.supply] : baseSupply,
114
+ hubBorrowIncentives: hubScoped?.borrow?.length ? [...baseBorrow, ...hubScoped.borrow] : baseBorrow,
100
115
  };
101
116
  };
@@ -11,6 +11,7 @@ import { calculateInterestEarned } from '../../staking';
11
11
  import {
12
12
  AaveV4AggregatedPositionData,
13
13
  AaveV4AssetsData,
14
+ AaveV4MerklIncentive,
14
15
  AaveV4ReserveAssetData,
15
16
  AaveV4SpokeInfo,
16
17
  AaveV4UsedReserveAsset,
@@ -93,22 +94,33 @@ export const calcUserRiskPremiumBps = (usedAssets: AaveV4UsedReserveAssets, asse
93
94
  };
94
95
 
95
96
  /**
96
- * `spokeXIncentives`/`hubXIncentives` are each the intrinsic `base` list with at most one Merkl
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.
97
+ * `spokeXIncentives`/`hubXIncentives` are each the intrinsic `base` list with the Merkl rewards
98
+ * of that scope appended (see attachAaveV4MerklIncentives). Merkl publishes a hub reward stream at both
99
+ * scopes the hub (parent) campaign plus a child campaign per covered spoke (e.g. USDC borrowed
100
+ * from the Prime Hub via the Bluechip Spoke) so a child must never be summed with its parent:
101
+ * the more specific spoke listing wins. A hub reward the spoke reward is not a child of comes from
102
+ * a distinct campaign, and both genuinely accrue, so they are shown together — which is also what
103
+ * every per-asset APY badge (and Aave's own UI) shows.
102
104
  */
103
105
  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)];
106
+ const spokeExtras: AaveV4MerklIncentive[] = spokeScoped ? spokeScoped.slice(base.length) : [];
107
+ const hubExtras: AaveV4MerklIncentive[] = hubScoped ? hubScoped.slice(base.length) : [];
108
+
109
+ const parentIds = new Set<string>();
110
+ spokeExtras.forEach((extra) => extra.parentCampaignIds?.forEach((id) => parentIds.add(id)));
111
+
112
+ const distinctHubExtras = hubExtras.filter((extra) => {
113
+ // without campaign identity the parent/child relation is unknowable — fail closed to spoke-wins
114
+ if (!extra.campaignIds?.length) return !spokeExtras.length;
115
+ return !extra.campaignIds.some((id) => parentIds.has(id));
116
+ });
117
+
118
+ return [...base, ...spokeExtras, ...distinctHubExtras];
107
119
  };
108
120
 
109
121
  /**
110
122
  * 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
123
+ * intrinsic (staking) incentives plus the applicable Merkl rewards. Display surfaces should
112
124
  * use this rather than picking a scoped list directly, so badges always match the net APY math.
113
125
  */
114
126
  export const getAaveV4ApplicableIncentives = (assetData: AaveV4ReserveAssetData, side: IncentiveSide): IncentiveData[] => (side === IncentiveSide.Supply
@@ -11,6 +11,25 @@ export enum OpportunityStatus {
11
11
  UPCOMING = 'UPCOMING',
12
12
  }
13
13
 
14
+ export type MerklCampaign = {
15
+ id: string;
16
+ campaignId: string;
17
+ /**
18
+ * Merkl-internal `id` of the parent campaign — set on child campaigns, which re-publish a hub
19
+ * (parent) campaign's reward scoped to a single spoke reserve. Absent on standalone campaigns,
20
+ * whose `params` still carry `hubAddress`/`hubAssetId`, so only this field tells the two apart.
21
+ */
22
+ parentCampaignId?: string;
23
+ childCampaignIds?: string[];
24
+ params?: {
25
+ reserveId?: string | number;
26
+ spokeAddress?: EthAddress;
27
+ hubAddress?: EthAddress;
28
+ hubAssetId?: string | number;
29
+ assetId?: string | number;
30
+ };
31
+ };
32
+
14
33
  export type MerklOpportunity = {
15
34
  chainId: number;
16
35
  type: string;
@@ -25,6 +44,7 @@ export type MerklOpportunity = {
25
44
  id: string;
26
45
  explorerAddress?: EthAddress;
27
46
  description?: string;
47
+ campaigns?: MerklCampaign[];
28
48
  tokens: {
29
49
  id: string;
30
50
  name: string;
@@ -70,7 +90,19 @@ export type MerklOpportunity = {
70
90
  export type MerkleRewardInfo = { apy: string; rewardTokenSymbol: string, description: string, identifier: string };
71
91
  export type MerkleRewardMap = Record<EthAddress, { supply?: MerkleRewardInfo; borrow?: MerkleRewardInfo }>;
72
92
 
73
- export type AaveV4MerklScopedReward = { [side in IncentiveSide]?: IncentiveData };
93
+ /**
94
+ * A scoped Aave V4 Merkl reward tagged with the campaign identity needed to resolve parent/child
95
+ * listings downstream: `campaignIds` are the Merkl-internal ids of the campaigns behind this
96
+ * entry, `parentCampaignIds` (spoke entries only) the ids of the hub campaigns those are children
97
+ * of. A hub reward is dropped only for a spoke reward that is its own child re-listing — a spoke
98
+ * reward from a distinct campaign combines with it instead.
99
+ */
100
+ export type AaveV4MerklIncentive = IncentiveData & {
101
+ campaignIds?: string[];
102
+ parentCampaignIds?: string[];
103
+ };
104
+
105
+ export type AaveV4MerklScopedReward = { [side in IncentiveSide]?: AaveV4MerklIncentive[] };
74
106
 
75
107
  /**
76
108
  * Fluid vault-scoped Merkl campaigns keyed by lowercase vault address — `supply` rewards apply to
@@ -80,10 +112,10 @@ export type FluidMerklRewardMap = Record<string, { supply: IncentiveData[], borr
80
112
 
81
113
  /**
82
114
  * Aave V4 Merkl reward campaigns split by scope:
83
- * - `hub`: keyed by `${hubAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing via a hub
84
- * - `spoke`: keyed by `${spokeAddress}_${underlyingAddress}` (both lowercase) — rewards for supplying/borrowing on a spoke
115
+ * - `hub`: keyed by `${hubAddress}_${assetId}` — rewards for supplying/borrowing via a hub asset
116
+ * - `spoke`: keyed by `${spokeAddress}_${reserveId}` — rewards for supplying/borrowing on a spoke reserve
85
117
  */
86
118
  export type AaveV4MerklRewardMap = {
87
119
  hub: Record<string, AaveV4MerklScopedReward>;
88
120
  spoke: Record<string, AaveV4MerklScopedReward>;
89
- };
121
+ };