@moonwell-fi/moonwell-sdk 0.9.26 → 0.9.28

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 (81) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/_cjs/actions/core/markets/common.js +291 -5
  3. package/_cjs/actions/core/markets/common.js.map +1 -1
  4. package/_cjs/actions/core/markets/getMarketSnapshots.js +83 -3
  5. package/_cjs/actions/core/markets/getMarketSnapshots.js.map +1 -1
  6. package/_cjs/actions/core/user-positions/common.js +61 -1
  7. package/_cjs/actions/core/user-positions/common.js.map +1 -1
  8. package/_cjs/actions/core/user-positions/getUserPositionSnapshots.js +70 -2
  9. package/_cjs/actions/core/user-positions/getUserPositionSnapshots.js.map +1 -1
  10. package/_cjs/actions/lunar-indexer-client.js +164 -0
  11. package/_cjs/actions/lunar-indexer-client.js.map +1 -0
  12. package/_cjs/actions/lunar-indexer-transformers.js +47 -0
  13. package/_cjs/actions/lunar-indexer-transformers.js.map +1 -0
  14. package/_cjs/environments/definitions/base/environment.js +2 -1
  15. package/_cjs/environments/definitions/base/environment.js.map +1 -1
  16. package/_cjs/environments/definitions/moonbeam/environment.js +2 -1
  17. package/_cjs/environments/definitions/moonbeam/environment.js.map +1 -1
  18. package/_cjs/environments/definitions/optimism/environment.js +2 -1
  19. package/_cjs/environments/definitions/optimism/environment.js.map +1 -1
  20. package/_cjs/environments/types/config.js +1 -0
  21. package/_cjs/environments/types/config.js.map +1 -1
  22. package/_cjs/errors/version.js +1 -1
  23. package/_cjs/utils/lunar-indexer-helpers.js +27 -0
  24. package/_cjs/utils/lunar-indexer-helpers.js.map +1 -0
  25. package/_esm/actions/core/markets/common.js +302 -5
  26. package/_esm/actions/core/markets/common.js.map +1 -1
  27. package/_esm/actions/core/markets/getMarketSnapshots.js +87 -3
  28. package/_esm/actions/core/markets/getMarketSnapshots.js.map +1 -1
  29. package/_esm/actions/core/user-positions/common.js +74 -1
  30. package/_esm/actions/core/user-positions/common.js.map +1 -1
  31. package/_esm/actions/core/user-positions/getUserPositionSnapshots.js +100 -2
  32. package/_esm/actions/core/user-positions/getUserPositionSnapshots.js.map +1 -1
  33. package/_esm/actions/lunar-indexer-client.js +201 -0
  34. package/_esm/actions/lunar-indexer-client.js.map +1 -0
  35. package/_esm/actions/lunar-indexer-transformers.js +80 -0
  36. package/_esm/actions/lunar-indexer-transformers.js.map +1 -0
  37. package/_esm/environments/definitions/base/environment.js +2 -1
  38. package/_esm/environments/definitions/base/environment.js.map +1 -1
  39. package/_esm/environments/definitions/moonbeam/environment.js +2 -1
  40. package/_esm/environments/definitions/moonbeam/environment.js.map +1 -1
  41. package/_esm/environments/definitions/optimism/environment.js +2 -1
  42. package/_esm/environments/definitions/optimism/environment.js.map +1 -1
  43. package/_esm/environments/types/config.js +1 -0
  44. package/_esm/environments/types/config.js.map +1 -1
  45. package/_esm/errors/version.js +1 -1
  46. package/_esm/utils/lunar-indexer-helpers.js +48 -0
  47. package/_esm/utils/lunar-indexer-helpers.js.map +1 -0
  48. package/_types/actions/core/markets/common.d.ts.map +1 -1
  49. package/_types/actions/core/markets/getMarketSnapshots.d.ts +4 -0
  50. package/_types/actions/core/markets/getMarketSnapshots.d.ts.map +1 -1
  51. package/_types/actions/core/user-positions/common.d.ts.map +1 -1
  52. package/_types/actions/core/user-positions/getUserPositionSnapshots.d.ts +28 -0
  53. package/_types/actions/core/user-positions/getUserPositionSnapshots.d.ts.map +1 -1
  54. package/_types/actions/lunar-indexer-client.d.ts +197 -0
  55. package/_types/actions/lunar-indexer-client.d.ts.map +1 -0
  56. package/_types/actions/lunar-indexer-transformers.d.ts +40 -0
  57. package/_types/actions/lunar-indexer-transformers.d.ts.map +1 -0
  58. package/_types/environments/definitions/base/environment.d.ts +1 -1
  59. package/_types/environments/definitions/base/environment.d.ts.map +1 -1
  60. package/_types/environments/definitions/moonbeam/environment.d.ts +1 -1
  61. package/_types/environments/definitions/moonbeam/environment.d.ts.map +1 -1
  62. package/_types/environments/definitions/optimism/environment.d.ts +1 -1
  63. package/_types/environments/definitions/optimism/environment.d.ts.map +1 -1
  64. package/_types/environments/types/config.d.ts +2 -0
  65. package/_types/environments/types/config.d.ts.map +1 -1
  66. package/_types/errors/version.d.ts +1 -1
  67. package/_types/utils/lunar-indexer-helpers.d.ts +38 -0
  68. package/_types/utils/lunar-indexer-helpers.d.ts.map +1 -0
  69. package/actions/core/markets/common.ts +500 -5
  70. package/actions/core/markets/getMarketSnapshots.ts +153 -2
  71. package/actions/core/user-positions/common.ts +139 -6
  72. package/actions/core/user-positions/getUserPositionSnapshots.ts +175 -1
  73. package/actions/lunar-indexer-client.ts +409 -0
  74. package/actions/lunar-indexer-transformers.ts +113 -0
  75. package/environments/definitions/base/environment.ts +3 -0
  76. package/environments/definitions/moonbeam/environment.ts +3 -0
  77. package/environments/definitions/optimism/environment.ts +3 -0
  78. package/environments/types/config.ts +3 -0
  79. package/errors/version.ts +1 -1
  80. package/package.json +1 -1
  81. package/utils/lunar-indexer-helpers.ts +57 -0
