@moonwell-fi/moonwell-sdk 0.24.0 → 0.25.1

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 (91) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/_cjs/actions/beam/getBeamQuote.js +35 -2
  3. package/_cjs/actions/beam/getBeamQuote.js.map +1 -1
  4. package/_cjs/actions/core/getUserBalances.js +63 -32
  5. package/_cjs/actions/core/getUserBalances.js.map +1 -1
  6. package/_cjs/actions/core/markets/common.js +14 -12
  7. package/_cjs/actions/core/markets/common.js.map +1 -1
  8. package/_cjs/actions/governance/getStakingInfo.js +1 -7
  9. package/_cjs/actions/governance/getStakingInfo.js.map +1 -1
  10. package/_cjs/actions/governance/getUserStakingInfo.js +1 -7
  11. package/_cjs/actions/governance/getUserStakingInfo.js.map +1 -1
  12. package/_cjs/actions/governance/getWellPrice.js +51 -1
  13. package/_cjs/actions/governance/getWellPrice.js.map +1 -1
  14. package/_cjs/actions/governance/proposals/common.js +34 -10
  15. package/_cjs/actions/governance/proposals/common.js.map +1 -1
  16. package/_cjs/actions/governance/proposals/getProposals.js +5 -4
  17. package/_cjs/actions/governance/proposals/getProposals.js.map +1 -1
  18. package/_cjs/actions/morpho/markets/common.js +32 -2
  19. package/_cjs/actions/morpho/markets/common.js.map +1 -1
  20. package/_cjs/actions/morpho/markets/lunarIndexerTransform.js +3 -0
  21. package/_cjs/actions/morpho/markets/lunarIndexerTransform.js.map +1 -1
  22. package/_cjs/actions/morpho/user-rewards/common.js +1 -7
  23. package/_cjs/actions/morpho/user-rewards/common.js.map +1 -1
  24. package/_cjs/actions/morpho/vaults/common.js +104 -87
  25. package/_cjs/actions/morpho/vaults/common.js.map +1 -1
  26. package/_cjs/actions/morpho/vaults/lunarIndexerTransform.js +69 -21
  27. package/_cjs/actions/morpho/vaults/lunarIndexerTransform.js.map +1 -1
  28. package/_cjs/environments/types/config.js.map +1 -1
  29. package/_cjs/errors/version.js +1 -1
  30. package/_esm/actions/beam/getBeamQuote.js +5 -1
  31. package/_esm/actions/beam/getBeamQuote.js.map +1 -1
  32. package/_esm/actions/core/getUserBalances.js +72 -35
  33. package/_esm/actions/core/getUserBalances.js.map +1 -1
  34. package/_esm/actions/core/markets/common.js +27 -13
  35. package/_esm/actions/core/markets/common.js.map +1 -1
  36. package/_esm/actions/governance/getStakingInfo.js +2 -8
  37. package/_esm/actions/governance/getStakingInfo.js.map +1 -1
  38. package/_esm/actions/governance/getUserStakingInfo.js +2 -8
  39. package/_esm/actions/governance/getUserStakingInfo.js.map +1 -1
  40. package/_esm/actions/governance/getWellPrice.js +67 -7
  41. package/_esm/actions/governance/getWellPrice.js.map +1 -1
  42. package/_esm/actions/governance/proposals/common.js +37 -10
  43. package/_esm/actions/governance/proposals/common.js.map +1 -1
  44. package/_esm/actions/governance/proposals/getProposals.js +7 -5
  45. package/_esm/actions/governance/proposals/getProposals.js.map +1 -1
  46. package/_esm/actions/morpho/markets/common.js +40 -2
  47. package/_esm/actions/morpho/markets/common.js.map +1 -1
  48. package/_esm/actions/morpho/markets/lunarIndexerTransform.js +5 -0
  49. package/_esm/actions/morpho/markets/lunarIndexerTransform.js.map +1 -1
  50. package/_esm/actions/morpho/user-rewards/common.js +2 -8
  51. package/_esm/actions/morpho/user-rewards/common.js.map +1 -1
  52. package/_esm/actions/morpho/vaults/common.js +110 -91
  53. package/_esm/actions/morpho/vaults/common.js.map +1 -1
  54. package/_esm/actions/morpho/vaults/lunarIndexerTransform.js +97 -22
  55. package/_esm/actions/morpho/vaults/lunarIndexerTransform.js.map +1 -1
  56. package/_esm/environments/types/config.js.map +1 -1
  57. package/_esm/errors/version.js +1 -1
  58. package/_types/actions/beam/getBeamQuote.d.ts.map +1 -1
  59. package/_types/actions/core/getUserBalances.d.ts.map +1 -1
  60. package/_types/actions/core/markets/common.d.ts.map +1 -1
  61. package/_types/actions/governance/getStakingInfo.d.ts.map +1 -1
  62. package/_types/actions/governance/getUserStakingInfo.d.ts.map +1 -1
  63. package/_types/actions/governance/getWellPrice.d.ts +9 -1
  64. package/_types/actions/governance/getWellPrice.d.ts.map +1 -1
  65. package/_types/actions/governance/proposals/common.d.ts.map +1 -1
  66. package/_types/actions/governance/proposals/getProposals.d.ts +10 -1
  67. package/_types/actions/governance/proposals/getProposals.d.ts.map +1 -1
  68. package/_types/actions/morpho/markets/common.d.ts.map +1 -1
  69. package/_types/actions/morpho/markets/lunarIndexerTransform.d.ts.map +1 -1
  70. package/_types/actions/morpho/user-rewards/common.d.ts.map +1 -1
  71. package/_types/actions/morpho/vaults/common.d.ts.map +1 -1
  72. package/_types/actions/morpho/vaults/lunarIndexerTransform.d.ts.map +1 -1
  73. package/_types/environments/types/config.d.ts +17 -1
  74. package/_types/environments/types/config.d.ts.map +1 -1
  75. package/_types/errors/version.d.ts +1 -1
  76. package/actions/beam/getBeamQuote.ts +5 -1
  77. package/actions/core/getUserBalances.ts +98 -51
  78. package/actions/core/markets/common.ts +44 -18
  79. package/actions/governance/getStakingInfo.ts +2 -9
  80. package/actions/governance/getUserStakingInfo.ts +2 -8
  81. package/actions/governance/getWellPrice.ts +99 -7
  82. package/actions/governance/proposals/common.ts +44 -16
  83. package/actions/governance/proposals/getProposals.ts +22 -5
  84. package/actions/morpho/markets/common.ts +45 -2
  85. package/actions/morpho/markets/lunarIndexerTransform.ts +7 -0
  86. package/actions/morpho/user-rewards/common.ts +2 -8
  87. package/actions/morpho/vaults/common.ts +161 -127
  88. package/actions/morpho/vaults/lunarIndexerTransform.ts +117 -36
  89. package/environments/types/config.ts +17 -1
  90. package/errors/version.ts +1 -1
  91. package/package.json +2 -2
