@moonwell-fi/moonwell-sdk 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/_cjs/actions/governance/getDelegates.js.map +1 -1
  3. package/_cjs/actions/governance/getUserVotingPowers.js +21 -6
  4. package/_cjs/actions/governance/getUserVotingPowers.js.map +1 -1
  5. package/_cjs/actions/governance/proposals/common.js +30 -3
  6. package/_cjs/actions/governance/proposals/common.js.map +1 -1
  7. package/_cjs/actions/morpho/user-rewards/common.js +95 -199
  8. package/_cjs/actions/morpho/user-rewards/common.js.map +1 -1
  9. package/_cjs/actions/morpho/user-rewards/getMorphoUserRewards.js +46 -10
  10. package/_cjs/actions/morpho/user-rewards/getMorphoUserRewards.js.map +1 -1
  11. package/_cjs/environments/definitions/ethereum/contracts.js +12 -0
  12. package/_cjs/environments/definitions/ethereum/contracts.js.map +1 -0
  13. package/_cjs/environments/definitions/ethereum/custom.js +18 -0
  14. package/_cjs/environments/definitions/ethereum/custom.js.map +1 -0
  15. package/_cjs/environments/definitions/ethereum/environment.js +9 -5
  16. package/_cjs/environments/definitions/ethereum/environment.js.map +1 -1
  17. package/_cjs/environments/definitions/ethereum/tokens.js +12 -0
  18. package/_cjs/environments/definitions/ethereum/tokens.js.map +1 -1
  19. package/_cjs/environments/definitions/governance.js +1 -1
  20. package/_cjs/environments/definitions/governance.js.map +1 -1
  21. package/_cjs/errors/version.js +1 -1
  22. package/_cjs/index.js +5 -1
  23. package/_cjs/index.js.map +1 -1
  24. package/_esm/actions/governance/getDelegates.js +2 -0
  25. package/_esm/actions/governance/getDelegates.js.map +1 -1
  26. package/_esm/actions/governance/getUserVotingPowers.js +24 -6
  27. package/_esm/actions/governance/getUserVotingPowers.js.map +1 -1
  28. package/_esm/actions/governance/proposals/common.js +43 -2
  29. package/_esm/actions/governance/proposals/common.js.map +1 -1
  30. package/_esm/actions/morpho/user-rewards/common.js +110 -203
  31. package/_esm/actions/morpho/user-rewards/common.js.map +1 -1
  32. package/_esm/actions/morpho/user-rewards/getMorphoUserRewards.js +51 -11
  33. package/_esm/actions/morpho/user-rewards/getMorphoUserRewards.js.map +1 -1
  34. package/_esm/environments/definitions/ethereum/contracts.js +10 -0
  35. package/_esm/environments/definitions/ethereum/contracts.js.map +1 -0
  36. package/_esm/environments/definitions/ethereum/custom.js +15 -0
  37. package/_esm/environments/definitions/ethereum/custom.js.map +1 -0
  38. package/_esm/environments/definitions/ethereum/environment.js +11 -5
  39. package/_esm/environments/definitions/ethereum/environment.js.map +1 -1
  40. package/_esm/environments/definitions/ethereum/tokens.js +12 -0
  41. package/_esm/environments/definitions/ethereum/tokens.js.map +1 -1
  42. package/_esm/environments/definitions/governance.js +2 -2
  43. package/_esm/environments/definitions/governance.js.map +1 -1
  44. package/_esm/errors/version.js +1 -1
  45. package/_esm/index.js +2 -0
  46. package/_esm/index.js.map +1 -1
  47. package/_types/actions/governance/getDelegates.d.ts.map +1 -1
  48. package/_types/actions/governance/getUserVotingPowers.d.ts.map +1 -1
  49. package/_types/actions/governance/proposals/common.d.ts +15 -0
  50. package/_types/actions/governance/proposals/common.d.ts.map +1 -1
  51. package/_types/actions/morpho/user-rewards/common.d.ts +23 -0
  52. package/_types/actions/morpho/user-rewards/common.d.ts.map +1 -1
  53. package/_types/actions/morpho/user-rewards/getMorphoUserRewards.d.ts +22 -0
  54. package/_types/actions/morpho/user-rewards/getMorphoUserRewards.d.ts.map +1 -1
  55. package/_types/client/createMoonwellClient.d.ts +62 -2
  56. package/_types/client/createMoonwellClient.d.ts.map +1 -1
  57. package/_types/environments/definitions/ethereum/contracts.d.ts +4 -0
  58. package/_types/environments/definitions/ethereum/contracts.d.ts.map +1 -0
  59. package/_types/environments/definitions/ethereum/custom.d.ts +18 -0
  60. package/_types/environments/definitions/ethereum/custom.d.ts.map +1 -0
  61. package/_types/environments/definitions/ethereum/environment.d.ts +52 -2
  62. package/_types/environments/definitions/ethereum/environment.d.ts.map +1 -1
  63. package/_types/environments/definitions/ethereum/tokens.d.ts +12 -0
  64. package/_types/environments/definitions/ethereum/tokens.d.ts.map +1 -1
  65. package/_types/environments/definitions/governance.d.ts.map +1 -1
  66. package/_types/environments/index.d.ts +34 -4
  67. package/_types/environments/index.d.ts.map +1 -1
  68. package/_types/errors/version.d.ts +1 -1
  69. package/_types/index.d.ts +2 -0
  70. package/_types/index.d.ts.map +1 -1
  71. package/actions/governance/getDelegates.ts +2 -0
  72. package/actions/governance/getUserVotingPowers.ts +30 -15
  73. package/actions/governance/proposals/common.ts +56 -2
  74. package/actions/morpho/user-rewards/common.ts +133 -355
  75. package/actions/morpho/user-rewards/getMorphoUserRewards.ts +77 -12
  76. package/environments/definitions/ethereum/contracts.ts +10 -0
  77. package/environments/definitions/ethereum/custom.ts +15 -0
  78. package/environments/definitions/ethereum/environment.ts +15 -6
  79. package/environments/definitions/ethereum/tokens.ts +12 -0
  80. package/environments/definitions/governance.ts +2 -2
  81. package/errors/version.ts +1 -1
  82. package/index.ts +3 -0
  83. package/package.json +1 -1