@@ -19,6 +19,24 @@ import {
19
19
  import type { Market } from "../../../types/market.js";
20
20
 
21
21
  export const getMarketsData = async (environment: Environment) => {
22
+ // Moonriver (chainId 1285) should always use on-chain data
23
+ const isMoonriver = environment.chainId === 1285;
24
+
25
+ if (environment.lunarIndexerUrl && !isMoonriver) {
26
+ try {
27
+ const result = await fetchMarketsFromLunar(environment);
28
+ return result;
29
+ } catch (error) {
30
+ // Import shouldFallback dynamically
31
+ const { shouldFallback } = await import("../../lunar-indexer-client.js");
32
+
33
+ if (!shouldFallback(error)) {
34
+ throw error;
35
+ }
36
+ console.debug("[Lunar fallback] Falling back to RPC/Ponder:", error);
37
+ }
38
+ }
39
+
22
40
  const homeEnvironment =
23
41
  (Object.values(publicEnvironments) as Environment[]).find((e) =>
24
42
  e.custom?.governance?.chainIds?.includes(environment.chainId),
@@ -27,17 +45,50 @@ export const getMarketsData = async (environment: Environment) => {
27
45
  const viewsContract = environment.contracts.views;
28
46
  const homeViewsContract = homeEnvironment.contracts.views;
29
47
 
30
- const marketData = await Promise.all([
48
+ const [
49
+ protocolInfoResult,
50
+ allMarketsInfoResult,
51
+ nativePriceResult,
52
+ govPriceResult,
53
+ ] = await Promise.allSettled([
31
54
  viewsContract?.read.getProtocolInfo(),
32
55
  viewsContract?.read.getAllMarketsInfo(),
33
56
  homeViewsContract?.read.getNativeTokenPrice(),
34
57
  homeViewsContract?.read.getGovernanceTokenPrice(),
35
58
  ]);
36
59
 
37
- const { seizePaused, transferPaused } = marketData[0]!;
38
- const allMarketsInfo = marketData[1]!;
39
- const nativeTokenPriceRaw = marketData[2]!;
40
- const governanceTokenPriceRaw = marketData[3]!;
60
+ // If getAllMarketsInfo failed (e.g. broken on-chain oracle), fall back to
61
+ // per-mToken RPC calls. This handles deprecated chains like Moonriver where
62
+ // the price oracle is non-functional but individual mToken data is readable.
63
+ if (allMarketsInfoResult.status === "rejected") {
64
+ console.debug(
65
+ "[mToken fallback] getAllMarketsInfo failed, using per-mToken fallback:",
66
+ allMarketsInfoResult.reason,
67
+ );
68
+ const seizePaused =
69
+ protocolInfoResult.status === "fulfilled"
70
+ ? protocolInfoResult.value!.seizePaused
71
+ : false;
72
+ const transferPaused =
73
+ protocolInfoResult.status === "fulfilled"
74
+ ? protocolInfoResult.value!.transferPaused
75
+ : false;
76
+ return await getMarketsFromMTokenFallback(
77
+ environment,
78
+ seizePaused,
79
+ transferPaused,
80
+ );
81
+ }
82
+
83
+ const { seizePaused, transferPaused } =
84
+ protocolInfoResult.status === "fulfilled"
85
+ ? protocolInfoResult.value!
86
+ : { seizePaused: false, transferPaused: false };
87
+ const allMarketsInfo = allMarketsInfoResult.value!;
88
+ const nativeTokenPriceRaw =
89
+ nativePriceResult.status === "fulfilled" ? nativePriceResult.value! : 0n;
90
+ const governanceTokenPriceRaw =
91
+ govPriceResult.status === "fulfilled" ? govPriceResult.value! : 0n;
41
92
 
42
93
  const governanceTokenPrice = new Amount(governanceTokenPriceRaw, 18);
43
94
  const nativeTokenPrice = new Amount(nativeTokenPriceRaw, 18);
@@ -185,6 +236,8 @@ export const getMarketsData = async (environment: Environment) => {
185
236
  : tokenPrice;
186
237
 
187
238
  if (price) {
239
+ // USDC on-chain returns borrowIncentivesPerSec=1 (1 wei) as a
240
+ // placeholder when there are no active borrow incentives. Treat as zero.
188
241
  if (token.symbol === "USDC" && borrowIncentivesPerSec === 1n) {
189
242
  borrowIncentivesPerSec = 0n;
190
243
  }
@@ -202,6 +255,7 @@ export const getMarketsData = async (environment: Environment) => {
202
255
  : (supplyRewardsPerDayUsd / totalSupplyUsd) *
203
256
  DAYS_PER_YEAR *
204
257
  100;
258
+ // Negative: borrow reward APR reduces the effective borrowing cost
205
259
  const borrowApr =
206
260
  totalBorrowsUsd === 0
207
261
  ? 0
@@ -236,6 +290,447 @@ export const getMarketsData = async (environment: Environment) => {
236
290
  return markets;
237
291
  };
238
292
 
293
+ /**
294
+ * Fallback for chains whose on-chain price oracle is non-functional (e.g.
295
+ * deprecated Moonriver). Reads raw mToken contract data individually via
296
+ * Promise.allSettled so a single failed call does not abort the entire chain.
297
+ * All USD/price values are set to 0 since oracle prices are unavailable.
298
+ */
299
+ async function getMarketsFromMTokenFallback(
300
+ environment: Environment,
301
+ seizePaused: boolean,
302
+ transferPaused: boolean,
303
+ ): Promise<Market[]> {
304
+ const markets: Market[] = [];
305
+
306
+ for (const marketKey of Object.keys(environment.config.markets)) {
307
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
308
+ const envAny = environment as any;
309
+ const marketConfig = envAny.config.markets[marketKey] as
310
+ | { underlyingToken: string; marketToken: string; deprecated?: boolean }
311
+ | undefined;
312
+ if (!marketConfig) continue;
313
+
314
+ const underlyingToken = envAny.config.tokens[
315
+ marketConfig.underlyingToken
316
+ ] as
317
+ | {
318
+ address: `0x${string}`;
319
+ decimals: number;
320
+ symbol: string;
321
+ name: string;
322
+ }
323
+ | undefined;
324
+ const marketToken = envAny.config.tokens[marketConfig.marketToken] as
325
+ | {
326
+ address: `0x${string}`;
327
+ decimals: number;
328
+ symbol: string;
329
+ name: string;
330
+ }
331
+ | undefined;
332
+ if (!underlyingToken || !marketToken) continue;
333
+
334
+ const mTokenContract = envAny.markets[marketKey] as
335
+ | { read: Record<string, (...args: unknown[]) => Promise<bigint>> }
336
+ | undefined;
337
+ if (!mTokenContract) continue;
338
+
339
+ const [
340
+ totalSupplyResult,
341
+ totalBorrowsResult,
342
+ totalReservesResult,
343
+ cashResult,
344
+ exchangeRateResult,
345
+ supplyRateResult,
346
+ borrowRateResult,
347
+ reserveFactorResult,
348
+ ] = await Promise.allSettled([
349
+ mTokenContract.read.totalSupply(),
350
+ mTokenContract.read.totalBorrows(),
351
+ mTokenContract.read.totalReserves(),
352
+ mTokenContract.read.getCash(),
353
+ mTokenContract.read.exchangeRateStored(),
354
+ mTokenContract.read.supplyRatePerTimestamp(),
355
+ mTokenContract.read.borrowRatePerTimestamp(),
356
+ mTokenContract.read.reserveFactorMantissa(),
357
+ ]);
358
+
359
+ const totalSupplyRaw =
360
+ totalSupplyResult.status === "fulfilled" ? totalSupplyResult.value : 0n;
361
+ const totalBorrowsRaw =
362
+ totalBorrowsResult.status === "fulfilled" ? totalBorrowsResult.value : 0n;
363
+ const totalReservesRaw =
364
+ totalReservesResult.status === "fulfilled"
365
+ ? totalReservesResult.value
366
+ : 0n;
367
+ const cashRaw = cashResult.status === "fulfilled" ? cashResult.value : 0n;
368
+ // Default exchange rate of 1.0: 10^(10 + underlyingDecimals) in raw form
369
+ const exchangeRateRaw =
370
+ exchangeRateResult.status === "fulfilled"
371
+ ? exchangeRateResult.value
372
+ : 10n ** BigInt(10 + underlyingToken.decimals);
373
+ const supplyRateRaw =
374
+ supplyRateResult.status === "fulfilled" ? supplyRateResult.value : 0n;
375
+ const borrowRateRaw =
376
+ borrowRateResult.status === "fulfilled" ? borrowRateResult.value : 0n;
377
+ const reserveFactorRaw =
378
+ reserveFactorResult.status === "fulfilled"
379
+ ? reserveFactorResult.value
380
+ : 0n;
381
+
382
+ const exchangeRate = new Amount(
383
+ exchangeRateRaw,
384
+ 10 + underlyingToken.decimals,
385
+ ).value;
386
+ const marketTotalSupply = new Amount(totalSupplyRaw, marketToken.decimals);
387
+ const totalSupply = new Amount(
388
+ marketTotalSupply.value * exchangeRate,
389
+ underlyingToken.decimals,
390
+ );
391
+ const totalBorrows = new Amount(totalBorrowsRaw, underlyingToken.decimals);
392
+ const totalReserves = new Amount(
393
+ totalReservesRaw,
394
+ underlyingToken.decimals,
395
+ );
396
+ const cash = new Amount(cashRaw, underlyingToken.decimals);
397
+ const supplyRate = new Amount(supplyRateRaw, 18);
398
+ const borrowRate = new Amount(borrowRateRaw, 18);
399
+ const reserveFactor = new Amount(reserveFactorRaw, 18).value;
400
+
401
+ const baseSupplyApy = calculateApy(supplyRate.value);
402
+ const baseBorrowApy = calculateApy(borrowRate.value);
403
+
404
+ const market: Market = {
405
+ marketKey,
406
+ chainId: environment.chainId,
407
+ seizePaused,
408
+ transferPaused,
409
+ // Oracle is non-functional so supply/borrow would fail on-chain; mark paused
410
+ mintPaused: true,
411
+ borrowPaused: true,
412
+ deprecated: marketConfig.deprecated === true,
413
+ borrowCaps: new Amount(0n, underlyingToken.decimals),
414
+ borrowCapsUsd: 0,
415
+ cash,
416
+ collateralFactor: 0,
417
+ exchangeRate,
418
+ marketToken,
419
+ reserveFactor,
420
+ supplyCaps: new Amount(0n, underlyingToken.decimals),
421
+ supplyCapsUsd: 0,
422
+ badDebt: new Amount(0n, underlyingToken.decimals),
423
+ badDebtUsd: 0,
424
+ totalBorrows,
425
+ totalBorrowsUsd: 0,
426
+ totalReserves,
427
+ totalReservesUsd: 0,
428
+ totalSupply,
429
+ totalSupplyUsd: 0,
430
+ underlyingPrice: 0,
431
+ underlyingToken,
432
+ baseBorrowApy,
433
+ baseSupplyApy,
434
+ totalBorrowApr: baseBorrowApy,
435
+ totalSupplyApr: baseSupplyApy,
436
+ rewards: [],
437
+ };
438
+
439
+ markets.push(market);
440
+ }
441
+
442
+ return markets;
443
+ }
444
+
445
+ /**
446
+ * Fetch markets data from Lunar Indexer (hybrid approach)
447
+ *
448
+ * Uses Lunar for core market data and conditionally:
449
+ * - If Lunar provides priceUsd/supplyApr/borrowApr in incentives: use those (NO RPC calls)
450
+ * - If Lunar fields are null: fetch governance/native token prices for reward APR calculations (RPC calls)
451
+ * - Always fetch liquid staking APRs from external APIs
452
+ */
453
+ async function fetchMarketsFromLunar(
454
+ environment: Environment,
455
+ ): Promise<Market[]> {
456
+ if (!environment.lunarIndexerUrl) {
457
+ throw new Error("Lunar Indexer URL not configured");
458
+ }
459
+
460
+ // Import client dynamically to avoid circular dependencies
461
+ const { createLunarIndexerClient, DEFAULT_LUNAR_TIMEOUT_MS } = await import(
462
+ "../../lunar-indexer-client.js"
463
+ );
464
+
465
+ const client = createLunarIndexerClient({
466
+ baseUrl: environment.lunarIndexerUrl,
467
+ timeout: DEFAULT_LUNAR_TIMEOUT_MS,
468
+ });
469
+
470
+ const lunarMarketsResponse = await client.listMarkets(environment.chainId);
471
+ const lunarMarkets = lunarMarketsResponse.results;
472
+
473
+ const needsRpcPrices = lunarMarkets.some((market) =>
474
+ market.incentives.some(
475
+ (incentive) =>
476
+ incentive.priceUsd === null ||
477
+ incentive.supplyApr === null ||
478
+ incentive.borrowApr === null,
479
+ ),
480
+ );
481
+
482
+ let governanceTokenPrice: Amount | undefined;
483
+ let nativeTokenPrice: Amount | undefined;
484
+
485
+ if (needsRpcPrices) {
486
+ const homeEnvironment =
487
+ (Object.values(publicEnvironments) as Environment[]).find((e) =>
488
+ e.custom?.governance?.chainIds?.includes(environment.chainId),
489
+ ) || environment;
490
+
491
+ const [nativeTokenPriceRaw, governanceTokenPriceRaw] = await Promise.all([
492
+ homeEnvironment.contracts.views?.read.getNativeTokenPrice(),
493
+ homeEnvironment.contracts.views?.read.getGovernanceTokenPrice(),
494
+ ]);
495
+
496
+ if (!nativeTokenPriceRaw || !governanceTokenPriceRaw) {
497
+ throw new Error(
498
+ "Failed to fetch native or governance token prices from home chain",
499
+ );
500
+ }
501
+
502
+ governanceTokenPrice = new Amount(governanceTokenPriceRaw, 18);
503
+ nativeTokenPrice = new Amount(nativeTokenPriceRaw, 18);
504
+ }
505
+
506
+ const markets: Market[] = [];
507
+
508
+ for (const lunarMarket of lunarMarkets) {
509
+ const marketFound = findMarketByAddress(
510
+ environment,
511
+ lunarMarket.address as `0x${string}`,
512
+ );
513
+
514
+ if (!marketFound) {
515
+ continue;
516
+ }
517
+
518
+ const { marketConfig, marketToken, underlyingToken, marketKey } =
519
+ marketFound;
520
+
521
+ // Transform Lunar decimal numbers to SDK Amount types
522
+ // Note: Number() wrapping is defensive — the Lunar API may return numeric
523
+ // fields as strings, which would break BigInt conversion via Math.floor.
524
+ const totalSupply = new Amount(
525
+ BigInt(
526
+ Math.floor(
527
+ Number(lunarMarket.totalSupply) * 10 ** underlyingToken.decimals,
528
+ ),
529
+ ),
530
+ underlyingToken.decimals,
531
+ );
532
+
533
+ const totalBorrows = new Amount(
534
+ BigInt(
535
+ Math.floor(
536
+ Number(lunarMarket.totalBorrows) * 10 ** underlyingToken.decimals,
537
+ ),
538
+ ),
539
+ underlyingToken.decimals,
540
+ );
541
+
542
+ const totalReserves = new Amount(
543
+ BigInt(
544
+ Math.floor(
545
+ Number(lunarMarket.totalReserves) * 10 ** underlyingToken.decimals,
546
+ ),
547
+ ),
548
+ underlyingToken.decimals,
549
+ );
550
+
551
+ const cash = new Amount(
552
+ BigInt(
553
+ Math.floor(Number(lunarMarket.cash) * 10 ** underlyingToken.decimals),
554
+ ),
555
+ underlyingToken.decimals,
556
+ );
557
+
558
+ const badDebt = new Amount(
559
+ BigInt(
560
+ Math.floor(
561
+ Number(lunarMarket.badDebt) * 10 ** underlyingToken.decimals,
562
+ ),
563
+ ),
564
+ underlyingToken.decimals,
565
+ );
566
+
567
+ const supplyCaps = new Amount(
568
+ BigInt(
569
+ Math.floor(
570
+ Number(lunarMarket.supplyCap) * 10 ** underlyingToken.decimals,
571
+ ),
572
+ ),
573
+ underlyingToken.decimals,
574
+ );
575
+
576
+ const borrowCaps = new Amount(
577
+ BigInt(
578
+ Math.floor(
579
+ Number(lunarMarket.borrowCap) * 10 ** underlyingToken.decimals,
580
+ ),
581
+ ),
582
+ underlyingToken.decimals,
583
+ );
584
+
585
+ // Lunar provides reserveFactor as wei string, convert to decimal
586
+ const reserveFactor = new Amount(BigInt(lunarMarket.reserveFactor), 18)
587
+ .value;
588
+
589
+ const market: Market = {
590
+ marketKey,
591
+ chainId: environment.chainId,
592
+ seizePaused: lunarMarket.seizePaused,
593
+ transferPaused: lunarMarket.transferPaused,
594
+ mintPaused: lunarMarket.mintPaused,
595
+ borrowPaused: lunarMarket.borrowPaused,
596
+ deprecated: marketConfig.deprecated === true,
597
+ borrowCaps,
598
+ borrowCapsUsd:
599
+ Number(lunarMarket.borrowCap) * Number(lunarMarket.priceUsd),
600
+ cash,
601
+ collateralFactor: Number(lunarMarket.collateralFactor),
602
+ exchangeRate: Number(lunarMarket.exchangeRate),
603
+ marketToken,
604
+ reserveFactor,
605
+ supplyCaps,
606
+ supplyCapsUsd:
607
+ Number(lunarMarket.supplyCap) * Number(lunarMarket.priceUsd),
608
+ badDebt,
609
+ badDebtUsd: Number(lunarMarket.badDebtUsd),
610
+ totalBorrows,
611
+ totalBorrowsUsd: Number(lunarMarket.totalBorrowsUsd),
612
+ totalReserves,
613
+ totalReservesUsd: Number(lunarMarket.totalReservesUsd),
614
+ totalSupply,
615
+ totalSupplyUsd: Number(lunarMarket.totalSupplyUsd),
616
+ underlyingPrice: Number(lunarMarket.priceUsd),
617
+ underlyingToken,
618
+ baseBorrowApy: Number(lunarMarket.baseBorrowApy),
619
+ baseSupplyApy: Number(lunarMarket.baseSupplyApy),
620
+ totalBorrowApr: 0,
621
+ totalSupplyApr: 0,
622
+ rewards: [],
623
+ };
624
+
625
+ for (const incentive of lunarMarket.incentives) {
626
+ const token = findTokenByAddress(
627
+ environment,
628
+ incentive.token as `0x${string}`,
629
+ );
630
+ if (!token) {
631
+ continue;
632
+ }
633
+
634
+ let supplyApr: number;
635
+ let borrowApr: number;
636
+
637
+ if (
638
+ incentive.priceUsd !== null &&
639
+ incentive.supplyApr !== null &&
640
+ incentive.borrowApr !== null
641
+ ) {
642
+ supplyApr = Number(incentive.supplyApr);
643
+ borrowApr = Number(incentive.borrowApr);
644
+ } else {
645
+ const isGovernanceToken =
646
+ token.symbol === environment.custom?.governance?.token;
647
+ const isNativeToken = token.address === zeroAddress;
648
+
649
+ const price = isNativeToken
650
+ ? nativeTokenPrice?.value
651
+ : isGovernanceToken
652
+ ? governanceTokenPrice?.value
653
+ : undefined;
654
+
655
+ if (!price) {
656
+ continue;
657
+ }
658
+
659
+ let borrowIncentivesPerSec = BigInt(incentive.borrowIncentivesPerSec);
660
+ const supplyIncentivesPerSec = BigInt(incentive.supplyIncentivesPerSec);
661
+
662
+ // USDC on-chain returns borrowIncentivesPerSec=1 (1 wei) as a
663
+ // placeholder when there are no active borrow incentives. Treat as zero.
664
+ if (token.symbol === "USDC" && borrowIncentivesPerSec === 1n) {
665
+ borrowIncentivesPerSec = 0n;
666
+ }
667
+
668
+ const supplyRewardsPerDayUsd =
669
+ perDay(new Amount(supplyIncentivesPerSec, token.decimals).value) *
670
+ price;
671
+ const borrowRewardsPerDayUsd =
672
+ perDay(new Amount(borrowIncentivesPerSec, token.decimals).value) *
673
+ price;
674
+
675
+ supplyApr =
676
+ Number(lunarMarket.totalSupplyUsd) === 0
677
+ ? 0
678
+ : (supplyRewardsPerDayUsd / Number(lunarMarket.totalSupplyUsd)) *
679
+ DAYS_PER_YEAR *
680
+ 100;
681
+ // Negative: borrow reward APR reduces the effective borrowing cost
682
+ borrowApr =
683
+ Number(lunarMarket.totalBorrowsUsd) === 0
684
+ ? 0
685
+ : (borrowRewardsPerDayUsd / Number(lunarMarket.totalBorrowsUsd)) *
686
+ DAYS_PER_YEAR *
687
+ 100 *
688
+ -1;
689
+ }
690
+
691
+ market.rewards.push({
692
+ liquidStakingApr: 0,
693
+ borrowApr,
694
+ supplyApr,
695
+ token,
696
+ });
697
+ }
698
+
699
+ market.totalSupplyApr = market.rewards.reduce(
700
+ (prev, curr) => prev + curr.supplyApr,
701
+ market.baseSupplyApy,
702
+ );
703
+ market.totalBorrowApr = market.rewards.reduce(
704
+ (prev, curr) => prev + curr.borrowApr,
705
+ market.baseBorrowApy,
706
+ );
707
+
708
+ markets.push(market);
709
+ }
710
+
711
+ // Still fetch liquid staking rewards from external APIs
712
+ const liquidStakingRewards = await fetchLiquidStakingRewards();
713
+
714
+ for (const market of markets) {
715
+ const symbol = market.underlyingToken.symbol;
716
+ if (symbol in liquidStakingRewards) {
717
+ const liquidStakingApr =
718
+ liquidStakingRewards[symbol as keyof typeof liquidStakingRewards];
719
+
720
+ market.rewards.push({
721
+ token: market.underlyingToken,
722
+ supplyApr: liquidStakingApr,
723
+ borrowApr: 0,
724
+ liquidStakingApr,
725
+ });
726
+
727
+ market.totalSupplyApr += liquidStakingApr;
728
+ }
729
+ }
730
+
731
+ return markets;
732
+ }
733
+
239
734
  const fetchFromGenericCacheApi = async <T>(uri: string): Promise<T> => {
240
735
  const response = await fetch(
241
736
  "https://generic-api-cache.moonwell.workers.dev/",