@@ -6,7 +6,7 @@ import type { OptionalNetworkParameterType } from "../../common/types.js";
6
6
  import type { Environment } from "../../environments/index.js";
7
7
  import type { UserStakingInfo } from "../../types/staking.js";
8
8
  import { getMerklCampaignIds, getMerklRewardsData } from "./common.js";
9
- import { getGovernanceTokenPriceFor } from "./getWellPrice.js";
9
+ import { getGovernanceTokenPriceOrZero } from "./getWellPrice.js";
10
10
 
11
11
  export type GetUserStakingInfoParameters<
12
12
  environments,
@@ -150,7 +150,7 @@ export async function getUserStakingInfo<
150
150
  environment.contracts.views?.read.getUserStakingInfo([userAddress]),
151
151
  environment.contracts.governanceToken?.read.balanceOf([userAddress]),
152
152
  environment.contracts.views?.read.getStakingInfo(),
153
- getGovernanceTokenPriceFor(environment, baseEnvironment),
153
+ getGovernanceTokenPriceOrZero(environment, baseEnvironment),
154
154
  ]);
155
155
 
156
156
  const [userStakingR, balanceR, stakingScheduleR, priceR] = settled;
@@ -175,12 +175,6 @@ export async function getUserStakingInfo<
175
175
  : 0n;