@@ -99,6 +99,24 @@ export const isMultichainProposal = (targets?: string[]): boolean => {
99
99
  );
100
100
  };
101
101
 
102
+ /**
103
+ * Routes a proposal to the multichain governor when:
104
+ * - its targets include the Wormhole bridge (legacy detection), OR
105
+ * - its proposalId is past the legacy Artemis governor's `proposalCount`,
106
+ * which means it could only have been created on the multichain governor
107
+ * (proposals migrated to the multichain governor after the cutoff but
108
+ * can have local-only targets, e.g. Moonbeam-internal contract calls).
109
+ *
110
+ * `legacyArtemisMaxId === 0` indicates the count read failed; in that case we
111
+ * fall back to the targets-only heuristic.
112
+ */
113
+ export const isMultichainAware = (
114
+ proposal: { targets?: string[]; proposalId: number },
115
+ legacyArtemisMaxId: number,
116
+ ): boolean =>
117
+ isMultichainProposal(proposal.targets) ||
118
+ (legacyArtemisMaxId > 0 && proposal.proposalId > legacyArtemisMaxId);
119
+
102
120
  export type ApiProposalFormatted = {
103
121
  forVotes: Amount;
104
122
  againstVotes: Amount;
@@ -181,6 +199,40 @@ export type ProposalOnChainData = {
181
199
  quorum: bigint;
182
200
  };
183
201
 
202
+ // Cached per chain: highest proposalId held by the legacy Artemis governor.
203
+ // Anything with a higher proposalId belongs to the multichain governor, even
204
+ // if its targets don't include the Wormhole bridge. The legacy governor only
205
+ // receives new proposals during chain migrations, so a 5-minute TTL is plenty.
206
+ const LEGACY_ARTEMIS_MAX_ID_TTL_MS = 5 * 60 * 1000;
207
+ const legacyArtemisMaxIdCache = new Map<
208
+ number,
209
+ { value: number; fetchedAt: number }
210
+ >();
211
+
212
+ const getLegacyArtemisMaxId = async (
213
+ governanceEnvironment: Environment,
214
+ ): Promise<number> => {
215
+ const governor = governanceEnvironment.contracts.governor;
216
+ if (!governor) return 0;
217
+
218
+ const cached = legacyArtemisMaxIdCache.get(governanceEnvironment.chainId);
219
+ if (cached && Date.now() - cached.fetchedAt < LEGACY_ARTEMIS_MAX_ID_TTL_MS) {
220
+ return cached.value;
221
+ }
222
+
223
+ try {
224
+ const value = Number(await governor.read.proposalCount());
225
+ legacyArtemisMaxIdCache.set(governanceEnvironment.chainId, {
226
+ value,
227
+ fetchedAt: Date.now(),
228
+ });
229
+ return value;
230
+ } catch (error) {
231
+ console.warn("Failed to fetch legacy governor proposalCount:", error);
232
+ return cached?.value ?? 0;
233
+ }
234
+ };
235
+
184
236
  /**
185
237
  * Fetches on-chain data for multiple proposals
186
238
  */
@@ -201,9 +253,11 @@ export const getProposalsOnChainData = async (
201
253
  }
202
254
  }
