@defisaver/positions-sdk 2.1.154 → 2.1.156
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.
- package/cjs/aaveV4/merkl.js +47 -26
- package/cjs/helpers/aaveV4Helpers/index.d.ts +1 -1
- package/cjs/helpers/aaveV4Helpers/index.js +18 -8
- package/cjs/portfolio/index.js +6 -1
- package/cjs/types/merkl.d.ts +33 -3
- package/esm/aaveV4/merkl.js +47 -26
- package/esm/helpers/aaveV4Helpers/index.d.ts +1 -1
- package/esm/helpers/aaveV4Helpers/index.js +18 -8
- package/esm/portfolio/index.js +6 -1
- package/esm/types/merkl.d.ts +33 -3
- package/package.json +1 -1
- package/src/aaveV4/merkl.ts +40 -25
- package/src/helpers/aaveV4Helpers/index.ts +22 -10
- package/src/portfolio/index.ts +6 -1
- package/src/types/merkl.ts +36 -4
package/cjs/aaveV4/merkl.js
CHANGED
|
@@ -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
|
|
19
|
-
* - AAVE_V4_SPOKE_SUPPLY / AAVE_V4_SPOKE_BORROW → reward tied to a spoke
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 =
|
|
93
|
-
const hubScoped =
|
|
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
|
|
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
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* more specific spoke
|
|
83
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
package/cjs/portfolio/index.js
CHANGED
|
@@ -69,7 +69,12 @@ function getPortfolioData(provider_1, network_1, defaultProvider_1, addresses_1)
|
|
|
69
69
|
const liquityV2Markets = [common_1.NetworkNumber.Eth].includes(network) ? Object.values((0, markets_1.LiquityV2Markets)(network)) : [];
|
|
70
70
|
const liquityV2MarketsStaking = [common_1.NetworkNumber.Eth].includes(network) ? Object.values((0, markets_1.LiquityV2Markets)(network)).filter(market => !market.isLegacy) : [];
|
|
71
71
|
const aaveV4Spokes = Object.values((0, markets_1.AaveV4Spokes)(network)).filter((market) => market.chainIds.includes(network));
|
|
72
|
-
|
|
72
|
+
// batchSize is viem's cap on a batch's raw subcall calldata (bytes); the JSON-RPC body ends up
|
|
73
|
+
// ~4x larger (hex + aggregate3 ABI + JSON overhead) and RPC providers reject bodies over
|
|
74
|
+
// ~2.5MB with HTTP 413, so keep this small enough that no single body gets near that.
|
|
75
|
+
// 250k gives the largest body of around 1.26MB - if we bump, we can save maybe 2-3 rpc calls but scaling takes a hit
|
|
76
|
+
// (e.g. adding new markets may result in body size going over alchemy body size limit)
|
|
77
|
+
const args = [network, { batch: { multicall: { batchSize: isSim ? 2000 : 250000 } } }];
|
|
73
78
|
const client = (0, viem_1.getViemProvider)(provider, ...args);
|
|
74
79
|
const defaultClient = (0, viem_1.getViemProvider)(defaultProvider, ...args);
|
|
75
80
|
const morphoMarketsData = {};
|
package/cjs/types/merkl.d.ts
CHANGED
|
@@ -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]?:
|
|
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}_${
|
|
90
|
-
* - `spoke`: keyed by `${spokeAddress}_${
|
|
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/esm/aaveV4/merkl.js
CHANGED
|
@@ -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
|
|
16
|
-
* - AAVE_V4_SPOKE_SUPPLY / AAVE_V4_SPOKE_BORROW → reward tied to a spoke
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 =
|
|
88
|
-
const hubScoped =
|
|
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
|
|
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
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* more specific spoke
|
|
76
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
package/esm/portfolio/index.js
CHANGED
|
@@ -49,7 +49,12 @@ export function getPortfolioData(provider_1, network_1, defaultProvider_1, addre
|
|
|
49
49
|
const liquityV2Markets = [NetworkNumber.Eth].includes(network) ? Object.values(LiquityV2Markets(network)) : [];
|
|
50
50
|
const liquityV2MarketsStaking = [NetworkNumber.Eth].includes(network) ? Object.values(LiquityV2Markets(network)).filter(market => !market.isLegacy) : [];
|
|
51
51
|
const aaveV4Spokes = Object.values(AaveV4Spokes(network)).filter((market) => market.chainIds.includes(network));
|
|
52
|
-
|
|
52
|
+
// batchSize is viem's cap on a batch's raw subcall calldata (bytes); the JSON-RPC body ends up
|
|
53
|
+
// ~4x larger (hex + aggregate3 ABI + JSON overhead) and RPC providers reject bodies over
|
|
54
|
+
// ~2.5MB with HTTP 413, so keep this small enough that no single body gets near that.
|
|
55
|
+
// 250k gives the largest body of around 1.26MB - if we bump, we can save maybe 2-3 rpc calls but scaling takes a hit
|
|
56
|
+
// (e.g. adding new markets may result in body size going over alchemy body size limit)
|
|
57
|
+
const args = [network, { batch: { multicall: { batchSize: isSim ? 2000 : 250000 } } }];
|
|
53
58
|
const client = getViemProvider(provider, ...args);
|
|
54
59
|
const defaultClient = getViemProvider(defaultProvider, ...args);
|
|
55
60
|
const morphoMarketsData = {};
|
package/esm/types/merkl.d.ts
CHANGED
|
@@ -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]?:
|
|
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}_${
|
|
90
|
-
* - `spoke`: keyed by `${spokeAddress}_${
|
|
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
package/src/aaveV4/merkl.ts
CHANGED
|
@@ -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
|
|
18
|
-
* - AAVE_V4_SPOKE_SUPPLY / AAVE_V4_SPOKE_BORROW → reward tied to a spoke
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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 =
|
|
92
|
-
const hubScoped =
|
|
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
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* more specific spoke
|
|
101
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
package/src/portfolio/index.ts
CHANGED
|
@@ -74,7 +74,12 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
|
|
|
74
74
|
const aaveV4Spokes = Object.values(AaveV4Spokes(network)).filter((market) => market.chainIds.includes(network));
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
// batchSize is viem's cap on a batch's raw subcall calldata (bytes); the JSON-RPC body ends up
|
|
78
|
+
// ~4x larger (hex + aggregate3 ABI + JSON overhead) and RPC providers reject bodies over
|
|
79
|
+
// ~2.5MB with HTTP 413, so keep this small enough that no single body gets near that.
|
|
80
|
+
// 250k gives the largest body of around 1.26MB - if we bump, we can save maybe 2-3 rpc calls but scaling takes a hit
|
|
81
|
+
// (e.g. adding new markets may result in body size going over alchemy body size limit)
|
|
82
|
+
const args: [NetworkNumber, any?] = [network, { batch: { multicall: { batchSize: isSim ? 2_000 : 250_000 } } }];
|
|
78
83
|
const client = getViemProvider(provider, ...args);
|
|
79
84
|
const defaultClient = getViemProvider(defaultProvider, ...args);
|
|
80
85
|
|
package/src/types/merkl.ts
CHANGED
|
@@ -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
|
-
|
|
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}_${
|
|
84
|
-
* - `spoke`: keyed by `${spokeAddress}_${
|
|
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
|
+
};
|