176
176
 
177
177
  const price = priceR.status === "fulfilled" ? priceR.value : 0n;
178
- if (priceR.status === "rejected") {
179
- environment.onError?.(priceR.reason, {
180
- source: "governance-token-price",
181
- chainId: environment.chainId,
182
- });
183
- }
184
178
 
185
179
  return { userStaking, schedule, tokenBalance, price };
186
180
  }),
@@ -41,6 +41,71 @@ export async function getWellPriceFromBaseOracle(
41
41
  return await oracle.read.getUnderlyingPrice([mWELL]);
42
42
  }
43
43
 
44
+ async function readGovernanceTokenPrice(
45
+ environment: Environment,
46
+ baseEnvironment?: Environment,
47
+ ): Promise<bigint> {
48
+ if (environment.custom?.governance?.token === "WELL") {
49
+ return getWellPriceFromBaseOracle(baseEnvironment);
50
+ }
51
+ // Non-WELL (e.g. Moonriver / MFAM): the env itself is the governance "home".
52
+ const views = environment.contracts.views;
53
+ if (!views) return 0n;
54
+ return (await views.read.getGovernanceTokenPrice()) ?? 0n;
55
+ }
56
+
57
+ const GOVERNANCE_TOKEN_PRICE_TTL_MS = 10_000;
58
+
59
+ type OnErrorCallback = NonNullable<Environment["onError"]>;
60
+
61
+ type GovernanceTokenPriceEntry = {
62
+ promise: Promise<bigint>;
63
+ expiresAt: number;
64
+ requestingChainIds: Set<number>;
65
+ /** Callbacks already told about this read's failure. */
66
+ reportedTo: Set<OnErrorCallback>;
67
+ };
68
+
69
+ // Keyed by the environment the price is read through (the Base env for every
70
+ // WELL chain), so markets, vaults, rewards and staking reads issued together
71
+ // share one request, while clients with their own RPCs never share results.
72
+ const governanceTokenPriceCache = new WeakMap<
73
+ Environment,
74
+ GovernanceTokenPriceEntry
75
+ >();
76
+
77
+ function getSharedGovernanceTokenPrice(
78
+ environment: Environment,
79
+ baseEnvironment?: Environment,
80
+ ): { sourceChainId: number; entry: GovernanceTokenPriceEntry } {
81
+ const sourceEnvironment: Environment =
82
+ environment.custom?.governance?.token === "WELL"
83
+ ? (baseEnvironment ?? publicEnvironments.base)
84
+ : environment;
85
+ const sourceChainId = sourceEnvironment.chainId;
86
+
87
+ const cached = governanceTokenPriceCache.get(sourceEnvironment);
88
+ if (cached && Date.now() < cached.expiresAt) {
89
+ cached.requestingChainIds.add(environment.chainId);
90
+ return { sourceChainId, entry: cached };
91
+ }
92
+
93
+ const entry: GovernanceTokenPriceEntry = {
94
+ promise: readGovernanceTokenPrice(environment, baseEnvironment),
95
+ expiresAt: Date.now() + GOVERNANCE_TOKEN_PRICE_TTL_MS,
96
+ requestingChainIds: new Set([environment.chainId]),
97
+ reportedTo: new Set(),
98
+ };
99
+ governanceTokenPriceCache.set(sourceEnvironment, entry);
100
+ // Failures are never cached: evict so the next call reads again.
101
+ entry.promise.catch(() => {
102
+ if (governanceTokenPriceCache.get(sourceEnvironment) === entry) {
103
+ governanceTokenPriceCache.delete(sourceEnvironment);
104
+ }
105
+ });
106
+ return { sourceChainId, entry };
107
+ }
108
+
44
109
  /**
45
110
  * Returns the governance-token-in-USD price for an environment.
46
111
  *
@@ -50,17 +115,44 @@ export async function getWellPriceFromBaseOracle(
50
115
  * env's own views.getGovernanceTokenPrice() — Moonriver has its own MFAM
51
116
  * oracle and isn't priced from Base.
52
117
  *
53
- * Returns 0n if the lookup fails.
118
+ * Concurrent calls for the same price source share one in-flight read, and a
119
+ * successful price is reused for a few seconds. Rejects if the read fails.
54
120
  */
