@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
@@ -19,7 +19,7 @@ import type {
19
19
  MorphoVault,
20
20
  MorphoVaultMarket,
21
21
  } from "../../../types/morphoVault.js";
22
- import { getGovernanceTokenPriceFor } from "../../governance/getWellPrice.js";
22
+ import { getGovernanceTokenPriceOrZero } from "../../governance/getWellPrice.js";
23
23
  import { getGraphQL, getVaultV2Apy } from "../utils/graphql.js";
24
24
  import {
25
25
  SECONDS_PER_YEAR,
@@ -300,144 +300,184 @@ async function getMorphoVaultsDataFromIndexer(params: {
300
300
 
301
301
  // Add staking rewards if includeRewards is true
302
302
  if (params.includeRewards) {
303
+ const vaultsByEnvironment = new Map<
304
+ Environment,
305
+ { vault: MorphoVault; multiReward: Address }[]
306
+ >();
303
307
  for (const vault of allVaults) {
304
308
  const environment = environments.find(
305
309
  (env) => env.chainId === vault.chainId,
306
310
  );
307
311
  if (!environment) continue;
308
312
 
309
- const vaultConfig = environment.config.vaults[vault.vaultKey];
310
- if (!vaultConfig?.multiReward) continue;
313
+ const multiReward =
314
+ environment.config.vaults[vault.vaultKey]?.multiReward;
315
+ if (!multiReward) continue;
311
316
 
312
- try {
313
- // Fetch market prices from the views contract to calculate rewards
314
- const homeEnvironment =
315
- (Object.values(publicEnvironments) as Environment[]).find((e) =>
316
- e.custom?.governance?.chainIds?.includes(environment.chainId),
317
- ) || environment;
318
-
319
- const viewsContract = environment.contracts.views;
320
- const homeViewsContract = homeEnvironment.contracts.views;
321
-
322
- const data = await Promise.all([
323
- viewsContract?.read.getAllMarketsInfo(),
324
- homeViewsContract?.read.getNativeTokenPrice(),
325
- getGovernanceTokenPriceFor(environment).catch((err) => {
326
- environment.onError?.(err, {
327
- source: "governance-token-price",
328
- chainId: environment.chainId,
329
- });
330
- return 0n;
331
- }),
332
- ]);
317
+ const group = vaultsByEnvironment.get(environment) ?? [];
318
+ group.push({ vault, multiReward });
319
+ vaultsByEnvironment.set(environment, group);
320
+ }
333
321
 
334
- const [allMarkets, nativeTokenPriceRaw, governanceTokenPriceRaw] = data;
322
+ // Price reads are per environment, not per vault: read them once per
323
+ // environment (in parallel across environments) and report failures once.
324
+ await Promise.all(
325
+ [...vaultsByEnvironment].map(([environment, group]) =>
326
+ addStakingRewardsForEnvironment(environment, group),
327
+ ),
328
+ );
329
+ }
335
330
 
336
- const governanceTokenPrice = new Amount(governanceTokenPriceRaw, 18);
337
- const nativeTokenPrice = new Amount(nativeTokenPriceRaw ?? 0n, 18);
331
+ return [...allVaults, ...fallbackVaults];
332
+ }
338
333
 
339
- let tokenPrices =
340
- allMarkets
341
- ?.map((marketInfo) => {
342
- const marketFound = findMarketByAddress(
343
- environment,
344
- marketInfo.market,
345
- );
346
- if (marketFound) {
347
- return {
348
- token: marketFound.underlyingToken,
349
- tokenPrice: new Amount(
350
- marketInfo.underlyingPrice,
351
- 36 - marketFound.underlyingToken.decimals,
352
- ),
353
- };
354
- } else {
355
- return;
356
- }
357
- })
358
- .filter((token) => !!token) || [];
359
-
360
- // Add governance token to token prices
361
- if (environment.custom?.governance?.token) {
362
- tokenPrices = [
363
- ...tokenPrices,
364
- {
365
- token:
366
- environment.config.tokens[environment.custom.governance.token]!,
367
- tokenPrice: governanceTokenPrice,
368
- },
369
- ];
370
- }
334
+ async function getStakingRewardTokenPrices(
335
+ environment: Environment,
336
+ ): Promise<{ token: TokenConfig; tokenPrice: Amount }[]> {
337
+ // Fetch market prices from the views contract to calculate rewards
338
+ const homeEnvironment =
339
+ (Object.values(publicEnvironments) as Environment[]).find((e) =>
340
+ e.custom?.governance?.chainIds?.includes(environment.chainId),
341
+ ) || environment;
342
+
343
+ const [allMarkets, nativeTokenPriceRaw, governanceTokenPriceRaw] =
344
+ await Promise.all([
345
+ environment.contracts.views?.read.getAllMarketsInfo(),
346
+ homeEnvironment.contracts.views?.read.getNativeTokenPrice(),
347
+ getGovernanceTokenPriceOrZero(environment),
348
+ ]);
349
+
350
+ const tokenPrices: { token: TokenConfig; tokenPrice: Amount }[] = [];
351
+ for (const marketInfo of allMarkets ?? []) {
352
+ const marketFound = findMarketByAddress(environment, marketInfo.market);
353
+ if (marketFound) {
354
+ tokenPrices.push({
355
+ token: marketFound.underlyingToken,
356
+ tokenPrice: new Amount(
357
+ marketInfo.underlyingPrice,
358
+ 36 - marketFound.underlyingToken.decimals,
359
+ ),
360
+ });
361
+ }
362
+ }
371
363
 
372
- // Add native token to token prices
373
- tokenPrices = [
374
- ...tokenPrices,
375
- {
376
- token: findTokenByAddress(environment, zeroAddress)!,
377
- tokenPrice: nativeTokenPrice,
378
- },
379
- ];
364
+ // Add governance token to token prices
365
+ const governanceToken = environment.custom?.governance?.token
366
+ ? environment.config.tokens[environment.custom.governance.token]
367
+ : undefined;
368
+ if (governanceToken) {
369
+ tokenPrices.push({
370
+ token: governanceToken,
371
+ tokenPrice: new Amount(governanceTokenPriceRaw, 18),
372
+ });
373
+ }
380
374
 
381
- const rewards = await getRewardsData(
382
- environment,
383
- vaultConfig.multiReward,
384
- );
375
+ // Add native token to token prices
376
+ const nativeToken = findTokenByAddress(environment, zeroAddress);
377
+ if (nativeToken) {
378
+ tokenPrices.push({
379
+ token: nativeToken,
380
+ tokenPrice: new Amount(nativeTokenPriceRaw ?? 0n, 18),
381
+ });
382
+ }
385
383
 
386
- const distributorTotalSupply = await getTotalSupplyData(
387
- environment,
388
- vaultConfig.multiReward,
389
- );
384
+ return tokenPrices;
385
+ }
390
386
 
391
- rewards
392
- .filter(
393
- (reward) =>
394
- reward?.periodFinish &&
395
- dayjs.utc().isBefore(dayjs.unix(Number(reward.periodFinish))),
396
- )
397
- .forEach((reward) => {
398
- const token = Object.values(environment.config.tokens).find(
399
- (token) => token.address === reward?.token,
400
- );
401
- if (!token || !reward?.rewardRate) return;
387
+ async function addStakingRewardsForEnvironment(
388
+ environment: Environment,
389
+ group: { vault: MorphoVault; multiReward: Address }[],
390
+ ): Promise<void> {
391
+ const vaultKeys = group.map(({ vault }) => vault.vaultKey);
402
392
 
403
- const market = tokenPrices.find(
404
- (m) => m?.token.address === reward.token,
405
- );
393
+ let tokenPrices: { token: TokenConfig; tokenPrice: Amount }[];
394
+ try {
395
+ tokenPrices = await getStakingRewardTokenPrices(environment);
396
+ } catch (error) {
397
+ console.warn(
398
+ `Failed to fetch staking reward prices for chain ${environment.chainId}:`,
399
+ error,
400
+ );
401
+ environment.onError?.(error, {
402
+ source: "morpho-vault-staking-rewards",
403
+ chainId: environment.chainId,
404
+ operation: "staking-reward-prices",
405
+ failedCount: group.length,
406
+ totalCount: group.length,
407
+ items: vaultKeys,
408
+ });
409
+ return;
410
+ }
406
411
 
407
- const rewardPriceUsd = market?.tokenPrice.value ?? 0;
408
-
409
- const rewardsPerYear =
410
- new Amount(reward.rewardRate, market?.token.decimals ?? 18)
411
- .value *
412
- SECONDS_PER_YEAR *
413
- rewardPriceUsd;
414
-
415
- vault.stakingRewards.push({
416
- apr:
417
- (rewardsPerYear /
418
- (new Amount(distributorTotalSupply, vault.vaultToken.decimals)
419
- .value *
420
- vault.underlyingPrice)) *
421
- 100,
422
- token: token,
423
- });
412
+ const settlements = await Promise.allSettled(
413
+ group.map(async ({ vault, multiReward }) => {
414
+ const [rewards, distributorTotalSupply] = await Promise.all([
415
+ getRewardsData(environment, multiReward),
416
+ getTotalSupplyData(environment, multiReward),
417
+ ]);
418
+
419
+ rewards
420
+ .filter(
421
+ (reward) =>
422
+ reward?.periodFinish &&
423
+ dayjs.utc().isBefore(dayjs.unix(Number(reward.periodFinish))),
424
+ )
425
+ .forEach((reward) => {
426
+ const token = Object.values(environment.config.tokens).find(
427
+ (token) => token.address === reward?.token,
428
+ );
429
+ if (!token || !reward?.rewardRate) return;
430
+
431
+ const market = tokenPrices.find(
432
+ (m) => m?.token.address === reward.token,
433
+ );
434
+
435
+ const rewardPriceUsd = market?.tokenPrice.value ?? 0;
436
+
437
+ const rewardsPerYear =
438
+ new Amount(reward.rewardRate, market?.token.decimals ?? 18).value *
439
+ SECONDS_PER_YEAR *
440
+ rewardPriceUsd;
441
+
442
+ vault.stakingRewards.push({
443
+ apr:
444
+ (rewardsPerYear /
445
+ (new Amount(distributorTotalSupply, vault.vaultToken.decimals)
446
+ .value *
447
+ vault.underlyingPrice)) *
448
+ 100,
449
+ token: token,
424
450
  });
451
+ });
425
452
 
426
- vault.stakingRewardsApr = vault.stakingRewards.reduce(
427
- (acc, curr) => acc + curr.apr,
428
- 0,
429
- );
430
- vault.totalStakingApr = vault.stakingRewardsApr + vault.baseApy;
431
- } catch (error) {
432
- console.warn(
433
- `Failed to fetch staking rewards for vault ${vault.vaultKey}:`,
434
- error,
435
- );
436
- }
437
- }
438
- }
453
+ vault.stakingRewardsApr = vault.stakingRewards.reduce(
454
+ (acc, curr) => acc + curr.apr,
455
+ 0,
456
+ );
457
+ vault.totalStakingApr = vault.stakingRewardsApr + vault.baseApy;
458
+ }),
459
+ );
439
460
 
440
- return [...allVaults, ...fallbackVaults];
461
+ const failedVaultKeys = vaultKeys.filter(
462
+ (_, i) => settlements[i]?.status === "rejected",
463
+ );
464
+ const firstFailure = settlements.find(
465
+ (s): s is PromiseRejectedResult => s.status === "rejected",
466
+ );
467
+ if (firstFailure) {
468
+ console.warn(
469
+ `Failed to fetch staking rewards for vault(s) ${failedVaultKeys.join(", ")} (chain ${environment.chainId}):`,
470
+ firstFailure.reason,
471
+ );
472
+ environment.onError?.(firstFailure.reason, {
473
+ source: "morpho-vault-staking-rewards",
474
+ chainId: environment.chainId,
475
+ operation: "staking-rewards",
476
+ failedCount: failedVaultKeys.length,
477
+ totalCount: group.length,
478
+ items: failedVaultKeys,
479
+ });
480
+ }
441
481
  }
442
482
 
443
483
  export async function getMorphoVaultsData(params: {
@@ -1060,13 +1100,7 @@ async function getMorphoVaultsDataFromOnChain(params: {
1060
1100
  const data = await Promise.all([
1061
1101
  viewsContract?.read.getAllMarketsInfo(),
1062
1102
  homeViewsContract?.read.getNativeTokenPrice(),
1063
- getGovernanceTokenPriceFor(environment).catch((err) => {
1064
- environment.onError?.(err, {
1065
- source: "governance-token-price",
1066
- chainId: environment.chainId,
1067
- });
1068
- return 0n;
1069
- }),
1103
+ getGovernanceTokenPriceOrZero(environment),
1070
1104
  ]);
1071
1105
 
1072
1106
  const [allMarkets, nativeTokenPriceRaw, governanceTokenPriceRaw] = data;
@@ -15,6 +15,7 @@ import type {
15
15
  MorphoVaultMarket,
16
16
  MorphoVaultSnapshot,
17
17
  } from "../../../types/morphoVault.js";
18
+ import { LunarIndexerError } from "../../lunar-indexer-client.js";
18
19
 
19
20
  /**
20
21
  * Lunar Indexer API Response Types
@@ -248,13 +249,14 @@ export function transformVaultFromIndexer(
248
249
  // where totalLiquidity briefly exceeds totalAssets.
249
250
  const vaultSupplyValue = Math.max(0, totalAssetsValue - totalLiquidityValue);
250
251
 
251
- // Transform markets
252
- const markets = indexerVault.markets.map((market) =>
252
+ // Transform markets. The indexer has served vaults without `markets` /
253
+ // `rewards` (Sentry MOONWELL-FRONTEND-GX); keep the vault with empty lists.
254
+ const markets = (indexerVault.markets ?? []).map((market) =>
253
255
  transformMarket(market, underlyingDecimals, totalAssetsValue, tokenMap),
254
256
  );
255
257
 
256
258
  // Transform rewards
257
- const rewards = transformRewards(indexerVault.rewards, tokenMap);
259
+ const rewards = transformRewards(indexerVault.rewards ?? [], tokenMap);
258
260
 
259
261
  // Build vault object
260
262
  const vault: MorphoVault = {
@@ -313,9 +315,99 @@ export function transformVaultsFromIndexer(
313
315
  environment: Environment,
314
316
  tokenMap: Map<string, LunarIndexerToken>,
315
317
  ): MorphoVault[] {
316
- return indexerVaults.map((vault) =>
317
- transformVaultFromIndexer(vault, environment, tokenMap),
318
- );
318
+ // A malformed record costs only that record, never the chain's whole list.
319
+ // Missing `markets` / `rewards` keep the vault (as empty lists); a vault
320
+ // that cannot be built at all is dropped. Both are reported once per call,
321
+ // with `droppedCount` telling them apart: only the first error is reported,
322
+ // so a consumer demoting the kept case (the frontend logs it at warning,
323
+ // since the list still renders in full) would otherwise misread a batch
324
+ // where a kept record precedes a dropped one.
325
+ const malformed: string[] = [];
326
+ let droppedCount = 0;
327
+ let firstError: unknown;
328
+ const vaults = indexerVaults.flatMap((vault) => {
329
+ // A `null` entry would throw on `vault.markets` below, before the try
330
+ // (PR #339 review). Drop it and count it with the other malformed records.
331
+ if (vault === null || typeof vault !== "object") {
332
+ malformed.push(String(vault));
333
+ droppedCount += 1;
334
+ firstError ??= new Error(
335
+ `Lunar Indexer vault record is ${String(vault)}, not an object`,
336
+ );
337
+ return [];
338
+ }
339
+ if (!Array.isArray(vault.markets) || !Array.isArray(vault.rewards)) {
340
+ malformed.push(vault.address);
341
+ firstError ??= new Error(
342
+ `Lunar Indexer vault ${vault.address} is missing ${!Array.isArray(vault.markets) ? "markets" : "rewards"}`,
343
+ );
344
+ }
345
+ try {
346
+ return [transformVaultFromIndexer(vault, environment, tokenMap)];
347
+ } catch (error) {
348
+ if (!malformed.includes(vault.address)) malformed.push(vault.address);
349
+ droppedCount += 1;
350
+ firstError ??= error;
351
+ return [];
352
+ }
353
+ });
354
+
355
+ if (malformed.length > 0) {
356
+ console.warn(
357
+ `Malformed Lunar Indexer vault record(s) on chain ${environment.chainId}: ${malformed.join(", ")}`,
358
+ firstError,
359
+ );
360
+ environment.onError?.(firstError, {
361
+ source: "vaults",
362
+ chainId: environment.chainId,
363
+ operation: "malformed-vault-records",
364
+ failedCount: malformed.length,
365
+ totalCount: indexerVaults.length,
366
+ items: malformed,
367
+ droppedCount,
368
+ });
369
+ }
370
+
371
+ return vaults;
372
+ }
373
+
374
+ /**
375
+ * Fetch JSON from the Lunar Indexer and reject on a non-OK or missing response.
376
+ *
377
+ * `response` can arrive `undefined` when a wallet extension monkeypatches
378
+ * `window.fetch` (Sentry MOONWELL-FRONTEND-H0 / -133, MOO-535); reading `.ok`
379
+ * on it used to throw a bare TypeError instead of a diagnosable error.
380
+ *
381
+ * The rejection is a `LunarIndexerError` whose message must not start with
382
+ * "Failed to fetch": the frontend's Sentry noise filter substring-matches that
383
+ * browser transport text and dropped every failure raised here (PR #339
384
+ * review). Consumers should match on `error.name`, not on the message.
385
+ *
386
+ * `AbortSignal.timeout` is missing in Safari < 16 and older WebViews, where it
387
+ * throws a bare TypeError before `fetch` runs; use AbortController instead and
388
+ * keep the timer alive until the body has been read.
389
+ */
390
+ async function fetchJsonFromIndexer<T>(url: string, what: string): Promise<T> {
391
+ const controller = new AbortController();
392
+ const timer = setTimeout(() => controller.abort(), 10_000);
393
+ try {
394
+ const response: Response | undefined = await fetch(url, {
395
+ signal: controller.signal,
396
+ });
397
+ if (!response?.ok) {
398
+ const status = response
399
+ ? `${response.status} ${response.statusText}`
400
+ : "no response";
401
+ throw new LunarIndexerError(
402
+ `Lunar Indexer ${what} request failed: ${status}`,
403
+ response?.status,
404
+ url,
405
+ );
406
+ }
407
+ return await response.json();
408
+ } finally {
409
+ clearTimeout(timer);
410
+ }
319
411
  }
320
412
 
321
413
  /**
@@ -331,16 +423,16 @@ export async function fetchTokenMap(
331
423
  ): Promise<Map<string, LunarIndexerToken>> {
332
424
  const url = `${lunarIndexerUrl}/api/v1/vaults/tokens/${chainId}`;
333
425
 
334
- const response = await fetch(url, { signal: AbortSignal.timeout(10_000) });
335
-
336
- if (!response.ok) {
426
+ const data = await fetchJsonFromIndexer<LunarIndexerTokensResponse>(
427
+ url,
428
+ "tokens",
429
+ );
430
+ if (!Array.isArray(data?.results)) {
337
431
  throw new Error(
338
- `Failed to fetch tokens from Lunar Indexer: ${response.status} ${response.statusText}`,
432
+ `Lunar Indexer tokens response for chain ${chainId} is missing the results array`,
339
433
  );
340
434
  }
341
435
 
342
- const data: LunarIndexerTokensResponse = await response.json();
343
-
344
436
  const tokenMap = new Map<string, LunarIndexerToken>();
345
437
  for (const token of data.results) {
346
438
  tokenMap.set(token.address.toLowerCase(), token);
@@ -373,15 +465,17 @@ export async function fetchVaultsFromIndexer(
373
465
 
374
466
  const url = `${lunarIndexerUrl}/api/v1/vaults/vaults/${chainId}${params.toString() ? `?${params.toString()}` : ""}`;
375
467
 
376
- const response = await fetch(url, { signal: AbortSignal.timeout(10_000) });
377
-
378
- if (!response.ok) {
468
+ const data = await fetchJsonFromIndexer<LunarIndexerVaultsResponse>(
469
+ url,
470
+ "vaults",
471
+ );
472
+ // transformVaultsFromIndexer calls `.flatMap` on this array (PR #339 review).
473
+ if (!Array.isArray(data?.results)) {
379
474
  throw new Error(
380
- `Failed to fetch vaults from Lunar Indexer: ${response.status} ${response.statusText}`,
475
+ `Lunar Indexer vaults response for chain ${chainId} is missing the results array`,
381
476
  );
382
477
  }
383
-
384
- return response.json();
478
+ return data;
385
479
  }
386
480
 
387
481
  /**
@@ -397,15 +491,7 @@ export async function fetchVaultFromIndexer(
397
491
  ): Promise<LunarIndexerVault> {
398
492
  const url = `${lunarIndexerUrl}/api/v1/vaults/vault/${vaultId}`;
399
493
 
400
- const response = await fetch(url, { signal: AbortSignal.timeout(10_000) });
401
-
402
- if (!response.ok) {
403
- throw new Error(
404
- `Failed to fetch vault from Lunar Indexer: ${response.status} ${response.statusText}`,
405
- );
406
- }
407
-
408
- return response.json();
494
+ return fetchJsonFromIndexer<LunarIndexerVault>(url, "vault");
409
495
  }
410
496
 
411
497
  /**
@@ -464,15 +550,10 @@ export async function fetchVaultSnapshotsFromIndexer(
464
550
  const queryString = params.toString();
465
551
  const url = `${lunarIndexerUrl}/api/v1/vaults/vault/${vaultId}/snapshots${queryString ? `?${queryString}` : ""}`;
466
552
 
467
- const response = await fetch(url, { signal: AbortSignal.timeout(10_000) });
468
-
469
- if (!response.ok) {
470
- throw new Error(
471
- `Failed to fetch vault snapshots from Lunar Indexer: ${response.status} ${response.statusText}`,
472
- );
473
- }
474
-
475
- return response.json();
553
+ return fetchJsonFromIndexer<LunarIndexerVaultSnapshotsResponse>(
554
+ url,
555
+ "vault snapshots",
556
+ );
476
557
  }
477
558
 
478
559
  /**
@@ -228,11 +228,27 @@ export type ContractsConfig<contracts, tokens> = {} extends contracts
228
228
  ? {}
229
229
  : { [name in keyof ContractConfig<tokens>]?: ContractConfig<tokens>[name] };
230
230
 
231
- /** Context passed to `onError` callbacks. `token` is set for per-token read failures. */
231
+ /**
232
+ * Context passed to `onError` callbacks. `token` is set for per-token read failures.
233
+ *
234
+ * Batched reads report once per operation rather than once per item: `error`
235
+ * is the first failure, `failedCount`/`totalCount` size the batch and `items`
236
+ * identifies what failed (proposalIds, token/vault addresses, chainIds...).
237
+ *
238
+ * `droppedCount` is how many of the failed items were left out of the result
239
+ * rather than kept in a degraded form. Only `error` (the first failure) is
240
+ * reported, so without it a consumer cannot tell a batch whose failed records
241
+ * were all kept from one where a later record was dropped.
242
+ */
232
243
  export type OnErrorContext = {
233
244
  source: string;
234
245
  chainId: number;
235
246
  token?: Address;
247
+ operation?: string;
248
+ failedCount?: number;
249
+ totalCount?: number;
250
+ items?: string[];
251
+ droppedCount?: number;
236
252
  };
237
253
 
238
254
  export type CustomConfig<custom> = {} extends custom
package/errors/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '0.24.0'
1
+ export const version = '0.25.1'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@moonwell-fi/moonwell-sdk",
3
3
  "description": "TypeScript Interface for Moonwell",
4
- "version": "0.24.0",
4
+ "version": "0.25.1",
5
5
  "main": "./_cjs/index.js",
6
6
  "module": "./_esm/index.js",
7
7
  "types": "./_types/index.d.ts",
@@ -76,7 +76,7 @@
76
76
  "@rhinestone/module-sdk": "0.2.7",
77
77
  "@types/lodash": "^4.17.9",
78
78
  "@types/node": "^22.13.10",
79
- "axios": "^1.18.1",
79
+ "axios": "^1.20.0",
80
80
  "dayjs": "^1.11.20",
81
81
  "lodash": "^4.17.21",
82
82
  "viem": "^2.26.2"