@defisaver/positions-sdk 2.1.161-portfolio-api-2-dev → 2.1.161-portfolio-api-3-dev

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 (166) hide show
  1. package/cjs/aaveV2/index.js +2 -0
  2. package/cjs/aaveV3/index.d.ts +39 -0
  3. package/cjs/aaveV3/index.js +237 -2
  4. package/cjs/aaveV4/lend.d.ts +2 -43
  5. package/cjs/curveUsd/index.d.ts +5 -2
  6. package/cjs/curveUsd/index.js +30 -15
  7. package/cjs/fluid/index.d.ts +5 -0
  8. package/cjs/fluid/index.js +36 -1
  9. package/cjs/helpers/aaveHelpers/index.d.ts +9 -1
  10. package/cjs/helpers/aaveHelpers/index.js +26 -6
  11. package/cjs/helpers/aaveV4Helpers/index.js +1 -0
  12. package/cjs/helpers/compoundHelpers/index.js +2 -0
  13. package/cjs/helpers/curveUsdHelpers/index.d.ts +3 -1
  14. package/cjs/helpers/curveUsdHelpers/index.js +15 -2
  15. package/cjs/helpers/fluidHelpers/index.js +1 -0
  16. package/cjs/helpers/liquityV2Helpers/index.js +1 -0
  17. package/cjs/helpers/llamaLendHelpers/index.js +1 -0
  18. package/cjs/helpers/makerHelpers/index.d.ts +1 -1
  19. package/cjs/helpers/makerHelpers/index.js +2 -2
  20. package/cjs/helpers/morphoBlueHelpers/index.js +2 -0
  21. package/cjs/helpers/morphoMidnightHelpers/index.js +2 -0
  22. package/cjs/helpers/sparkHelpers/index.js +1 -0
  23. package/cjs/liquity/index.js +15 -2
  24. package/cjs/liquityV2/index.d.ts +5 -0
  25. package/cjs/liquityV2/index.js +37 -19
  26. package/cjs/maker/index.d.ts +7 -2
  27. package/cjs/maker/index.js +44 -12
  28. package/cjs/markets/index.d.ts +1 -0
  29. package/cjs/markets/index.js +3 -1
  30. package/cjs/markets/maker/index.d.ts +1 -0
  31. package/cjs/markets/maker/index.js +13 -0
  32. package/cjs/moneymarket/moneymarketCommonService.d.ts +9 -0
  33. package/cjs/moneymarket/moneymarketCommonService.js +13 -1
  34. package/cjs/portfolio/index.d.ts +17 -5
  35. package/cjs/portfolio/index.js +583 -394
  36. package/cjs/portfolio/lendingProtocols.d.ts +184 -0
  37. package/cjs/portfolio/lendingProtocols.js +196 -0
  38. package/cjs/savings/index.d.ts +8 -2
  39. package/cjs/savings/index.js +37 -5
  40. package/cjs/savings/morphoVaults/index.d.ts +1 -1
  41. package/cjs/savings/morphoVaults/index.js +56 -18
  42. package/cjs/savings/morphoVaults/options.d.ts +2 -0
  43. package/cjs/savings/morphoVaults/options.js +61 -1
  44. package/cjs/spark/index.js +1 -0
  45. package/cjs/staking/staking.d.ts +20 -0
  46. package/cjs/staking/staking.js +19 -1
  47. package/cjs/types/aave.d.ts +3 -0
  48. package/cjs/types/aaveV4.d.ts +45 -0
  49. package/cjs/types/compound.d.ts +2 -0
  50. package/cjs/types/curveUsd.d.ts +6 -0
  51. package/cjs/types/fluid.d.ts +1 -0
  52. package/cjs/types/liquity.d.ts +4 -0
  53. package/cjs/types/liquityV2.d.ts +2 -0
  54. package/cjs/types/maker.d.ts +4 -0
  55. package/cjs/types/morphoBlue.d.ts +2 -0
  56. package/cjs/types/morphoMidnight.d.ts +1 -0
  57. package/cjs/types/portfolio.d.ts +64 -6
  58. package/cjs/types/savings/index.d.ts +9 -1
  59. package/cjs/types/savings/morphoVaults.d.ts +10 -2
  60. package/cjs/types/savings/morphoVaults.js +9 -1
  61. package/cjs/types/spark.d.ts +2 -0
  62. package/esm/aaveV2/index.js +2 -0
  63. package/esm/aaveV3/index.d.ts +39 -0
  64. package/esm/aaveV3/index.js +231 -2
  65. package/esm/aaveV4/lend.d.ts +2 -43
  66. package/esm/curveUsd/index.d.ts +5 -2
  67. package/esm/curveUsd/index.js +30 -15
  68. package/esm/fluid/index.d.ts +5 -0
  69. package/esm/fluid/index.js +33 -0
  70. package/esm/helpers/aaveHelpers/index.d.ts +9 -1
  71. package/esm/helpers/aaveHelpers/index.js +23 -5
  72. package/esm/helpers/aaveV4Helpers/index.js +1 -0
  73. package/esm/helpers/compoundHelpers/index.js +2 -0
  74. package/esm/helpers/curveUsdHelpers/index.d.ts +3 -1
  75. package/esm/helpers/curveUsdHelpers/index.js +16 -3
  76. package/esm/helpers/fluidHelpers/index.js +1 -0
  77. package/esm/helpers/liquityV2Helpers/index.js +1 -0
  78. package/esm/helpers/llamaLendHelpers/index.js +2 -1
  79. package/esm/helpers/makerHelpers/index.d.ts +1 -1
  80. package/esm/helpers/makerHelpers/index.js +2 -2
  81. package/esm/helpers/morphoBlueHelpers/index.js +2 -0
  82. package/esm/helpers/morphoMidnightHelpers/index.js +2 -0
  83. package/esm/helpers/sparkHelpers/index.js +1 -0
  84. package/esm/liquity/index.js +16 -3
  85. package/esm/liquityV2/index.d.ts +5 -0
  86. package/esm/liquityV2/index.js +35 -18
  87. package/esm/maker/index.d.ts +7 -2
  88. package/esm/maker/index.js +44 -14
  89. package/esm/markets/index.d.ts +1 -0
  90. package/esm/markets/index.js +1 -0
  91. package/esm/markets/maker/index.d.ts +1 -0
  92. package/esm/markets/maker/index.js +10 -0
  93. package/esm/moneymarket/moneymarketCommonService.d.ts +9 -0
  94. package/esm/moneymarket/moneymarketCommonService.js +11 -0
  95. package/esm/portfolio/index.d.ts +17 -5
  96. package/esm/portfolio/index.js +584 -398
  97. package/esm/portfolio/lendingProtocols.d.ts +184 -0
  98. package/esm/portfolio/lendingProtocols.js +189 -0
  99. package/esm/savings/index.d.ts +8 -2
  100. package/esm/savings/index.js +35 -4
  101. package/esm/savings/morphoVaults/index.d.ts +1 -1
  102. package/esm/savings/morphoVaults/index.js +57 -19
  103. package/esm/savings/morphoVaults/options.d.ts +2 -0
  104. package/esm/savings/morphoVaults/options.js +61 -1
  105. package/esm/spark/index.js +1 -0
  106. package/esm/staking/staking.d.ts +20 -0
  107. package/esm/staking/staking.js +17 -0
  108. package/esm/types/aave.d.ts +3 -0
  109. package/esm/types/aaveV4.d.ts +45 -0
  110. package/esm/types/compound.d.ts +2 -0
  111. package/esm/types/curveUsd.d.ts +6 -0
  112. package/esm/types/fluid.d.ts +1 -0
  113. package/esm/types/liquity.d.ts +4 -0
  114. package/esm/types/liquityV2.d.ts +2 -0
  115. package/esm/types/maker.d.ts +4 -0
  116. package/esm/types/morphoBlue.d.ts +2 -0
  117. package/esm/types/morphoMidnight.d.ts +1 -0
  118. package/esm/types/portfolio.d.ts +64 -6
  119. package/esm/types/savings/index.d.ts +9 -1
  120. package/esm/types/savings/morphoVaults.d.ts +10 -2
  121. package/esm/types/savings/morphoVaults.js +8 -0
  122. package/esm/types/spark.d.ts +2 -0
  123. package/package.json +3 -2
  124. package/src/aaveV2/index.ts +2 -0
  125. package/src/aaveV3/index.ts +291 -1
  126. package/src/aaveV4/lend.ts +2 -48
  127. package/src/curveUsd/index.ts +33 -16
  128. package/src/fluid/index.ts +40 -0
  129. package/src/helpers/aaveHelpers/index.ts +27 -7
  130. package/src/helpers/aaveV4Helpers/index.ts +1 -0
  131. package/src/helpers/compoundHelpers/index.ts +2 -0
  132. package/src/helpers/curveUsdHelpers/index.ts +23 -3
  133. package/src/helpers/fluidHelpers/index.ts +1 -0
  134. package/src/helpers/liquityV2Helpers/index.ts +1 -0
  135. package/src/helpers/llamaLendHelpers/index.ts +2 -1
  136. package/src/helpers/makerHelpers/index.ts +2 -1
  137. package/src/helpers/morphoBlueHelpers/index.ts +2 -0
  138. package/src/helpers/morphoMidnightHelpers/index.ts +2 -0
  139. package/src/helpers/sparkHelpers/index.ts +1 -0
  140. package/src/liquity/index.ts +17 -3
  141. package/src/liquityV2/index.ts +45 -19
  142. package/src/maker/index.ts +77 -30
  143. package/src/markets/index.ts +2 -1
  144. package/src/markets/maker/index.ts +10 -0
  145. package/src/moneymarket/moneymarketCommonService.ts +15 -0
  146. package/src/portfolio/index.ts +428 -215
  147. package/src/portfolio/lendingProtocols.ts +281 -0
  148. package/src/savings/index.ts +45 -5
  149. package/src/savings/morphoVaults/index.ts +73 -21
  150. package/src/savings/morphoVaults/options.ts +67 -1
  151. package/src/spark/index.ts +1 -0
  152. package/src/staking/staking.ts +28 -0
  153. package/src/types/aave.ts +4 -0
  154. package/src/types/aaveV4.ts +50 -0
  155. package/src/types/compound.ts +2 -0
  156. package/src/types/curveUsd.ts +7 -0
  157. package/src/types/fluid.ts +1 -0
  158. package/src/types/liquity.ts +6 -0
  159. package/src/types/liquityV2.ts +2 -0
  160. package/src/types/maker.ts +6 -0
  161. package/src/types/morphoBlue.ts +2 -0
  162. package/src/types/morphoMidnight.ts +1 -0
  163. package/src/types/portfolio.ts +71 -6
  164. package/src/types/savings/index.ts +12 -1
  165. package/src/types/savings/morphoVaults.ts +11 -1
  166. package/src/types/spark.ts +2 -0