55
121
  export async function getGovernanceTokenPriceFor(
56
122
  environment: Environment,
57
123
  baseEnvironment?: Environment,
58
124
  ): Promise<bigint> {
59
- if (environment.custom?.governance?.token === "WELL") {
60
- return getWellPriceFromBaseOracle(baseEnvironment);
125
+ return getSharedGovernanceTokenPrice(environment, baseEnvironment).entry
126
+ .promise;
127
+ }
128
+
129
+ /**
130
+ * Same shared read as `getGovernanceTokenPriceFor`, but resolves to 0n on
131
+ * failure. A failed shared read is reported once per `onError` callback,
132
+ * however many callers using that callback were waiting on it; `items` lists
133
+ * the requesting chainIds.
134
+ */
135
+ export async function getGovernanceTokenPriceOrZero(
136
+ environment: Environment,
137
+ baseEnvironment?: Environment,
138
+ ): Promise<bigint> {
139
+ const { sourceChainId, entry } = getSharedGovernanceTokenPrice(
140
+ environment,
141
+ baseEnvironment,
142
+ );
143
+ try {
144
+ return await entry.promise;
145
+ } catch (error) {
146
+ const onError = environment.onError;
147
+ if (onError && !entry.reportedTo.has(onError)) {
148
+ entry.reportedTo.add(onError);
149
+ environment.onError?.(error, {
150
+ source: "governance-token-price",
151
+ chainId: sourceChainId,
152
+ operation: "governance-token-price-read",
153
+ items: [...entry.requestingChainIds].map(String),
154
+ });
155
+ }
156
+ return 0n;
61
157
  }
62
- // Non-WELL (e.g. Moonriver / MFAM): the env itself is the governance "home".
63
- const views = environment.contracts.views;
64
- if (!views) return 0n;
65
- return (await views.read.getGovernanceTokenPrice()) ?? 0n;
66
158
  }
@@ -447,6 +447,29 @@ export const getProposalsOnChainData = async (
447
447
  ? governanceEnvironment
448
448
  : getEnvironmentByChainId(chainId);
449
449
 
450
+ // viem batches these reads into one multicall, so a single failed request
451
+ // rejects every read in it. Collect failures per chain and report them once
452
+ // after the batch instead of once per read (Sentry MOONWELL-FRONTEND-1AX).
453
+ const readFailures = new Map<
454
+ number,
455
+ { error: unknown; failed: number; total: number; proposalIds: Set<string> }
456
+ >();
457
+ const recordRead = (chainId: number, proposalId: number, error?: unknown) => {
458
+ const entry = readFailures.get(chainId) ?? {
459
+ error: undefined,
460
+ failed: 0,
461
+ total: 0,
462
+ proposalIds: new Set<string>(),
463
+ };
464
+ entry.total++;
465
+ if (error !== undefined) {
466
+ entry.error ??= error;
467
+ entry.failed++;
468
+ entry.proposalIds.add(String(proposalId));
469
+ }
470
+ readFailures.set(chainId, entry);
471
+ };
472
+
450
473
  const onChainDataList = await Promise.all(
451
474
  apiProposals.map(async (p) => {
452
475
  const isLocal = p.chainId === governanceEnvironment.chainId;
@@ -501,29 +524,17 @@ export const getProposalsOnChainData = async (
501
524
  ]);
502
525
  if (stateResult.status === "fulfilled") {
503
526
  state = Number(stateResult.value);
527
+ recordRead(p.chainId, p.proposalId);
504
528
  } else {
505
529
  stateReadFailed = true;
506
- console.warn(
507
- `Failed to fetch state for proposal ${p.proposalId} (chainId=${p.chainId}):`,
508
- stateResult.reason,
509
- );
510
- governanceEnvironment.onError?.(stateResult.reason, {
511
- source: "governance-proposals",
512
- chainId: p.chainId,
513
- });
530
+ recordRead(p.chainId, p.proposalId, stateResult.reason);
514
531
  }
515
532
  if (proposalsResult.status === "fulfilled") {
516
533
  proposalData = proposalsResult.value ?? null;
534
+ recordRead(p.chainId, p.proposalId);
517
535
  } else {
518
536
  proposalsReadFailed = true;
519
- console.warn(
520
- `Failed to fetch proposalData for proposal ${p.proposalId} (chainId=${p.chainId}):`,
521
- proposalsResult.reason,
522
- );
523
- governanceEnvironment.onError?.(proposalsResult.reason, {
524
- source: "governance-proposals",
525
- chainId: p.chainId,
526
- });
537
+ recordRead(p.chainId, p.proposalId, proposalsResult.reason);
527
538
  }
528
539
  }
529
540
 
@@ -609,6 +620,23 @@ export const getProposalsOnChainData = async (
609
620
  }),
610
621
  );
611
622
 
623
+ for (const [chainId, entry] of readFailures) {
624
+ if (entry.failed === 0) continue;
625
+ const proposalIds = [...entry.proposalIds];
626
+ console.warn(
627
+ `Failed ${entry.failed}/${entry.total} governor reads (chainId=${chainId}, proposals ${proposalIds.join(", ")}):`,
628
+ entry.error,
629
+ );
630
+ governanceEnvironment.onError?.(entry.error, {
631
+ source: "governance-proposals",
632
+ chainId,
633
+ operation: "proposal-onchain-read",
634
+ failedCount: entry.failed,
635
+ totalCount: entry.total,
636
+ items: proposalIds,
637
+ });
638
+ }
639
+
612
640
  return onChainDataList;
613
641
  };