203
255
 
256
+ const legacyArtemisMaxId = await getLegacyArtemisMaxId(governanceEnvironment);
257
+
204
258
  const onChainDataList = await Promise.all(
205
259
  apiProposals.map(async (p) => {
206
- const isMultichain = isMultichainProposal(p.targets);
260
+ const isMultichain = isMultichainAware(p, legacyArtemisMaxId);
207
261
 
208
262
  const governorContract = isMultichain
209
263
  ? governanceEnvironment.contracts.multichainGovernor
@@ -242,7 +296,7 @@ export const getProposalsOnChainData = async (
242
296
 
243
297
  const votesCollectedList = await Promise.all(
244
298
  apiProposals.map(async (apiProposal) => {
245
- const isMultichain = isMultichainProposal(apiProposal.targets);
299
+ const isMultichain = isMultichainAware(apiProposal, legacyArtemisMaxId);
246
300
 
247
301
  if (
248
302
  !isMultichain ||
@@ -1,5 +1,3 @@
1
- import lodash from "lodash";
2
- const { uniq } = lodash;
3
1
  import { type Address, getContract, parseAbi, zeroAddress } from "viem";
4
2
  import { Amount } from "../../../common/amount.js";
5
3
  import { MOONWELL_FETCH_JSON_HEADERS } from "../../../common/fetch-headers.js";
@@ -14,258 +12,90 @@ import {
14
12
  } from "../../../environments/utils/index.js";
15
13
  import type { MorphoUserReward } from "../../../types/morphoUserReward.js";
16
14
  import type { MorphoUserStakingReward } from "../../../types/morphoUserStakingReward.js";
17
- import { getGraphQL } from "../utils/graphql.js";
15
+
16
+ /**
17
+ * Error thrown for any failure communicating with the Merkl API: non-ok HTTP
18
+ * responses, network rejections (fetch threw), and response-body parse errors.
19
+ *
20
+ * - HTTP failures populate `status` and `statusText`.
21
+ * - Network and parse failures leave `status`/`statusText` undefined and
22
+ * carry the original error via `cause`.
23
+ */
24
+ export class MerklApiError extends Error {
25
+ readonly status: number | undefined;
26
+ readonly statusText: string | undefined;
27
+ readonly url: string;
28
+ readonly chainId: number;
29
+
30
+ constructor(params: {
31
+ message: string;
32
+ url: string;
33
+ chainId: number;
34
+ status?: number | undefined;
35
+ statusText?: string | undefined;
36
+ cause?: unknown;
37
+ }) {
38
+ super(
39
+ params.message,
40
+ params.cause !== undefined ? { cause: params.cause } : undefined,
41
+ );
42
+ this.name = "MerklApiError";
43
+ this.url = params.url;
44
+ this.chainId = params.chainId;
45
+ this.status = params.status;
46
+ this.statusText = params.statusText;
47
+ }
48
+ }
18
49
 
19
50
  export async function getUserMorphoRewardsData(params: {
20
51
  environment: Environment;
21
52
  account: `0x${string}`;
53
+ throwOnExternalApiError?: boolean;
22
54
  }): Promise<MorphoUserReward[]> {
55
+ // The Morpho URD distributions endpoint (rewards.morpho.org) was
56
+ // deprecated and now 301-redirects to a SPA, so JSON parsing fails.
57
+ // Surface only Merkl rewards.
58
+ const merklRewards = await getMerklRewardsData(
59
+ params.environment,
60
+ params.account,
61
+ { throwOnError: params.throwOnExternalApiError ?? false },
62
+ );
63
+
23
64
  const isFullDeployment =
24
65
  params.environment.custom.morpho?.minimalDeployment === false;
25
66
 
26
- const emptyMorphoRewards: MorphoRewardsResponse[] = [];
27
- const [merklRewards, morphoRewards] = await Promise.all([
28
- getMerklRewardsData(params.environment, params.account),
29
- isFullDeployment
30
- ? getMorphoRewardsData(params.environment, params.account)
31
- : Promise.resolve(emptyMorphoRewards),
32
- ]);
33
-
34
- if (isFullDeployment) {
35
- // Process Morpho rewards (GraphQL query depends on morphoRewards result)
36
- const morphoAssets = await getMorphoAssetsData(
37
- params.environment,
38
- morphoRewards.map((r) => r.asset.address),
39
- );
40
-
41
- const morphoResult: (MorphoUserReward | undefined)[] = morphoRewards.map(
42
- (r) => {
43
- const asset = morphoAssets.find(
44
- (a) => a.address.toLowerCase() === r.asset.address.toLowerCase(),
45
- );
46
-
47
- if (!asset) {
48
- return undefined;
49
- }
50
-
51
- const rewardToken: TokenConfig = {
52
- address: asset.address,
53
- decimals: asset.decimals,
54
- symbol: asset.symbol,
55
- name: asset.name,
56
- };
57
-
58
- switch (r.type) {
59
- case "uniform-reward": {
60
- const claimableNow = new Amount(
61
- BigInt(r.amount?.claimable_now || 0),
62
- rewardToken.decimals,
63
- );
64
- const claimableNowUsd = claimableNow.value * (asset.priceUsd || 0);
65
- const claimableFuture = new Amount(
66
- BigInt(r.amount?.claimable_next || 0),
67
- rewardToken.decimals,
68
- );
69
- const claimableFutureUsd =
70
- claimableFuture.value * (asset.priceUsd || 0);
71
-
72
- const uniformReward: MorphoUserReward = {
73
- type: "uniform-reward",
74
- chainId: r.asset.chain_id,
75
- account: r.user,
76
- rewardToken,
77
- claimableNow,
78
- claimableNowUsd,
79
- claimableFuture,
80
- claimableFutureUsd,
81
- };
82
- return uniformReward;
83
- }
84
-
85
- case "market-reward": {
86
- const claimableNow = new Amount(
87
- BigInt(r.for_supply?.claimable_now || 0),
88
- rewardToken.decimals,
89
- );
90
- const claimableNowUsd = claimableNow.value * (asset.priceUsd || 0);
91
-
92
- const claimableFuture = new Amount(
93
- BigInt(r.for_supply?.claimable_next || 0),
94
- rewardToken.decimals,
95
- );
96
- const claimableFutureUsd =
97
- claimableFuture.value * (asset.priceUsd || 0);
98
-
99
- const collateralClaimableNow = new Amount(
100
- BigInt(r.for_collateral?.claimable_now || 0),
101
- rewardToken.decimals,
102
- );
103
- const collateralClaimableNowUsd =
104
- collateralClaimableNow.value * (asset.priceUsd || 0);
105
- const collateralClaimableFuture = new Amount(
106
- BigInt(r.for_collateral?.claimable_next || 0),
107
- rewardToken.decimals,
108
- );
109
- const collateralClaimableFutureUsd =
110
- collateralClaimableFuture.value * (asset.priceUsd || 0);
111
-
112
- const borrowClaimableNow = new Amount(
113
- BigInt(r.for_borrow?.claimable_now || 0),
114
- rewardToken.decimals,
115
- );
116
- const borrowClaimableNowUsd =
117
- borrowClaimableNow.value * (asset.priceUsd || 0);
118
- const borrowClaimableFuture = new Amount(
119
- BigInt(r.for_borrow?.claimable_next || 0),
120
- rewardToken.decimals,
121
- );
122
- const borrowClaimableFutureUsd =
123
- borrowClaimableFuture.value * (asset.priceUsd || 0);
124
-
125
- //Rewards reallocated to vaults are reported as vault rewards
126
- if (r.reallocated_from) {
127
- const vaultReward: MorphoUserReward = {
128
- type: "vault-reward",
129
- chainId: r.program.chain_id,
130
- account: r.user,
131
- vaultId: r.reallocated_from,
132
- rewardToken,
133
- claimableNow,
134
- claimableNowUsd,
135
- claimableFuture,
136
- claimableFutureUsd,
137
- };
138
- return vaultReward;
139
- } else {
140
- const marketReward: MorphoUserReward = {
141
- type: "market-reward",
142
- chainId: r.program.chain_id,
143
- account: r.user,
144
- marketId: r.program.market_id || "",
145
- rewardToken,
146
- collateralRewards: {
147
- claimableNow: collateralClaimableNow,
148
- claimableNowUsd: collateralClaimableNowUsd,
149
- claimableFuture: collateralClaimableFuture,
150
- claimableFutureUsd: collateralClaimableFutureUsd,
151
- },
152
- borrowRewards: {
153
- claimableNow: borrowClaimableNow,
154
- claimableNowUsd: borrowClaimableNowUsd,
155
- claimableFuture: borrowClaimableFuture,
156
- claimableFutureUsd: borrowClaimableFutureUsd,
157
- },
158
- };
159
- return marketReward;
160
- }
161
- }
162
- case "vault-reward": {
163
- const claimableNow = new Amount(
164
- BigInt(r.for_supply?.claimable_now || 0),
165
- rewardToken.decimals,
166
- );
167
- const claimableNowUsd = claimableNow.value * (asset.priceUsd || 0);
168
- const claimableFuture = new Amount(
169
- BigInt(r.for_supply?.claimable_next || 0),
170
- rewardToken.decimals,
171
- );
172
- const claimableFutureUsd =
173
- claimableFuture.value * (asset.priceUsd || 0);
174
-
175
- const vaultReward: MorphoUserReward = {
176
- type: "vault-reward",
177
- chainId: r.program.chain_id,
178
- account: r.user,
179
- vaultId: r.program.vault,
180
- rewardToken,
181
- claimableNow,
182
- claimableNowUsd,
183
- claimableFuture,
184
- claimableFutureUsd,
185
- };
186
-
187
- return vaultReward;
188
- }
189
- }
190
- },
191
- );
192
-
193
- // Process Merkl rewards
194
- const vaultCampaignIds = new Set<string>(
195
- (Object.values(publicEnvironments) as Environment[]).flatMap(
196
- (environment) =>
197
- Object.values(environment.config.vaults ?? {})
198
- .map((vault) => vault.campaignId)
199
- .filter((id): id is string => id !== undefined),
200
- ),
67
+ // For full deployments (Base), restrict to Moonwell vault campaigns so the
68
+ // result excludes staking and other Moonwell campaigns; those are returned
69
+ // by their own actions (e.g. getUserStakingInfo). On other chains, surface
70
+ // every Merkl reward we get back.
71
+ const vaultCampaignIds = isFullDeployment
72
+ ? new Set<string>(
73
+ (Object.values(publicEnvironments) as Environment[]).flatMap(
74
+ (environment) =>
75
+ Object.values(environment.config.vaults ?? {})
76
+ .map((vault) => vault.campaignId)
77
+ .filter((id): id is string => id !== undefined),
78
+ ),
79
+ )
80
+ : null;
81
+
82
+ const sumBreakdowns = (
83
+ breakdowns: {
84
+ campaignId: string;
85
+ amount: string;
86
+ claimed: string;
87
+ pending: string;
88
+ }[],
89
+ field: "amount" | "claimed" | "pending",
90
+ ): bigint =>
91
+ breakdowns.reduce(
92
+ (acc, curr) =>
93
+ vaultCampaignIds === null || vaultCampaignIds.has(curr.campaignId)
94
+ ? acc + BigInt(curr[field])
95
+ : acc,
96
+ 0n,
201
97
  );
202
98
 
203
- const getVaultRewardAmount = (
204
- breakdowns: any[],
205
- field: "amount" | "claimed" | "pending",
206
- ) => {
207
- return breakdowns.reduce(
208
- (acc, curr) =>
209
- vaultCampaignIds.has(curr.campaignId)
210
- ? acc + BigInt(curr[field])
211
- : acc,
212
- 0n,
213
- );
214
- };
215
-
216
- const merklResult: MorphoUserReward[] = [];
217
-
218
- for (const chainData of merklRewards) {
219
- for (const reward of chainData.rewards) {
220
- // Try to find token info in morphoAssets first
221
- const morphoAsset = morphoAssets.find(
222
- (a) => a.address.toLowerCase() === reward.token.address.toLowerCase(),
223
- );
224
-
225
- const rewardToken: TokenConfig = {
226
- address: reward.token.address as Address,
227
- decimals: morphoAsset?.decimals ?? reward.token.decimals,
228
- symbol: morphoAsset?.symbol ?? reward.token.symbol,
229
- name: morphoAsset?.name ?? reward.token.symbol,
230
- };
231
-
232
- const amount = getVaultRewardAmount(reward.breakdowns, "amount");
233
- const claimed = getVaultRewardAmount(reward.breakdowns, "claimed");
234
- const pending = getVaultRewardAmount(reward.breakdowns, "pending");
235
-
236
- const claimableNow = new Amount(amount - claimed, rewardToken.decimals);
237
- const claimableNowUsd =
238
- claimableNow.value *
239
- (morphoAsset?.priceUsd ?? reward.token.price ?? 0);
240
- const claimableFuture = new Amount(pending, rewardToken.decimals);
241
- const claimableFutureUsd =
242
- claimableFuture.value *
243
- (morphoAsset?.priceUsd ?? reward.token.price ?? 0);
244
-
245
- const merklReward: MorphoUserReward = {
246
- type: "merkl-reward",
247
- chainId: chainData.chain.id,
248
- account: params.account,
249
- rewardToken,
250
- claimableNow,
251
- claimableNowUsd,
252
- claimableFuture,
253
- claimableFutureUsd,
254
- };
255
-
256
- merklResult.push(merklReward);
257
- }
258
- }
259
-
260
- // Combine both results
261
- const allResults = [
262
- ...(morphoResult.filter((r) => r !== undefined) as MorphoUserReward[]),
263
- ...merklResult,
264
- ];
265
-
266
- return allResults;
267
- }
268
-
269
99
  const merklResult: MorphoUserReward[] = [];
270
100
 
271
101
  for (const chainData of merklRewards) {
@@ -277,19 +107,23 @@ export async function getUserMorphoRewardsData(params: {
277
107
  name: reward.token.symbol,
278
108
  };
279
109
 
280
- const claimableNow = new Amount(
281
- BigInt(reward.amount) - BigInt(reward.claimed),
282
- rewardToken.decimals,
283
- );
110
+ const amount = vaultCampaignIds
111
+ ? sumBreakdowns(reward.breakdowns, "amount")
112
+ : BigInt(reward.amount);
113
+ const claimed = vaultCampaignIds
114
+ ? sumBreakdowns(reward.breakdowns, "claimed")
115
+ : BigInt(reward.claimed);
116
+ const pending = vaultCampaignIds
117
+ ? sumBreakdowns(reward.breakdowns, "pending")
118
+ : BigInt(reward.pending);
119
+
120
+ const claimableNow = new Amount(amount - claimed, rewardToken.decimals);
284
121
  const claimableNowUsd = claimableNow.value * (reward.token.price ?? 0);
285
- const claimableFuture = new Amount(
286
- BigInt(reward.pending),
287
- rewardToken.decimals,
288
- );
122
+ const claimableFuture = new Amount(pending, rewardToken.decimals);
289
123
  const claimableFutureUsd =
290
124
  claimableFuture.value * (reward.token.price ?? 0);
291
125
 
292
- const merklReward: MorphoUserReward = {
126
+ merklResult.push({
293
127
  type: "merkl-reward",
294
128
  chainId: chainData.chain.id,
295
129
  account: params.account,
@@ -298,9 +132,7 @@ export async function getUserMorphoRewardsData(params: {
298
132
  claimableNowUsd,
299
133
  claimableFuture,
300
134
  claimableFutureUsd,
301
- };
302
-
303
- merklResult.push(merklReward);
135
+ });
304
136
  }
305
137
  }
306
138
 
@@ -456,94 +288,6 @@ const getRewardsEarnedData = async (
456
288
  return rewards.filter(Boolean);
457
289
  };
458
290
 
459
- type MorphoRewardsResponse = {
460
- user: Address;
461
- for_borrow: {
462
- claimable_next: string;
463
- claimable_now: string;
464
- claimed: string;
465
- total: string;
466
- };
467
- for_collateral: {
468
- claimable_next: string;
469
- claimable_now: string;
470
- claimed: string;
471
- total: string;
472
- };
473
- for_supply: {
474
- claimable_next: string;
475
- claimable_now: string;
476
- claimed: string;
477
- total: string;
478
- };
479
- program: {
480
- asset: { address: Address };
481
- market_id?: string;
482
- chain_id: number;
483
- vault: Address;
484
- };
485
- asset: { address: Address; chain_id: number };
486
- amount?: { claimable_next: string; claimable_now: string };
487
- type: "vault-reward" | "market-reward" | "uniform-reward";
488
- reallocated_from: Address;
489
- };
490
-
491
- type MorphoAssetResponse = {
492
- address: Address;
493
- symbol: string;
494
- priceUsd: number | undefined;
495
- name: string;
496
- decimals: number;
497
- };
498
-
499
- async function getMorphoRewardsData(
500
- environment: Environment,
501
- account: Address,
502
- ): Promise<MorphoRewardsResponse[]> {
503
- const baseUrl =
504
- environment.custom.morpho?.rewardsApiUrl || "https://rewards.morpho.org";
505
- const rewardsRequest = await fetch(
506
- `${baseUrl}/v1/users/${account}/rewards?chain_id=${environment.chainId}&trusted=true&exclude_merkl_programs=true`,
507
- {
508
- headers: MOONWELL_FETCH_JSON_HEADERS,
509
- },
510
- );
511
- const rewards = await rewardsRequest.json();
512
- return (rewards.data || []) as MorphoRewardsResponse[];
513
- }
514
-
515
- async function getMorphoAssetsData(
516
- environment: Environment,
517
- addresses: Address[],
518
- ): Promise<MorphoAssetResponse[]> {
519
- const rewardsRequest = await getGraphQL<{
520
- assets: {
521
- items: MorphoAssetResponse[];
522
- };
523
- }>(
524
- environment,
525
- `
526
- query {
527
- assets(where: { address_in:[${uniq(addresses)
528
- .map((a: string) => `"${a.toLowerCase()}"`)
529
- .join(",")}]}) {
530
- items {
531
- address
532
- symbol
533
- priceUsd
534
- name
535
- decimals
536
- }
537
- }
538
- }
539
- `,
540
- );
541
- if (rewardsRequest) {
542
- return rewardsRequest.assets.items;
543
- }
544
- return [];
545
- }
546
-
547
291
  type MerklRewardsResponse = {
548
292
  chain: {
549
293
  id: number;
@@ -586,30 +330,64 @@ type MerklRewardsResponse = {
586
330
  async function getMerklRewardsData(
587
331
  environment: Environment,
588
332
  account: Address,
333
+ options: { throwOnError: boolean } = { throwOnError: false },
589
334
  ): Promise<MerklRewardsResponse[]> {
335
+ const url = `https://api.merkl.xyz/v4/users/${account}/rewards?chainId=${environment.chainId}&test=false&breakdownPage=0&reloadChainId=${environment.chainId}`;
336
+
337
+ let response: Response;
590
338
  try {
591
339
  // Merkl campaigns always distribute rewards on the same chain as the
592
340
  // opportunity, so environment.chainId is the only chain we need to query.
593
341
  // The previous two-phase approach (fetch opportunities per vault → extract
594
342
  // chain IDs → fetch rewards per chain) made N+1 HTTP calls to discover
595
343
  // a chain ID we already know.
596
- const response = await fetch(
597
- `https://api.merkl.xyz/v4/users/${account}/rewards?chainId=${environment.chainId}&test=false&breakdownPage=0&reloadChainId=${environment.chainId}`,
598
- {
599
- headers: MOONWELL_FETCH_JSON_HEADERS,
600
- },
344
+ response = await fetch(url, { headers: MOONWELL_FETCH_JSON_HEADERS });
345
+ } catch (error) {
346
+ if (options.throwOnError) {
347
+ throw new MerklApiError({
348
+ message: `Merkl API network error for chain ${environment.chainId}`,
349
+ url,
350
+ chainId: environment.chainId,
351
+ cause: error,
352
+ });
353
+ }
354
+ console.error(
355
+ `[getMerklRewardsData:network] chain=${environment.chainId} url=${url}`,
356
+ error,
601
357
  );
358
+ return [];
359
+ }
602
360
 
603
- if (!response.ok) {
604
- console.warn(
605
- `Merkl API request failed: ${response.status} ${response.statusText}`,
606
- );
607
- return [];
361
+ if (!response.ok) {
362
+ const message = `Merkl API request failed for chain ${environment.chainId}: ${response.status} ${response.statusText}`;
363
+ if (options.throwOnError) {
364
+ throw new MerklApiError({
365
+ message,
366
+ url,
367
+ chainId: environment.chainId,
368
+ status: response.status,
369
+ statusText: response.statusText,
370
+ });
608
371
  }
372
+ console.warn(`${message} (url=${url})`);
373
+ return [];
374
+ }
609
375
 
376
+ try {
610
377
  return (await response.json()) as MerklRewardsResponse[];
611
378
  } catch (error) {
612
- console.error("Error in getMerklRewardsData:", error);
379
+ if (options.throwOnError) {
380
+ throw new MerklApiError({
381
+ message: `Merkl API response parse error for chain ${environment.chainId}`,
382
+ url,
383
+ chainId: environment.chainId,
384
+ cause: error,
385
+ });
386
+ }
387
+ console.error(
388
+ `[getMerklRewardsData:parse] chain=${environment.chainId} url=${url}`,
389
+ error,
390
+ );
613
391
  return [];
614
392
  }
615
393
  }