@@ -7,141 +7,155 @@ import {
7
7
  CrvUsdMarkets,
8
8
  LiquityV2Markets,
9
9
  LlamaLendMarkets,
10
+ MakerActiveIlks,
10
11
  MorphoBlueMarkets,
11
- MorphoMidnightMarkets,
12
12
  SparkMarkets,
13
13
  } from '../markets';
14
14
  import { _getMorphoBlueAccountData, _getMorphoBluePortfolioMarketData, getMorphoEarn } from '../morphoBlue';
15
- import { _getMorphoMidnightAccountData, _getMorphoMidnightMarketData } from '../morphoMidnight';
16
15
  import {
17
- AaveV2MarketData,
18
- AaveV3MarketData,
19
- AaveV4SpokeData,
20
16
  AaveVersions,
21
17
  CdpInfo,
22
- CompoundV2MarketsData,
23
- CompoundV3MarketsData,
24
18
  CompoundVersions,
25
- CrvUSDGlobalMarketData,
26
19
  LiquityV2MarketData,
27
- LlamaLendGlobalMarketData,
28
- MorphoBlueMarketInfo,
29
- MorphoMidnightMarketInfo,
20
+ LiquityV2Versions,
21
+ PortfolioData,
30
22
  PortfolioDataOptions,
23
+ PortfolioLiquityV2Troves,
31
24
  PortfolioMarketsData,
25
+ PortfolioMarketsErrors,
26
+ PortfolioMarketsResult,
32
27
  PortfolioPositionsData,
28
+ PortfolioPositionsDataForAddress,
33
29
  PortfolioRewardsData,
34
30
  PortfolioStakingPositionsData,
35
- SparkMarketsData,
31
+ PortfolioUserData,
32
+ ShifterPortfolioMarketsData,
36
33
  } from '../types';
37
34
  import { _getCompoundV3AccountData, _getCompoundV3MarketsData } from '../compoundV3';
38
35
  import { _getSparkAccountData, _getSparkMarketsData } from '../spark';
39
36
  import { _getCurveUsdGlobalData, _getCurveUsdUserData } from '../curveUsd';
40
37
  import { _getLlamaLendGlobalData, _getLlamaLendUserData } from '../llamaLend';
41
38
  import { _getAaveV3AccountData, _getAaveV3MarketData, getStakeAaveData } from '../aaveV3';
39
+ import { _getAaveV4AccountData, _getAaveV4SpokeData } from '../aaveV4';
42
40
  import { ZERO_ADDRESS } from '../constants';
43
- import { _getMakerCdpData, _getUserCdps } from '../maker';
41
+ import { _getMakerCdpData, _getMakerIlksData, _getUserCdps } from '../maker';
44
42
  import { _getAaveV2AccountData, _getAaveV2MarketsData } from '../aaveV2';
45
43
  import { _getCompoundV2AccountData, _getCompoundV2MarketsData } from '../compoundV2';
46
44
  import { getViemProvider } from '../services/viem';
47
45
  import { _getLiquityTroveInfo, getLiquityStakingData } from '../liquity';
48
- import { _getLiquityV2MarketData, getLiquitySAndYBold, getLiquityV2Staking } from '../liquityV2';
49
- import { _getAllUserEarnPositionsWithFTokens, _getUserPositionsPortfolio } from '../fluid';
46
+ import {
47
+ _getLiquityV2MarketData, _getLiquityV2TroveData, _getLiquityV2UsersTroveIds, getLiquitySAndYBold, getLiquityV2Staking,
48
+ } from '../liquityV2';
49
+ import { _getAllFluidMarketDataPortfolio, _getAllUserEarnPositionsWithFTokens, _getUserPositionsPortfolio } from '../fluid';
50
50
  import { getUmbrellaData } from '../umbrella';
51
51
  import { getMerklUnclaimedRewards, getUnclaimedRewardsForAllMarkets } from '../claiming/aaveV3';
52
52
  import { fetchSparkAirdropRewards, fetchSparkRewards } from '../claiming/spark';
53
53
  import { getKingRewards } from '../claiming/king';
54
54
  import { fetchEthenaAirdropRewards } from '../claiming/ethena';
55
- import { _getAaveV4AccountData, _getAaveV4SpokeData } from '../aaveV4';
56
55
  import { getUniswapRewards } from '../claiming/uniswap';
56
+ import {
57
+ aaveV3Lending, LENDING_PROTOCOLS, LendingKey, morphoBlueLending, requireMarket, sparkLending,
58
+ } from './lendingProtocols';
57
59
 