614
642
 
@@ -20,7 +20,16 @@ import {
20
20
  export type GetProposalsParameters<
21
21
  environments,
22
22
  network extends Chain | undefined,
23
- > = OptionalNetworkParameterType<environments, network>;
23
+ > = OptionalNetworkParameterType<environments, network> & {
24
+ /**
25
+ * Governance chains to fetch proposals from: 1 (Ethereum, active),
26
+ * 1284 (Moonbeam archive) and/or 1285 (Moonriver archive). Defaults to all
27
+ * of them. The archives hold almost all of the payload (~6.6MB of ~7MB), so
28
+ * views that only show recent proposals can pass `[1]` and load the archives
29
+ * separately when needed. Unsupported chainIds are ignored.
30
+ */
31
+ proposalChainIds?: readonly number[];
32
+ };
24
33
 
25
34
  export type GetProposalsReturnType = Promise<Proposal[]>;
26
35
 
@@ -42,7 +51,10 @@ export async function getProposals<
42
51
  return [];
43
52
  }
44
53
 
45
- const proposals = await fetchGovernorProposals(governanceEnvironment);
54
+ const proposals = await fetchGovernorProposals(
55
+ governanceEnvironment,
56
+ args?.proposalChainIds,
57
+ );
46
58
  // Newer multigov-ethereum proposals (chainId=1) and historical Moonbeam