58
- export async function getPortfolioData(provider: EthereumProvider, network: NetworkNumber, defaultProvider: EthereumProvider, addresses: EthAddress[], isSim = false, options: PortfolioDataOptions = {}): Promise<{
59
- positions: PortfolioPositionsData;
60
- stakingPositions: PortfolioStakingPositionsData;
61
- rewardsData: PortfolioRewardsData;
62
- markets: PortfolioMarketsData;
63
- }> {
64
- const isMainnet = network === NetworkNumber.Eth;
65
- const includeStaking = options.staking !== false;
66
- const includeRewards = options.rewards !== false;
67
- // The addresses staking and rewards are read for: none when left out, so their fetches below make no calls.
68
- const stakingAddresses = includeStaking ? addresses : [];
69
- const rewardsAddresses = includeRewards ? addresses : [];
70
- const isFluidSupported = [NetworkNumber.Eth, NetworkNumber.Arb, NetworkNumber.Base, NetworkNumber.Plasma].includes(network);
71
-
72
- const morphoMarkets = Object.values(MorphoBlueMarkets(network)).filter((market) => market.chainIds.includes(network));
73
- const morphoMidnightMarkets = Object.values(MorphoMidnightMarkets(network)).filter((market) => market.chainIds.includes(network));
74
- const compoundV3Markets = Object.values(CompoundMarkets(network)).filter((market) => market.chainIds.includes(network) && market.value !== CompoundVersions.CompoundV2);
75
- const sparkMarkets = Object.values(SparkMarkets(network)).filter((market) => market.chainIds.includes(network));
76
- const aaveV3Markets = [AaveVersions.AaveV3, AaveVersions.AaveV3Lido, AaveVersions.AaveV3Etherfi].map((version) => AaveMarkets(network)[version]).filter((market) => market.chainIds.includes(network));
77
- const aaveV2Markets = [AaveVersions.AaveV2].map((version) => AaveMarkets(network)[version]).filter((market) => market.chainIds.includes(network));
78
- const compoundV2Markets = [CompoundVersions.CompoundV2].map((version) => CompoundMarkets(network)[version]).filter((market) => market.chainIds.includes(network));
79
- const crvUsdMarkets = Object.values(CrvUsdMarkets(network)).filter((market) => market.chainIds.includes(network));
80
- const llamaLendMarkets = [NetworkNumber.Eth, NetworkNumber.Arb].includes(network) ? Object.values(LlamaLendMarkets(network)).filter((market) => market.chainIds.includes(network)) : [];
81
- const liquityV2Markets = [NetworkNumber.Eth].includes(network) ? Object.values(LiquityV2Markets(network)) : [];
82
- const liquityV2MarketsStaking = [NetworkNumber.Eth].includes(network) ? Object.values(LiquityV2Markets(network)).filter(market => !market.isLegacy) : [];
83
- const aaveV4Spokes = Object.values(AaveV4Spokes(network)).filter((market) => market.chainIds.includes(network));
60
+ type PortfolioClient = ReturnType<typeof getViemProvider>;
84
61
 
62
+ /** The markets of the protocols read by their own code below; LENDING_PROTOCOLS has the others'. */
63
+ const getPortfolioMarkets = (network: NetworkNumber) => ({
64
+ liquityV2Markets: [NetworkNumber.Eth].includes(network) ? Object.values(LiquityV2Markets(network)) : [],
65
+ liquityV2MarketsStaking: [NetworkNumber.Eth].includes(network) ? Object.values(LiquityV2Markets(network)).filter(market => !market.isLegacy) : [],
66
+ });
85
67
 
68
+ const getPortfolioClients = (provider: EthereumProvider, defaultProvider: EthereumProvider, network: NetworkNumber, isSim: boolean) => {
86
69
  // batchSize is viem's cap on a batch's raw subcall calldata (bytes); the JSON-RPC body ends up
87
70
  // ~4x larger (hex + aggregate3 ABI + JSON overhead) and RPC providers reject bodies over
88
71
  // ~2.5MB with HTTP 413, so keep this small enough that no single body gets near that.
89
72
  // 250k gives the largest body of around 1.26MB - if we bump, we can save maybe 2-3 rpc calls but scaling takes a hit
90
73
  // (e.g. adding new markets may result in body size going over alchemy body size limit)
91
74
  const args: [NetworkNumber, any?] = [network, { batch: { multicall: { batchSize: isSim ? 2_000 : 250_000 } } }];
92
- const client = getViemProvider(provider, ...args);
93
- const defaultClient = getViemProvider(defaultProvider, ...args);
75
+ return {
76
+ client: getViemProvider(provider, ...args),
77
+ defaultClient: getViemProvider(defaultProvider, ...args),
78
+ };
79
+ };
94
80
 
95
- const morphoMarketsData: Record<string, MorphoBlueMarketInfo> = {};
96
- const morphoMidnightMarketsData: Record<string, MorphoMidnightMarketInfo> = {};
97
- const compoundV3MarketsData: Record<string, CompoundV3MarketsData> = {};
98
- const sparkMarketsData: Record<string, SparkMarketsData> = {};
99
- const aaveV3MarketsData: Record<string, AaveV3MarketData> = {};
100
- const makerCdps: Record<string, CdpInfo[]> = {};
101
- const aaveV2MarketsData: Record<string, AaveV2MarketData> = {};
102
- const compoundV2MarketsData: Record<string, CompoundV2MarketsData> = {};
103
- const crvUsdMarketsData: Record<string, CrvUSDGlobalMarketData> = {};
104
- const llamaLendMarketsData: Record<string, LlamaLendGlobalMarketData> = {};
105
- const liquityV2MarketsData: Record<string, LiquityV2MarketData> = {};
106
- const aaveV4SpokesData: Record<string, AaveV4SpokeData> = {};
81
+ const _getPortfolioMarketsData = async (client: PortfolioClient, defaultClient: PortfolioClient, network: NetworkNumber): Promise<PortfolioMarketsResult> => {
82
+ const { liquityV2Markets } = getPortfolioMarkets(network);
107
83
 
108
84
  const markets: PortfolioMarketsData = {
109
- morphoMarketsData,
110
- morphoMidnightMarketsData,
111
- compoundV3MarketsData,
112
- sparkMarketsData,
113
- aaveV3MarketsData,
114
- aaveV2MarketsData,
115
- compoundV2MarketsData,
116
- crvUsdMarketsData,
117
- llamaLendMarketsData,
118
- liquityV2MarketsData,
119
- aaveV4SpokesData,
85
+ morphoMarketsData: {},
86
+ morphoMidnightMarketsData: {},
87
+ compoundV3MarketsData: {},
88
+ sparkMarketsData: {},
89
+ aaveV3MarketsData: {},
90
+ aaveV2MarketsData: {},
91
+ compoundV2MarketsData: {},
92
+ crvUsdMarketsData: {},
93
+ llamaLendMarketsData: {},
94
+ liquityV2MarketsData: {},
95
+ aaveV4SpokesData: {},
120
96
  };
97
+ const liquityV2Failed: Record<string, string> = {};
98
+
99
+ const [lendingFailed] = await Promise.all([
100
+ Promise.all(LENDING_PROTOCOLS.map(async (protocol) => [protocol.marketsKey, await protocol.readMarkets({ client, defaultClient, network }, markets)] as const)),
101
+ // Liquity V2's markets: its troves are read by their own code below.
102
+ ...liquityV2Markets.map(async (market) => {
103
+ try {
104
+ markets.liquityV2MarketsData[market.value] = await _getLiquityV2MarketData(client, network, market);
105
+ } catch (error) {
106
+ console.error(`Error fetching liquityV2MarketsData for market ${market.value}:`, error);
107
+ liquityV2Failed[market.value] = `Error fetching liquityV2MarketsData for market ${market.value}`;
108
+ }
109
+ }),
110
+ ]);
111
+
112
+ // A market that failed is left out of markets and named here, under its protocol's key.
113
+ const errors: PortfolioMarketsErrors = {};
114
+ for (const [key, failed] of [...lendingFailed, ['liquityV2MarketsData', liquityV2Failed] as const]) {
115
+ if (Object.keys(failed).length) errors[key] = failed;
116
+ }
117
+ return { markets, errors };
118
+ };
119
+
120
+ const _getPortfolioUserData = async (
121
+ client: PortfolioClient,
122
+ network: NetworkNumber,
123
+ addresses: EthAddress[],
124
+ marketsData: Promise<PortfolioMarketsData>,
125
+ isSim: boolean,
126
+ options: PortfolioDataOptions,
127
+ ): Promise<PortfolioUserData> => {
128
+ const isMainnet = network === NetworkNumber.Eth;
129
+ const includePositions = options.positions !== false;
130
+ const includeStaking = options.staking !== false;
131
+ const includeRewards = options.rewards !== false;
132
+ const includeLiquityV2 = includePositions && options.liquityV2 === true;
133
+ // The addresses positions, staking, rewards and Liquity V2 troves are read for: none when left out, so their fetches
134
+ // below make no calls.
135
+ const positionsAddresses = includePositions ? addresses : [];
136
+ const stakingAddresses = includeStaking ? addresses : [];
137
+ const rewardsAddresses = includeRewards ? addresses : [];
138
+ const liquityV2Addresses = includeLiquityV2 ? addresses : [];
139
+ const isFluidSupported = [NetworkNumber.Eth, NetworkNumber.Arb, NetworkNumber.Base, NetworkNumber.Plasma].includes(network);
140
+
141
+ const { liquityV2Markets, liquityV2MarketsStaking } = getPortfolioMarkets(network);
121
142
 
122
143
  const positions: PortfolioPositionsData = {};
123
144
  const stakingPositions: PortfolioStakingPositionsData = {};
124
145
  const rewardsData: PortfolioRewardsData = {};
125
- const allAddresses = [...addresses];
146
+ const makerCdpsErrors: Record<EthAddress, string> = {};
126
147
 
127
- for (const address of allAddresses) {
148
+ for (const address of positionsAddresses) {
128
149
  positions[address.toLowerCase() as EthAddress] = {
129
- aaveV3: {},
130
- aaveV4: {},
131
- morphoBlue: {},
132
- morphoMidnight: {},
133
- compoundV3: {},
134
- spark: {},
150
+ // Every registry protocol, keyed by market as its reads fill it in.
151
+ ...Object.fromEntries(LENDING_PROTOCOLS.map((protocol) => [protocol.key, {}])) as Pick<PortfolioPositionsDataForAddress, LendingKey>,
135
152
  maker: {},
136
- aaveV2: {},
137
- compoundV2: {},
138
153
  liquity: {},
139
- crvUsd: {},
140
- llamaLend: {},
141
154
  fluid: {
142
155
  error: '',
143
156
  data: {},
144
157
  },
158
+ ...(includeLiquityV2 ? { liquityV2: {} } : {}),
145
159
  };
146
160
  }
147
161
 
@@ -178,60 +192,34 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
178
192
  };
179
193
  }
180
194
 
181
- await Promise.allSettled([
182
- // === MARKET DATA (needs to be fetched first) ===
183
- ...morphoMarkets.map(async (market) => {
184
- const marketData = await _getMorphoBluePortfolioMarketData(client, network, market);
185
- morphoMarketsData[market.value] = marketData;
186
- }),
187
- ...morphoMidnightMarkets.map(async (market) => {
188
- const marketData = await _getMorphoMidnightMarketData(client, network, market);
189
- morphoMidnightMarketsData[market.value] = marketData;
190
- }),
191
- ...compoundV3Markets.map(async (market) => {
192
- const marketData = await _getCompoundV3MarketsData(client, network, market, defaultClient);
193
- compoundV3MarketsData[market.value] = marketData;
194
- }),
195
- ...sparkMarkets.map(async (market) => {
196
- const marketData = await _getSparkMarketsData(client, network, market);
197
- sparkMarketsData[market.value] = marketData;
198
- }),
199
- ...aaveV3Markets.map(async (market) => {
200
- const marketData = await _getAaveV3MarketData(client, network, market);
201
- aaveV3MarketsData[market.value] = marketData;
202
- }),
203
- ...aaveV4Spokes.map(async (spoke) => {
204
- const spokeData = await _getAaveV4SpokeData(client, network, spoke);
205
- aaveV4SpokesData[spoke.value] = spokeData;
206
- }),
207
- ...aaveV2Markets.map(async (market) => {
208
- const marketData = await _getAaveV2MarketsData(client, network, market);
209
- aaveV2MarketsData[market.value] = marketData;
210
- }),
211
- ...compoundV2Markets.map(async (market) => {
212
- const marketData = await _getCompoundV2MarketsData(client, network);
213
- compoundV2MarketsData[market.value] = marketData;
214
- }),
215
- ...crvUsdMarkets.map(async (market) => {
216
- const marketData = await _getCurveUsdGlobalData(client, network, market);
217
- crvUsdMarketsData[market.value] = marketData;
218
- }),
219
- ...llamaLendMarkets.map(async (market) => {
220
- const marketData = await _getLlamaLendGlobalData(client, network, market);
221
- llamaLendMarketsData[market.value] = marketData;
222
- }),
223
- ...liquityV2Markets.map(async (market) => {
224
- const marketData = await _getLiquityV2MarketData(client, network, market);
225
- liquityV2MarketsData[market.value] = marketData;
226
- }),
227
-
228
- // === INDEPENDENT USER DATA (doesn't depend on market data) ===
229
- ...addresses.map(async (address) => {
195
+ // Everything starts at once; a read that needs a market's data waits for marketsData.
196
+ await Promise.all([
197
+ // === POSITIONS THAT DO NOT NEED MARKET DATA ===
198
+ // Maker: the address's vault list, then each vault. A failed list is reported in errors.makerCdps: maker stays {},
199
+ // which would otherwise read as "no vaults".
200
+ ...positionsAddresses.map(async (address) => {
230
201
  if (!isMainnet) return; // Maker CDPs are only available on mainnet
231
- const makerCdp = await _getUserCdps(client, network, address);
232
- makerCdps[address.toLowerCase() as EthAddress] = makerCdp;
202
+ let cdps: CdpInfo[];
203
+ try {
204
+ cdps = await _getUserCdps(client, network, address);
205
+ } catch (error) {
206
+ console.error(`Error fetching Maker CDPs for address ${address}:`, error);
207
+ makerCdpsErrors[address.toLowerCase() as EthAddress] = `Error fetching Maker CDPs for address ${address}`;
208
+ return;
209
+ }
210
+ await Promise.all(cdps.map(async (cdpInfo) => {
211
+ try {
212
+ const cdpData = await _getMakerCdpData(client, network, cdpInfo);
213
+ if (cdpData) {
214
+ positions[address.toLowerCase() as EthAddress].maker[cdpInfo.id] = { error: '', data: cdpData };
215
+ }
216
+ } catch (error) {
217
+ console.error(`Error fetching Maker CDP data for address ${address} with ID ${cdpInfo.id}:`, error);
218
+ positions[address.toLowerCase() as EthAddress].maker[cdpInfo.id] = { error: `Error fetching Maker CDP data for address ${address} with ID ${cdpInfo.id}`, data: null };
219
+ }
220
+ }));
233
221
  }),
234
- ...addresses.map(async (address) => {
222
+ ...positionsAddresses.map(async (address) => {
235
223
  try {
236
224
  if (!isFluidSupported) return; // Fluid is not available on Optimism
237
225
  const userPositions = (await _getUserPositionsPortfolio(client, network, address));
@@ -248,8 +236,18 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
248
236
  };
249
237
  }
250
238
  }),
239
+ ...positionsAddresses.map(async (address) => {
240
+ try {
241
+ if (!isMainnet) return; // Liquity trove info is only available on mainnet
242
+ const troveInfo = await _getLiquityTroveInfo(client, network, address);
243
+ if (new Dec(troveInfo.collateral).gt(0)) positions[address.toLowerCase() as EthAddress].liquity = { error: '', data: troveInfo };
244
+ } catch (error) {
245
+ console.error(`Error fetching Liquity trove info for address ${address}:`, error);
246
+ positions[address.toLowerCase() as EthAddress].liquity = { error: `Error fetching Liquity trove info for address ${address}`, data: null };
247
+ }
248
+ }),
251
249
 
252
- // === STAKING DATA (independent of market data) ===
250
+ // === STAKING DATA (independent of market data; Morpho Earn is with the positions below) ===
253
251
  ...stakingAddresses.map(async (address) => {
254
252
  try {
255
253
  if (!isFluidSupported) return;
@@ -286,20 +284,33 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
286
284
  stakingPositions[address.toLowerCase() as EthAddress].umbrella = { error: `Error fetching Umbrella staking data for address ${address}`, data: null };
287
285
  }
288
286
  }),
289
- // Liquity V2 staking
290
- ...liquityV2MarketsStaking.map(market => stakingAddresses.map(async (address) => {
287
+ // Liquity V2 stability pools, then sBOLD/yBOLD, which reads them.
288
+ ...stakingAddresses.map(async (address) => {
289
+ await Promise.all(liquityV2MarketsStaking.map(async (market) => {
290
+ try {
291
+ if (!isMainnet) {
292
+ stakingPositions[address.toLowerCase() as EthAddress].liquityV2[market.value] = { error: '', data: null };
293
+ return;
294
+ }
295
+ const liquityV2StakingData = await getLiquityV2Staking(client, network, market.value, address);
296
+ stakingPositions[address.toLowerCase() as EthAddress].liquityV2[market.value] = { error: '', data: liquityV2StakingData };
297
+ } catch (error) {
298
+ console.error(`Error fetching Liquity V2 staking data for address ${address}, market ${market.value}:`, error);
299
+ stakingPositions[address.toLowerCase() as EthAddress].liquityV2[market.value] = { error: `Error fetching Liquity V2 staking data for address ${address}`, data: null };
300
+ }
301
+ }));
291
302
  try {
292
303
  if (!isMainnet) {
293
- stakingPositions[address.toLowerCase() as EthAddress].liquityV2[market.value] = { error: '', data: null };
304
+ stakingPositions[address.toLowerCase() as EthAddress].liquityV2SBoldYBold = { error: '', data: null };
294
305
  return;
295
306
  }
296
- const liquityV2StakingData = await getLiquityV2Staking(client, network, market.value, address);
297
- stakingPositions[address.toLowerCase() as EthAddress].liquityV2[market.value] = { error: '', data: liquityV2StakingData };
307
+ const data = await getLiquitySAndYBold(client, network, stakingPositions[address.toLowerCase() as EthAddress].liquityV2, address);
308
+ stakingPositions[address.toLowerCase() as EthAddress].liquityV2SBoldYBold = { error: '', data };
298
309
  } catch (error) {
299
- console.error(`Error fetching Liquity V2 staking data for address ${address}, market ${market.value}:`, error);
300
- stakingPositions[address.toLowerCase() as EthAddress].liquityV2[market.value] = { error: `Error fetching Liquity V2 staking data for address ${address}`, data: null };
310
+ console.error(`Error fetching SBold/YBold data for address ${address}:`, error);
311
+ stakingPositions[address.toLowerCase() as EthAddress].liquityV2SBoldYBold = { error: `Error fetching sBold/yBold data for address ${address}`, data: null };
301
312
  }
302
- })).flat(),
313
+ }),
303
314
 
304
315
  // === REWARDS DATA (independent of market data) ===
305
316
  // Batch King rewards
@@ -356,7 +367,7 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
356
367
  }
357
368
  }
358
369
  })(),
359
- ...sparkMarkets.map((market) => rewardsAddresses.map(async address => {
370
+ ...sparkLending.markets(network).map((market) => rewardsAddresses.map(async address => {
360
371
  try {
361
372
  if (!isMainnet) {
362
373
  rewardsData[address.toLowerCase() as EthAddress].spark[market.value] = { error: '', data: [] };
@@ -378,7 +389,7 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
378
389
  rewardsData[address.toLowerCase() as EthAddress].merkl = { error: `Error fetching Merkl rewards data for address ${address}`, data: null };
379
390
  }
380
391
  }),
381
- ...aaveV3Markets.map(market => rewardsAddresses.map(async (address) => {
392
+ ...aaveV3Lending.markets(network).map(market => rewardsAddresses.map(async (address) => {
382
393
  try {
383
394
  const aaveData = await getUnclaimedRewardsForAllMarkets(client, network, address, market.providerAddress);
384
395
  rewardsData[address.toLowerCase() as EthAddress].aaveV3[market.value] = { error: '', data: aaveData };
@@ -440,21 +451,274 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
440
451
  }
441
452
  }
442
453
  })(),
454
+
455
+ // === POSITIONS ON MARKETS (each waits for its market's data) ===
456
+ ...LENDING_PROTOCOLS.flatMap((protocol) => protocol.readPositions({ client, network }, positionsAddresses, marketsData, positions)),
457
+ // Morpho Blue Earn: a supply without a borrow, kept as staking, read on the same markets as Morpho Blue's positions.
458
+ ...morphoBlueLending.markets(network).flatMap((market) => stakingAddresses.map(async (address) => {
459
+ try {
460
+ const marketData = requireMarket((await marketsData).morphoMarketsData, market.value);
461
+ const earnData = await getMorphoEarn(client, network, address, market, marketData);
462
+ if (earnData && new Dec(earnData.amount).gt(0)) {
463
+ stakingPositions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = { error: '', data: earnData };
464
+ }
465
+ } catch (error) {
466
+ console.error(`Error fetching MorphoBlue earn data for address ${address} on market ${market.value}:`, error);
467
+ stakingPositions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = { error: `Error fetching MorphoBlue earn data for address ${address} on market ${market.value}`, data: null };
468
+ }
469
+ })),
470
+ // Liquity V2 troves: one market's trove ids for every address at once, then each trove's data. A failure is the
471
+ // market's error; the troves that did load stay in its data.
472
+ ...(liquityV2Addresses.length ? liquityV2Markets : []).map(async (market) => {
473
+ const setEntry = (address: EthAddress, entry: PortfolioLiquityV2Troves) => {
474
+ positions[address.toLowerCase() as EthAddress].liquityV2![market.value] = entry;
475
+ };
476
+ const failAll = (error: string) => liquityV2Addresses.forEach((address) => setEntry(address, { error, data: {} }));
477
+
478
+ const allMarketsData = (await marketsData).liquityV2MarketsData;
479
+ const marketData = allMarketsData[market.value];
480
+ if (!marketData) {
481
+ failAll(`Error fetching Liquity V2 market data for market ${market.value}`);
482
+ return;
483
+ }
484
+ let troveIds: Record<EthAddress, string[]>;
485
+ try {
486
+ troveIds = await _getLiquityV2UsersTroveIds(client, network, market, marketData.marketData.troveNFTAddress, isSim, liquityV2Addresses);
487
+ } catch (error) {
488
+ console.error(`Error fetching Liquity V2 trove ids on market ${market.value}:`, error);
489
+ failAll(`Error fetching Liquity V2 troves on market ${market.value}`);
490
+ return;
491
+ }
492
+ await Promise.all(liquityV2Addresses.map(async (address) => {
493
+ const entry: PortfolioLiquityV2Troves = { error: '', data: {} };
494
+ const failed: string[] = [];
495
+ await Promise.all((troveIds[address] ?? []).map(async (troveId) => {
496
+ try {
497
+ entry.data[troveId] = await _getLiquityV2TroveData(client, network, {
498
+ selectedMarket: market,
499
+ assetsData: marketData.assetsData,
500
+ troveId,
501
+ allMarketsData: allMarketsData as Record<LiquityV2Versions, LiquityV2MarketData>,
502
+ });
503
+ } catch (error) {
504
+ console.error(`Error fetching Liquity V2 trove ${troveId} for address ${address} on market ${market.value}:`, error);
505
+ failed.push(troveId);
506
+ }
507
+ }));
508
+ if (failed.length) entry.error = `Error fetching Liquity V2 troves ${failed.join(', ')} for address ${address} on market ${market.value}`;
509
+ setEntry(address, entry);
510
+ }));
511
+ }),
512
+ ]);
513
+
514
+ return {
515
+ positions,
516
+ stakingPositions,
517
+ rewardsData,
518
+ errors: Object.keys(makerCdpsErrors).length ? { makerCdps: makerCdpsErrors } : {},
519
+ };
520
+ };
521
+
522
+ /**
523
+ * Every market the portfolio reads on a network — the markets half of getPortfolioData, for a caller that keeps
524
+ * markets apart from what it reads per address (a cache shared by many addresses). A market that fails is left out
525
+ * of `markets` and named in `errors`.
526
+ */
527
+ export async function getPortfolioMarketsData(provider: EthereumProvider, network: NetworkNumber, defaultProvider: EthereumProvider, isSim = false): Promise<PortfolioMarketsResult> {
528
+ const { client, defaultClient } = getPortfolioClients(provider, defaultProvider, network, isSim);
529
+ return _getPortfolioMarketsData(client, defaultClient, network);
530
+ }
531
+
532
+ /**
533
+ * What the portfolio reads per address — the other half of getPortfolioData — against `markets` from
534
+ * getPortfolioMarketsData. `markets` can be a promise: reads that do not need a market (Maker, Fluid, Liquity, staking,
535
+ * rewards) start at once, and each read on a market waits for it. A position on a market missing from `markets` is an
536
+ * entry with an error.
537
+ */
538
+ export async function getPortfolioUserData(
539
+ provider: EthereumProvider,
540
+ network: NetworkNumber,
541
+ addresses: EthAddress[],
542
+ markets: PortfolioMarketsData | Promise<PortfolioMarketsData>,
543
+ isSim = false,
544
+ options: PortfolioDataOptions = {},
545
+ ): Promise<PortfolioUserData> {
546
+ const { client } = getPortfolioClients(provider, provider, network, isSim);
547
+ return _getPortfolioUserData(client, network, addresses, Promise.resolve(markets), isSim, options);
548
+ }
549
+
550
+ export async function getPortfolioData(provider: EthereumProvider, network: NetworkNumber, defaultProvider: EthereumProvider, addresses: EthAddress[], isSim = false, options: PortfolioDataOptions = {}): Promise<PortfolioData> {
551
+ // One pair of clients for both halves, so their calls batch together.
552
+ const { client, defaultClient } = getPortfolioClients(provider, defaultProvider, network, isSim);
553
+ const marketsResult = _getPortfolioMarketsData(client, defaultClient, network);
554
+ const [{ markets, errors: marketsErrors }, userData] = await Promise.all([
555
+ marketsResult,
556
+ _getPortfolioUserData(client, network, addresses, marketsResult.then((result) => result.markets), isSim, options),
557
+ ]);
558
+
559
+ return {
560
+ positions: userData.positions,
561
+ stakingPositions: userData.stakingPositions,
562
+ rewardsData: userData.rewardsData,
563
+ markets,
564
+ errors: {
565
+ ...(Object.keys(marketsErrors).length ? { markets: marketsErrors } : {}),
566
+ ...userData.errors,
567
+ },
568
+ };
569
+ }
570
+
571
+
572
+ export async function getShifterPortfolioData(provider: EthereumProvider, network: NetworkNumber, defaultProvider: EthereumProvider, addresses: EthAddress[], isSim = false): Promise<{
573
+ positions: PortfolioPositionsData;
574
+ markets: ShifterPortfolioMarketsData;
575
+ }> {
576
+ const isMainnet = network === NetworkNumber.Eth;
577
+ const isFluidSupported = [NetworkNumber.Eth, NetworkNumber.Arb, NetworkNumber.Base, NetworkNumber.Plasma].includes(network);
578
+
579
+ const morphoMarkets = Object.values(MorphoBlueMarkets(network)).filter((market) => market.chainIds.includes(network));
580
+ const compoundV3Markets = Object.values(CompoundMarkets(network)).filter((market) => market.chainIds.includes(network) && market.value !== CompoundVersions.CompoundV2);
581
+ const sparkMarkets = Object.values(SparkMarkets(network)).filter((market) => market.chainIds.includes(network));
582
+ const aaveV3Markets = [AaveVersions.AaveV3, AaveVersions.AaveV3Lido, AaveVersions.AaveV3Etherfi].map((version) => AaveMarkets(network)[version]).filter((market) => market.chainIds.includes(network));
583
+ const aaveV2Markets = [AaveVersions.AaveV2].map((version) => AaveMarkets(network)[version]).filter((market) => market.chainIds.includes(network));
584
+ const compoundV2Markets = [CompoundVersions.CompoundV2].map((version) => CompoundMarkets(network)[version]).filter((market) => market.chainIds.includes(network));
585
+ const crvUsdMarkets = Object.values(CrvUsdMarkets(network)).filter((market) => market.chainIds.includes(network));
586
+ const llamaLendMarkets = [NetworkNumber.Eth, NetworkNumber.Arb].includes(network) ? Object.values(LlamaLendMarkets(network)).filter((market) => market.chainIds.includes(network)) : [];
587
+ const liquityV2Markets = [NetworkNumber.Eth].includes(network) ? Object.values(LiquityV2Markets(network)) : [];
588
+ const aaveV4Spokes = Object.values(AaveV4Spokes(network)).filter((market) => market.chainIds.includes(network));
589
+
590
+ const args: [NetworkNumber, any?] = [network, { batch: { multicall: { batchSize: isSim ? 2_000 : 2_500_000 } } }];
591
+ const client = getViemProvider(provider, ...args);
592
+ const defaultClient = getViemProvider(defaultProvider, ...args);
593
+
594
+ const markets: ShifterPortfolioMarketsData = {
595
+ morphoMarketsData: {},
596
+ morphoMidnightMarketsData: {}, // Morpho Midnight is not fetched for the shifter; key kept so the payload shape is stable
597
+ compoundV3MarketsData: {},
598
+ sparkMarketsData: {},
599
+ aaveV3MarketsData: {},
600
+ aaveV2MarketsData: {},
601
+ compoundV2MarketsData: {},
602
+ crvUsdMarketsData: {},
603
+ llamaLendMarketsData: {},
604
+ liquityV2MarketsData: {},
605
+ aaveV4SpokesData: {},
606
+ fluidMarketsData: {},
607
+ makerMarketsData: {},
608
+ };
609
+ const makerCdps: Record<string, CdpInfo[]> = {};
610
+
611
+ const positions: PortfolioPositionsData = {};
612
+
613
+ for (const address of addresses) {
614
+ positions[address.toLowerCase() as EthAddress] = {
615
+ aaveV3: {},
616
+ aaveV4: {},
617
+ morphoBlue: {},
618
+ morphoMidnight: {}, // not fetched for the shifter (see markets above)
619
+ compoundV3: {},
620
+ spark: {},
621
+ maker: {},
622
+ aaveV2: {},
623
+ compoundV2: {},
624
+ liquity: {},
625
+ crvUsd: {},
626
+ llamaLend: {},
627
+ fluid: {
628
+ error: '',
629
+ data: {},
630
+ },
631
+ };
632
+ }
633
+
634
+ await Promise.allSettled([
635
+ // === MARKET DATA (needs to be fetched first) ===
636
+ ...morphoMarkets.map(async (market) => {
637
+ markets.morphoMarketsData[market.value] = await _getMorphoBluePortfolioMarketData(client, network, market);
638
+ }),
639
+ ...compoundV3Markets.map(async (market) => {
640
+ markets.compoundV3MarketsData[market.value] = await _getCompoundV3MarketsData(client, network, market, defaultClient);
641
+ }),
642
+ ...sparkMarkets.map(async (market) => {
643
+ markets.sparkMarketsData[market.value] = await _getSparkMarketsData(client, network, market);
644
+ }),
645
+ ...aaveV3Markets.map(async (market) => {
646
+ markets.aaveV3MarketsData[market.value] = await _getAaveV3MarketData(client, network, market);
647
+ }),
648
+ ...aaveV4Spokes.map(async (spoke) => {
649
+ markets.aaveV4SpokesData[spoke.value] = await _getAaveV4SpokeData(client, network, spoke);
650
+ }),
651
+ ...aaveV2Markets.map(async (market) => {
652
+ markets.aaveV2MarketsData[market.value] = await _getAaveV2MarketsData(client, network, market);
653
+ }),
654
+ ...compoundV2Markets.map(async (market) => {
655
+ markets.compoundV2MarketsData[market.value] = await _getCompoundV2MarketsData(client, network);
656
+ }),
657
+ ...crvUsdMarkets.map(async (market) => {
658
+ markets.crvUsdMarketsData[market.value] = await _getCurveUsdGlobalData(client, network, market);
659
+ }),
660
+ ...llamaLendMarkets.map(async (market) => {
661
+ markets.llamaLendMarketsData[market.value] = await _getLlamaLendGlobalData(client, network, market);
662
+ }),
663
+ ...liquityV2Markets.map(async (market) => {
664
+ markets.liquityV2MarketsData[market.value] = await _getLiquityV2MarketData(client, network, market);
665
+ }),
666
+ (async () => {
667
+ if (!isFluidSupported) return;
668
+ try {
669
+ markets.fluidMarketsData = await _getAllFluidMarketDataPortfolio(client, network);
670
+ } catch (error) {
671
+ console.error('Error fetching Fluid markets data:', error);
672
+ }
673
+ })(),
674
+ (async () => {
675
+ if (!isMainnet) return; // Maker CDPs are only available on mainnet
676
+ try {
677
+ markets.makerMarketsData = await _getMakerIlksData(client, network, MakerActiveIlks);
678
+ } catch (error) {
679
+ console.error('Error fetching Maker ilks data:', error);
680
+ }
681
+ })(),
682
+
683
+ // === INDEPENDENT USER DATA (doesn't depend on market data) ===
684
+ ...addresses.map(async (address) => {
685
+ if (!isMainnet) return; // Maker CDPs are only available on mainnet
686
+ const makerCdp = await _getUserCdps(client, network, address);
687
+ makerCdps[address.toLowerCase() as EthAddress] = makerCdp;
688
+ }),
689
+ ...addresses.map(async (address) => {
690
+ try {
691
+ if (!isFluidSupported) return; // Fluid is not available on Optimism
692
+ const userPositions = (await _getUserPositionsPortfolio(client, network, address));
693
+ for (const position of userPositions) {
694
+ if (position.userData && new Dec(position.userData.suppliedUsd).gt(0)) {
695
+ positions[address.toLowerCase() as EthAddress].fluid.data[position.userData.nftId] = position.userData;
696
+ }
697
+ }
698
+ } catch (error) {
699
+ console.error(`Error fetching Fluid positions for address ${address}:`, error);
700
+ positions[address.toLowerCase() as EthAddress].fluid = {
701
+ error: `Error fetching Fluid positions for address ${address}`,
702
+ data: {},
703
+ };
704
+ }
705
+ }),
443
706
  ]);
444
707
 
445
708
  await Promise.all([
446
- ...aaveV3Markets.map((market) => allAddresses.map(async (address) => {
709
+ ...aaveV3Markets.map((market) => addresses.map(async (address) => {
447
710
  try {
448
- const accData = await _getAaveV3AccountData(client, network, address, { selectedMarket: market, ...aaveV3MarketsData[market.value] });
449
- if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].aaveV3[market.value] = { error: '', data: accData };
711
+ const accData = await _getAaveV3AccountData(client, network, address, { selectedMarket: market, ...markets.aaveV3MarketsData[market.value] });
712
+ // Always store the position (even with no supply) so the eMode category is available to the shifter
713
+ positions[address.toLowerCase() as EthAddress].aaveV3[market.value] = { error: '', data: accData };
450
714
  } catch (error) {
451
715
  console.error(`Error fetching AaveV3 account data for address ${address} on market ${market.value}:`, error);
452
716
  positions[address.toLowerCase() as EthAddress].aaveV3[market.value] = { error: `Error fetching AaveV3 account data for address ${address} on market ${market.value}`, data: null };
453
717
  }
454
718
  })).flat(),
455
- ...aaveV4Spokes.map((spoke) => allAddresses.map(async (address) => {
719
+ ...aaveV4Spokes.map((spoke) => addresses.map(async (address) => {
456
720
  try {
457
- const accData = await _getAaveV4AccountData(client, network, aaveV4SpokesData[spoke.value], address);
721
+ const accData = await _getAaveV4AccountData(client, network, markets.aaveV4SpokesData[spoke.value], address);
458
722
  if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].aaveV4[spoke.value] = { error: '', data: accData };
459
723
  } catch (error) {
460
724
  console.error(`Error fetching AaveV4 account data for address ${address} on spoke ${spoke.value}:`, error);
@@ -463,71 +727,36 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
463
727
  })).flat(),
464
728
  ...morphoMarkets.map((market) => addresses.map(async (address) => {
465
729
  try {
466
- const [accDataPromise, earnDataPromise] = await Promise.allSettled([
467
- _getMorphoBlueAccountData(client, network, address, market, morphoMarketsData[market.value]),
468
- includeStaking ? getMorphoEarn(client, network, address, market, morphoMarketsData[market.value]) : null,
469
- ]);
470
- if (accDataPromise.status === 'rejected') {
471
- console.error(`Error fetching MorphoBlue account data for address ${address} on market ${market.value}:`, accDataPromise.reason);
472
- positions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = { error: `Error fetching MorphoBlue account data for address ${address} on market ${market.value}`, data: null };
473
- }
474
- if (earnDataPromise.status === 'rejected') {
475
- // An Earn failure belongs to the staking entry: the lending position is not the one that failed.
476
- console.error(`Error fetching MorphoBlue earn data for address ${address} on market ${market.value}:`, earnDataPromise.reason);
477
- stakingPositions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = { error: `Error fetching MorphoBlue earn data for address ${address} on market ${market.value}`, data: null };
478
- }
479
- if (accDataPromise.status !== 'rejected') {
480
- const accData = accDataPromise.value;
481
- if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = { error: '', data: accData };
482
- }
483
- if (earnDataPromise.status !== 'rejected') {
484
- const earnData = earnDataPromise.value;
485
- if (earnData && new Dec(earnData.amount).gt(0)) {
486
- stakingPositions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = {
487
- error: '',
488
- data: earnData,
489
- };
490
- }
491
- }
730
+ const accData = await _getMorphoBlueAccountData(client, network, address, market, markets.morphoMarketsData[market.value]);
731
+ if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = { error: '', data: accData };
492
732
  } catch (error) {
493
733
  console.error(`Error fetching MorphoBlue account data for address ${address} on market ${market.value}:`, error);
494
734
  positions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = { error: `Error fetching MorphoBlue account data for address ${address} on market ${market.value}`, data: null };
495
735
  }
496
736
  })).flat(),
497
- ...morphoMidnightMarkets.map((market) => addresses.map(async (address) => {
498
- try {
499
- // Markets are created lazily on the first position, so an uncreated one can hold no position.
500
- if (!morphoMidnightMarketsData[market.value]?.isCreated) return;
501
- const accData = await _getMorphoMidnightAccountData(client, network, address, market, morphoMidnightMarketsData[market.value]);
502
- if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].morphoMidnight[market.value] = { error: '', data: accData };
503
- } catch (error) {
504
- console.error(`Error fetching MorphoMidnight account data for address ${address} on market ${market.value}:`, error);
505
- positions[address.toLowerCase() as EthAddress].morphoMidnight[market.value] = { error: `Error fetching MorphoMidnight account data for address ${address} on market ${market.value}`, data: null };
506
- }
507
- })).flat(),
508
737
  ...compoundV3Markets.map((market) => addresses.map(async (address) => {
509
738
  try {
510
- const accData = await _getCompoundV3AccountData(client, network, address, ZERO_ADDRESS, { selectedMarket: market, assetsData: compoundV3MarketsData[market.value].assetsData });
739
+ const accData = await _getCompoundV3AccountData(client, network, address, ZERO_ADDRESS, { selectedMarket: market, assetsData: markets.compoundV3MarketsData[market.value].assetsData });
511
740
  if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].compoundV3[market.value] = { error: '', data: accData };
512
741
  } catch (error) {
513
742
  console.error(`Error fetching CompoundV3 account data for address ${address} on market ${market.value}:`, error);
514
743
  positions[address.toLowerCase() as EthAddress].compoundV3[market.value] = { error: `Error fetching CompoundV3 account data for address ${address} on market ${market.value}`, data: null };
515
744
  }
516
745
  })).flat(),
517
- ...sparkMarkets.map((market) => allAddresses.map(async (address) => {
746
+ ...sparkMarkets.map((market) => addresses.map(async (address) => {
518
747
  try {
519
- const accData = await _getSparkAccountData(client, network, address, { selectedMarket: market, assetsData: sparkMarketsData[market.value].assetsData, eModeCategoriesData: sparkMarketsData[market.value].eModeCategoriesData });
520
- if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].spark[market.value] = { error: '', data: accData };
748
+ const accData = await _getSparkAccountData(client, network, address, { selectedMarket: market, assetsData: markets.sparkMarketsData[market.value].assetsData, eModeCategoriesData: markets.sparkMarketsData[market.value].eModeCategoriesData });
749
+ // Always store the position (even with no supply) so the eMode category is available to the shifter
750
+ positions[address.toLowerCase() as EthAddress].spark[market.value] = { error: '', data: accData };
521
751
  } catch (error) {
522
752
  console.error(`Error fetching Spark account data for address ${address} on market ${market.value}:`, error);
523
753
  positions[address.toLowerCase() as EthAddress].spark[market.value] = { error: `Error fetching Spark account data for address ${address} on market ${market.value}`, data: null };
524
754
  }
525
755
  })).flat(),
526
- // flatMap, not map: each CDP's fetch has to be one of the awaited promises, or getPortfolioData can return
527
- // before a vault's data lands and the vault is missing from the result.
528
- ...addresses.flatMap((address) => (makerCdps[address.toLowerCase() as EthAddress] ?? []).map(async (cdpInfo) => {
756
+ ...addresses.map((address) => (makerCdps[address.toLowerCase() as EthAddress] || []).map(async (cdpInfo) => {
529
757
  try {
530
- const cdpData = await _getMakerCdpData(client, network, cdpInfo);
758
+ // reuse ilk data fetched for the markets payload; ilks outside the active set are fetched on demand
759
+ const cdpData = await _getMakerCdpData(client, network, cdpInfo, markets.makerMarketsData[cdpInfo.ilkLabel]);
531
760
  if (cdpData) {
532
761
  positions[address.toLowerCase() as EthAddress].maker[cdpInfo.id] = { error: '', data: cdpData };
533
762
  }
@@ -535,10 +764,10 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
535
764
  console.error(`Error fetching Maker CDP data for address ${address} with ID ${cdpInfo.id}:`, error);
536
765
  positions[address.toLowerCase() as EthAddress].maker[cdpInfo.id] = { error: `Error fetching Maker CDP data for address ${address} with ID ${cdpInfo.id}`, data: null };
537
766
  }
538
- })),
767
+ })).flat(),
539
768
  ...aaveV2Markets.map((market) => addresses.map(async (address) => {
540
769
  try {
541
- const accData = await _getAaveV2AccountData(client, network, address, aaveV2MarketsData[market.value].assetsData, market);
770
+ const accData = await _getAaveV2AccountData(client, network, address, markets.aaveV2MarketsData[market.value].assetsData, market);
542
771
  if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].aaveV2[market.value] = { error: '', data: accData };
543
772
  } catch (error) {
544
773
  console.error(`Error fetching AaveV2 account data for address ${address}:`, error);
@@ -547,7 +776,7 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
547
776
  })).flat(),
548
777
  ...compoundV2Markets.map((market) => addresses.map(async (address) => {
549
778
  try {
550
- const accData = await _getCompoundV2AccountData(client, network, address, compoundV2MarketsData[market.value].assetsData);
779
+ const accData = await _getCompoundV2AccountData(client, network, address, markets.compoundV2MarketsData[market.value].assetsData);
551
780
  if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].compoundV2[market.value] = { error: '', data: accData };
552
781
  } catch (error) {
553
782
  console.error(`Error fetching CompoundV2 account data for address ${address}:`, error);
@@ -566,9 +795,9 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
566
795
  }),
567
796
  ...crvUsdMarkets.map((market) => addresses.map(async (address) => {
568
797
  try {
569
- const accData = await _getCurveUsdUserData(client, network, address, market, crvUsdMarketsData[market.value].activeBand);
798
+ const accData = await _getCurveUsdUserData(client, network, address, market, markets.crvUsdMarketsData[market.value].activeBand, markets.crvUsdMarketsData[market.value].borrowRate);
570
799
  if (new Dec(accData.suppliedUsd).gt(0) || new Dec(accData.borrowedUsd).gt(0)) {
571
- positions[address.toLowerCase() as EthAddress].crvUsd[market.value] = { error: '', data: { ...accData, borrowRate: crvUsdMarketsData[market.value].borrowRate } };
800
+ positions[address.toLowerCase() as EthAddress].crvUsd[market.value] = { error: '', data: accData };
572
801
  }
573
802
  } catch (error) {
574
803
  console.error(`Error fetching Curve USD account data for address ${address} on market ${market.value}:`, error);
@@ -577,35 +806,19 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
577
806
  })).flat(),
578
807
  ...llamaLendMarkets.map((market) => addresses.map(async (address) => {
579
808
  try {
580
- const accData = await _getLlamaLendUserData(client, network, address, market, llamaLendMarketsData[market.value]);
809
+ const accData = await _getLlamaLendUserData(client, network, address, market, markets.llamaLendMarketsData[market.value]);
581
810
  if (new Dec(accData.suppliedUsd).gt(0) || new Dec(accData.borrowedUsd).gt(0)) {
582
- positions[address.toLowerCase() as EthAddress].llamaLend[market.value] = { error: '', data: { ...accData, borrowRate: llamaLendMarketsData[market.value].borrowRate } };
811
+ positions[address.toLowerCase() as EthAddress].llamaLend[market.value] = { error: '', data: { ...accData, borrowRate: markets.llamaLendMarketsData[market.value].borrowRate } };
583
812
  }
584
813
  } catch (error) {
585
814
  console.error(`Error fetching LlamaLend account data for address ${address} on market ${market.value}:`, error);
586
815
  positions[address.toLowerCase() as EthAddress].llamaLend[market.value] = { error: `Error fetching LlamaLend account data for address ${address} on market ${market.value}`, data: null };
587
816
  }
588
817
  })).flat(),
589
- // liquity sBold/yBold and staking options
590
- ...stakingAddresses.map(async (address) => {
591
- try {
592
- if (!isMainnet) {
593
- stakingPositions[address.toLowerCase() as EthAddress].liquityV2SBoldYBold = { error: '', data: null };
594
- return;
595
- }
596
- const data = await getLiquitySAndYBold(client, network, stakingPositions[address.toLowerCase() as EthAddress].liquityV2, address);
597
- stakingPositions[address.toLowerCase() as EthAddress].liquityV2SBoldYBold = { error: '', data };
598
- } catch (error) {
599
- console.error(`Error fetching SBold/YBold data for address ${address}:`, error);
600
- stakingPositions[address.toLowerCase() as EthAddress].liquityV2SBoldYBold = { error: `Error fetching sBold/yBold data for address ${address}`, data: null };
601
- }
602
- }),
603
818
  ]);
604
819
 
605
820
  return {
606
821
  positions,
607
- stakingPositions,
608
- rewardsData,
609
822
  markets,
610
823
  };
611
824
  }