47
59
  // proposals (chainId=1284) restart their proposalId counters from 1, so IDs
48
60
  // may collide across chains. Sort by proposalId desc with chainId as a
@@ -58,7 +70,8 @@ export async function getProposals<
58
70
  }
59
71
 
60
72
  /**
61
- * Fetch every proposal the Governor API serves, across all governance chains.
73
+ * Fetch every proposal the Governor API serves, across all governance chains
74
+ * (or only `proposalChainIds`, when given).
62
75
  *
63
76
  * One indexer DO serves them all:
64
77
  * - chainId=1 (Ethereum) — the active multigov contract
@@ -76,16 +89,20 @@ export async function getProposals<
76
89
  */
77
90
  async function fetchGovernorProposals(
78
91
  governanceEnvironment: Environment,
92
+ proposalChainIds?: readonly number[],
79
93
  ): Promise<Proposal[]> {
94
+ const chainIds = SUPPORTED_GOVERNOR_CHAIN_IDS.filter(
95
+ (chainId) => !proposalChainIds || proposalChainIds.includes(chainId),
96
+ );
80
97
  const results = await Promise.allSettled(
81
- SUPPORTED_GOVERNOR_CHAIN_IDS.map((chainId) =>
98
+ chainIds.map((chainId) =>
82
99
  fetchAllProposals(governanceEnvironment, { chainId }),
83
100
  ),
84
101
  );
85
102
 
86
103
  const apiProposals: ApiProposal[] = [];
87
104
  results.forEach((result, index) => {
88
- const chainId = SUPPORTED_GOVERNOR_CHAIN_IDS[index];
105
+ const chainId = chainIds[index];
89
106
  if (result.status === "fulfilled") {
90
107
  apiProposals.push(...result.value);
91
108
  } else if (chainId !== undefined) {
@@ -76,6 +76,21 @@ function validateLoanDecimals(value: unknown): number {
76
76
  return value;
77
77
  }
78
78
 
79
+ /** Summarizes an unexpected top-level body so the error says what arrived. */
80
+ function describeSharedLiquidityBody(data: unknown): string {
81
+ if (data === null || data === undefined) return `received ${data}`;
82
+ if (typeof data === "string") {
83
+ return `received string of ${data.length} chars: ${JSON.stringify(data.slice(0, 100))}`;
84
+ }
85
+ if (typeof data !== "object") return `received ${typeof data}`;
86
+ if (Array.isArray(data)) return `received array of ${data.length}`;
87
+ const body = data as Record<string, unknown>;
88
+ const kind = (value: unknown): string =>
89
+ Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
90
+ const keys = Object.keys(body).slice(0, 10).join(", ");
91
+ return `received object with keys [${keys}]; vaults: ${kind(body.vaults)}, markets: ${kind(body.markets)}`;
92
+ }
93
+
79
94
  function validatedMarketLiquidity(
80
95
  data: LunarSharedLiquidityResponse,
81
96
  ): Map<string, number> {
@@ -86,9 +101,19 @@ function validatedMarketLiquidity(
86
101
  typeof data.markets !== "object" ||
87
102
  Array.isArray(data.markets)
88
103
  ) {
89
- throw new TypeError("Invalid shared-liquidity response");
104
+ throw new TypeError(
105
+ `Invalid shared-liquidity response (${describeSharedLiquidityBody(data)})`,
106
+ );
90
107
  }
91
108
  for (const vault of data.vaults) {
109
+ // A null entry or a vault record without `markets` (Sentry
110
+ // MOONWELL-FRONTEND-116, MOO-535) must fail validation here, not as a
111
+ // TypeError from `vault.fee` or the loop below.
112
+ if (!vault || !Array.isArray(vault.markets)) {
113
+ throw new TypeError(
114
+ `Invalid shared-liquidity response (vault ${vault?.address ?? "<null>"} has no markets array)`,
115
+ );
116
+ }
92
117
  parseLiquidityQuantity(vault.fee, "vault.fee", true);
93
118
  for (const market of vault.markets) {
94
119
  for (const field of [
@@ -158,13 +183,31 @@ export async function fetchSharedLiquidityFromLunar(
158
183
  const abort = (): void => controller.abort();
159
184
  if (options.signal?.aborted) abort();
160
185
  options.signal?.addEventListener("abort", abort, { once: true });
161
- const timer = setTimeout(abort, timeoutMs);
186
+ let timedOut = false;
187
+ const timer = setTimeout(() => {
188
+ timedOut = true;
189
+ abort();
190
+ }, timeoutMs);
162
191
  try {
163
192
  const response = await getWithRetry<LunarSharedLiquidityResponse>(
164
193
  `${lunarIndexerUrl}/api/v1/isolated/shared-liquidity/${chainId}`,
165
194
  { timeout: timeoutMs, signal: controller.signal },
166
195
  );
167
196
  return response.data;
197
+ } catch (error) {
198
+ // Our own deadline aborts the request, which axios reports as a bare
199
+ // `CanceledError: canceled` (Sentry MOONWELL-FRONTEND-1C7 & co). That is a
200
+ // timeout, not a cancellation: surface it as one. A caller's own abort
201
+ // still propagates as the CanceledError it is.
202
+ if (timedOut && !options.signal?.aborted) {
203
+ const timeoutError = new Error(
204
+ `Shared-liquidity request timed out after ${timeoutMs}ms (chainId=${chainId})`,
205
+ { cause: error },
206
+ );
207
+ timeoutError.name = "TimeoutError";
208
+ throw timeoutError;
209
+ }
210
+ throw error;
168
211
  } finally {
169
212
  clearTimeout(timer);
170
213
  options.signal?.removeEventListener("abort", abort);
@@ -186,6 +186,13 @@ export async function fetchMarketsFromIndexer(
186
186
  const queryString = params.toString();
187
187
  const url = `${lunarIndexerUrl}/api/v1/isolated/markets/${chainId}${queryString ? `?${queryString}` : ""}`;
188
188
  const response = await getWithRetry<LunarIndexerMarketsResponse>(url);
189
+ // A 2xx body without `results` otherwise reaches `.filter` / iteration in
190
+ // getMorphoMarketsDataFromIndexer as a TypeError (PR #339 review).
191
+ if (!Array.isArray(response.data?.results)) {
192
+ throw new Error(
193
+ `Lunar Indexer morpho markets response for chain ${chainId} is missing the results array`,
194
+ );
195
+ }
189
196
  return response.data;
190
197
  }
191
198
 
@@ -13,7 +13,7 @@ import {
13
13
  } from "../../../environments/utils/index.js";
14
14
  import type { MorphoUserReward } from "../../../types/morphoUserReward.js";
15
15
  import type { MorphoUserStakingReward } from "../../../types/morphoUserStakingReward.js";
16
- import { getGovernanceTokenPriceFor } from "../../governance/getWellPrice.js";
16
+ import { getGovernanceTokenPriceOrZero } from "../../governance/getWellPrice.js";
17
17
 
18
18
  /**
19
19
  * Error thrown for any failure communicating with the Merkl API: non-ok HTTP
@@ -166,13 +166,7 @@ export async function getUserMorphoStakingRewardsData(params: {
166
166
  await Promise.all([
167
167
  viewsContract?.read.getAllMarketsInfo(),
168
168
  homeViewsContract?.read.getNativeTokenPrice(),
169
- getGovernanceTokenPriceFor(params.environment).catch((err) => {
170
- params.environment.onError?.(err, {
171
- source: "governance-token-price",
172
- chainId: params.environment.chainId,
173
- });
174
- return 0n;
175
- }),
169
+ getGovernanceTokenPriceOrZero(params.environment),
176
170
  ]);
177
171
 
178
172
  const governanceTokenPrice = new Amount(governanceTokenPriceRaw, 18);