@defisaver/positions-sdk 2.1.168-stable-pair-dev → 2.1.169-portfolio-api-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 (105) hide show
  1. package/cjs/aaveV3/index.d.ts +2 -12
  2. package/cjs/aaveV3/sgho.d.ts +2 -16
  3. package/cjs/aaveV4/lend.d.ts +2 -43
  4. package/cjs/claiming/aaveV3.d.ts +2 -2
  5. package/cjs/claiming/ethena.d.ts +2 -1
  6. package/cjs/claiming/king.d.ts +2 -1
  7. package/cjs/claiming/spark.d.ts +2 -2
  8. package/cjs/fluid/index.d.ts +5 -49
  9. package/cjs/helpers/aaveV4Helpers/index.js +1 -11
  10. package/cjs/liquity/index.d.ts +2 -12
  11. package/cjs/liquityV2/index.d.ts +8 -23
  12. package/cjs/liquityV2/index.js +37 -19
  13. package/cjs/moneymarket/moneymarketCommonService.js +2 -12
  14. package/cjs/portfolio/index.d.ts +16 -8
  15. package/cjs/portfolio/index.js +458 -520
  16. package/cjs/portfolio/lendingProtocols.d.ts +184 -0
  17. package/cjs/portfolio/lendingProtocols.js +196 -0
  18. package/cjs/savings/index.d.ts +8 -2
  19. package/cjs/savings/index.js +33 -4
  20. package/cjs/types/aave.d.ts +98 -2
  21. package/cjs/types/aaveV4.d.ts +44 -0
  22. package/cjs/types/common.d.ts +0 -1
  23. package/cjs/types/common.js +0 -1
  24. package/cjs/types/compound.d.ts +26 -2
  25. package/cjs/types/curveUsd.d.ts +9 -1
  26. package/cjs/types/fluid.d.ts +13 -0
  27. package/cjs/types/index.d.ts +1 -0
  28. package/cjs/types/index.js +1 -0
  29. package/cjs/types/liquity.d.ts +12 -0
  30. package/cjs/types/liquityV2.d.ts +24 -0
  31. package/cjs/types/llamaLend.d.ts +12 -1
  32. package/cjs/types/morphoBlue.d.ts +2 -0
  33. package/cjs/types/portfolio.d.ts +125 -5
  34. package/cjs/types/savings/index.d.ts +9 -1
  35. package/cjs/types/spark.d.ts +21 -1
  36. package/cjs/umbrella/index.d.ts +2 -3
  37. package/esm/aaveV3/index.d.ts +2 -12
  38. package/esm/aaveV3/sgho.d.ts +2 -16
  39. package/esm/aaveV4/lend.d.ts +2 -43
  40. package/esm/claiming/aaveV3.d.ts +2 -2
  41. package/esm/claiming/aaveV3.js +1 -1
  42. package/esm/claiming/ethena.d.ts +2 -1
  43. package/esm/claiming/king.d.ts +2 -1
  44. package/esm/claiming/spark.d.ts +2 -2
  45. package/esm/claiming/spark.js +1 -1
  46. package/esm/fluid/index.d.ts +5 -49
  47. package/esm/helpers/aaveV4Helpers/index.js +1 -11
  48. package/esm/liquity/index.d.ts +2 -12
  49. package/esm/liquityV2/index.d.ts +8 -23
  50. package/esm/liquityV2/index.js +35 -18
  51. package/esm/moneymarket/moneymarketCommonService.js +2 -12
  52. package/esm/portfolio/index.d.ts +16 -8
  53. package/esm/portfolio/index.js +458 -522
  54. package/esm/portfolio/lendingProtocols.d.ts +184 -0
  55. package/esm/portfolio/lendingProtocols.js +189 -0
  56. package/esm/savings/index.d.ts +8 -2
  57. package/esm/savings/index.js +31 -3
  58. package/esm/types/aave.d.ts +98 -2
  59. package/esm/types/aaveV4.d.ts +44 -0
  60. package/esm/types/common.d.ts +0 -1
  61. package/esm/types/common.js +0 -1
  62. package/esm/types/compound.d.ts +26 -2
  63. package/esm/types/curveUsd.d.ts +9 -1
  64. package/esm/types/fluid.d.ts +13 -0
  65. package/esm/types/index.d.ts +1 -0
  66. package/esm/types/index.js +1 -0
  67. package/esm/types/liquity.d.ts +12 -0
  68. package/esm/types/liquityV2.d.ts +24 -0
  69. package/esm/types/llamaLend.d.ts +12 -1
  70. package/esm/types/morphoBlue.d.ts +2 -0
  71. package/esm/types/portfolio.d.ts +125 -5
  72. package/esm/types/savings/index.d.ts +9 -1
  73. package/esm/types/spark.d.ts +21 -1
  74. package/esm/umbrella/index.d.ts +2 -3
  75. package/package.json +1 -1
  76. package/src/aaveV3/index.ts +2 -1
  77. package/src/aaveV3/sgho.ts +2 -17
  78. package/src/aaveV4/lend.ts +2 -48
  79. package/src/claiming/aaveV3.ts +4 -2
  80. package/src/claiming/ethena.ts +5 -5
  81. package/src/claiming/king.ts +3 -3
  82. package/src/claiming/spark.ts +6 -4
  83. package/src/fluid/index.ts +4 -3
  84. package/src/helpers/aaveV4Helpers/index.ts +1 -12
  85. package/src/liquity/index.ts +2 -2
  86. package/src/liquityV2/index.ts +48 -22
  87. package/src/moneymarket/moneymarketCommonService.ts +2 -13
  88. package/src/portfolio/index.ts +306 -331
  89. package/src/portfolio/lendingProtocols.ts +281 -0
  90. package/src/savings/index.ts +41 -4
  91. package/src/types/aave.ts +104 -1
  92. package/src/types/aaveV4.ts +49 -0
  93. package/src/types/common.ts +0 -1
  94. package/src/types/compound.ts +38 -2
  95. package/src/types/curveUsd.ts +17 -1
  96. package/src/types/fluid.ts +14 -0
  97. package/src/types/index.ts +1 -0
  98. package/src/types/liquity.ts +13 -1
  99. package/src/types/liquityV2.ts +26 -1
  100. package/src/types/llamaLend.ts +19 -1
  101. package/src/types/morphoBlue.ts +3 -0
  102. package/src/types/portfolio.ts +151 -5
  103. package/src/types/savings/index.ts +12 -1
  104. package/src/types/spark.ts +25 -1
  105. package/src/umbrella/index.ts +2 -1
@@ -9,137 +9,158 @@ import {
9
9
  LlamaLendMarkets,
10
10
  MakerActiveIlks,
11
11
  MorphoBlueMarkets,
12
- MorphoMidnightMarkets,
13
12
  SparkMarkets,
14
13
  } from '../markets';
15
14
  import { _getMorphoBlueAccountData, _getMorphoBluePortfolioMarketData, getMorphoEarn } from '../morphoBlue';
16
- import { _getMorphoMidnightAccountData, _getMorphoMidnightMarketData } from '../morphoMidnight';
17
15
  import {
18
- AaveV2MarketData,
19
- AaveV3MarketData,
20
- AaveV4SpokeData,
21
16
  AaveVersions,
22
17
  CdpInfo,
23
- CompoundV2MarketsData,
24
- CompoundV3MarketsData,
25
18
  CompoundVersions,
26
- CrvUSDGlobalMarketData,
27
19
  LiquityV2MarketData,
28
- LlamaLendGlobalMarketData,
29
- MorphoBlueMarketInfo,
30
- MorphoMidnightMarketInfo,
20
+ LiquityV2Versions,
21
+ PortfolioData,
22
+ PortfolioDataOptions,
23
+ PortfolioLiquityV2Troves,
31
24
  PortfolioMarketsData,
25
+ PortfolioMarketsErrors,
26
+ PortfolioMarketsResult,
32
27
  PortfolioPositionsData,
33
- SparkMarketsData,
28
+ PortfolioPositionsDataForAddress,
29
+ PortfolioRewardsData,
30
+ PortfolioStakingPositionsData,
31
+ PortfolioUserData,
32
+ ShifterPortfolioMarketsData,
34
33
  } from '../types';
35
34
  import { _getCompoundV3AccountData, _getCompoundV3MarketsData } from '../compoundV3';
36
35
  import { _getSparkAccountData, _getSparkMarketsData } from '../spark';
37
36
  import { _getCurveUsdGlobalData, _getCurveUsdUserData } from '../curveUsd';
38
37
  import { _getLlamaLendGlobalData, _getLlamaLendUserData } from '../llamaLend';
39
38
  import { _getAaveV3AccountData, _getAaveV3MarketData, getStakeAaveData } from '../aaveV3';
39
+ import { _getAaveV4AccountData, _getAaveV4SpokeData } from '../aaveV4';
40
40
  import { ZERO_ADDRESS } from '../constants';
41
41
  import { _getMakerCdpData, _getMakerIlksData, _getUserCdps } from '../maker';
42
42
  import { _getAaveV2AccountData, _getAaveV2MarketsData } from '../aaveV2';
43
43
  import { _getCompoundV2AccountData, _getCompoundV2MarketsData } from '../compoundV2';
44
44
  import { getViemProvider } from '../services/viem';
45
45
  import { _getLiquityTroveInfo, getLiquityStakingData } from '../liquity';
46
- import { _getLiquityV2MarketData, getLiquitySAndYBold, getLiquityV2Staking } from '../liquityV2';
46
+ import {
47
+ _getLiquityV2MarketData, _getLiquityV2TroveData, _getLiquityV2UsersTroveIds, getLiquitySAndYBold, getLiquityV2Staking,
48
+ } from '../liquityV2';
47
49
  import { _getAllFluidMarketDataPortfolio, _getAllUserEarnPositionsWithFTokens, _getUserPositionsPortfolio } from '../fluid';
48
50
  import { getUmbrellaData } from '../umbrella';
49
51
  import { getMerklUnclaimedRewards, getUnclaimedRewardsForAllMarkets } from '../claiming/aaveV3';
50
52
  import { fetchSparkAirdropRewards, fetchSparkRewards } from '../claiming/spark';
51
53
  import { getKingRewards } from '../claiming/king';
52
54
  import { fetchEthenaAirdropRewards } from '../claiming/ethena';
53
- import { _getAaveV4AccountData, _getAaveV4SpokeData } from '../aaveV4';
54
55
  import { getUniswapRewards } from '../claiming/uniswap';
56
+ import {
57
+ aaveV3Lending, LENDING_PROTOCOLS, LendingKey, morphoBlueLending, requireMarket, sparkLending,
58
+ } from './lendingProtocols';
55
59
 
56
- export async function getPortfolioData(provider: EthereumProvider, network: NetworkNumber, defaultProvider: EthereumProvider, addresses: EthAddress[], isSim = false): Promise<{
57
- positions: PortfolioPositionsData;
58
- stakingPositions: any;
59
- rewardsData: any;
60
- markets: any;
61
- }> {
62
- const isMainnet = network === NetworkNumber.Eth;
63
- const isFluidSupported = [NetworkNumber.Eth, NetworkNumber.Arb, NetworkNumber.Base, NetworkNumber.Plasma].includes(network);
64
-
65
- const morphoMarkets = Object.values(MorphoBlueMarkets(network)).filter((market) => market.chainIds.includes(network));
66
- const morphoMidnightMarkets = Object.values(MorphoMidnightMarkets(network)).filter((market) => market.chainIds.includes(network));
67
- const compoundV3Markets = Object.values(CompoundMarkets(network)).filter((market) => market.chainIds.includes(network) && market.value !== CompoundVersions.CompoundV2);
68
- const sparkMarkets = Object.values(SparkMarkets(network)).filter((market) => market.chainIds.includes(network));
69
- const aaveV3Markets = [AaveVersions.AaveV3, AaveVersions.AaveV3Lido, AaveVersions.AaveV3Etherfi].map((version) => AaveMarkets(network)[version]).filter((market) => market.chainIds.includes(network));
70
- const aaveV2Markets = [AaveVersions.AaveV2].map((version) => AaveMarkets(network)[version]).filter((market) => market.chainIds.includes(network));
71
- const compoundV2Markets = [CompoundVersions.CompoundV2].map((version) => CompoundMarkets(network)[version]).filter((market) => market.chainIds.includes(network));
72
- const crvUsdMarkets = Object.values(CrvUsdMarkets(network)).filter((market) => market.chainIds.includes(network));
73
- const llamaLendMarkets = [NetworkNumber.Eth, NetworkNumber.Arb].includes(network) ? Object.values(LlamaLendMarkets(network)).filter((market) => market.chainIds.includes(network)) : [];
74
- const liquityV2Markets = [NetworkNumber.Eth].includes(network) ? Object.values(LiquityV2Markets(network)) : [];
75
- const liquityV2MarketsStaking = [NetworkNumber.Eth].includes(network) ? Object.values(LiquityV2Markets(network)).filter(market => !market.isLegacy) : [];
76
- const aaveV4Spokes = Object.values(AaveV4Spokes(network)).filter((market) => market.chainIds.includes(network));
60
+ type PortfolioClient = ReturnType<typeof getViemProvider>;
77
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
+ });
78
67
 
68
+ const getPortfolioClients = (provider: EthereumProvider, defaultProvider: EthereumProvider, network: NetworkNumber, isSim: boolean) => {
79
69
  // batchSize is viem's cap on a batch's raw subcall calldata (bytes); the JSON-RPC body ends up
80
70
  // ~4x larger (hex + aggregate3 ABI + JSON overhead) and RPC providers reject bodies over
81
71
  // ~2.5MB with HTTP 413, so keep this small enough that no single body gets near that.
82
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
83
73
  // (e.g. adding new markets may result in body size going over alchemy body size limit)
84
74
  const args: [NetworkNumber, any?] = [network, { batch: { multicall: { batchSize: isSim ? 2_000 : 250_000 } } }];
85
- const client = getViemProvider(provider, ...args);
86
- const defaultClient = getViemProvider(defaultProvider, ...args);
75
+ return {
76
+ client: getViemProvider(provider, ...args),
77
+ defaultClient: getViemProvider(defaultProvider, ...args),
78
+ };
79
+ };
87
80
 
88
- const morphoMarketsData: Record<string, MorphoBlueMarketInfo> = {};
89
- const morphoMidnightMarketsData: Record<string, MorphoMidnightMarketInfo> = {};
90
- const compoundV3MarketsData: Record<string, CompoundV3MarketsData> = {};
91
- const sparkMarketsData: Record<string, SparkMarketsData> = {};
92
- const aaveV3MarketsData: Record<string, AaveV3MarketData> = {};
93
- const makerCdps: Record<string, CdpInfo[]> = {};
94
- const aaveV2MarketsData: Record<string, AaveV2MarketData> = {};
95
- const compoundV2MarketsData: Record<string, CompoundV2MarketsData> = {};
96
- const crvUsdMarketsData: Record<string, CrvUSDGlobalMarketData> = {};
97
- const llamaLendMarketsData: Record<string, LlamaLendGlobalMarketData> = {};
98
- const liquityV2MarketsData: Record<string, LiquityV2MarketData> = {};
99
- const aaveV4SpokesData: Record<string, AaveV4SpokeData> = {};
100
-
101
- const markets = {
102
- morphoMarketsData,
103
- morphoMidnightMarketsData,
104
- compoundV3MarketsData,
105
- sparkMarketsData,
106
- aaveV3MarketsData,
107
- aaveV2MarketsData,
108
- compoundV2MarketsData,
109
- crvUsdMarketsData,
110
- llamaLendMarketsData,
111
- liquityV2MarketsData,
112
- aaveV4SpokesData,
81
+ const _getPortfolioMarketsData = async (client: PortfolioClient, defaultClient: PortfolioClient, network: NetworkNumber): Promise<PortfolioMarketsResult> => {
82
+ const { liquityV2Markets } = getPortfolioMarkets(network);
83
+
84
+ const markets: PortfolioMarketsData = {
85
+ morphoMarketsData: {},
86
+ morphoMidnightMarketsData: {},
87
+ compoundV3MarketsData: {},
88
+ sparkMarketsData: {},
89
+ aaveV3MarketsData: {},
90
+ aaveV2MarketsData: {},
91
+ compoundV2MarketsData: {},
92
+ crvUsdMarketsData: {},
93
+ llamaLendMarketsData: {},
94
+ liquityV2MarketsData: {},
95
+ aaveV4SpokesData: {},
113
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);
114
142
 
115
143
  const positions: PortfolioPositionsData = {};
116
- const stakingPositions: any = {};
117
- const rewardsData: any = {};
118
- const allAddresses = [...addresses];
144
+ const stakingPositions: PortfolioStakingPositionsData = {};
145
+ const rewardsData: PortfolioRewardsData = {};
146
+ const makerCdpsErrors: Record<EthAddress, string> = {};
119
147
 
120
- for (const address of allAddresses) {
148
+ for (const address of positionsAddresses) {
121
149
  positions[address.toLowerCase() as EthAddress] = {
122
- aaveV3: {},
123
- aaveV4: {},
124
- morphoBlue: {},
125
- morphoMidnight: {},
126
- compoundV3: {},
127
- 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>,
128
152
  maker: {},
129
- aaveV2: {},
130
- compoundV2: {},
131
153
  liquity: {},
132
- crvUsd: {},
133
- llamaLend: {},
134
154
  fluid: {
135
155
  error: '',
136
156
  data: {},
137
157
  },
158
+ ...(includeLiquityV2 ? { liquityV2: {} } : {}),
138
159
  };
139
160
  }
140
161
 
141
162
  // TODO: check default values, probably needed when fetching portfolio on unsupported networks
142
- for (const address of addresses) {
163
+ for (const address of stakingAddresses) {
143
164
  stakingPositions[address.toLowerCase() as EthAddress] = {
144
165
  aaveV3: {},
145
166
  morphoBlue: {},
@@ -153,72 +174,52 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
153
174
  error: '',
154
175
  data: {},
155
176
  },
177
+ // Set by the sBOLD/yBOLD fetch below, whether it succeeds or fails.
178
+ liquityV2SBoldYBold: { error: '', data: null },
156
179
  };
180
+ }
157
181
 
182
+ for (const address of rewardsAddresses) {
183
+ // merkl, spk, king and uniswap are set by their fetches below, whether they succeed or fail.
158
184
  rewardsData[address.toLowerCase() as EthAddress] = {
159
- merkl: {},
185
+ merkl: { error: '', data: null },
160
186
  aaveV3: {},
161
187
  spark: {},
162
- spk: {},
163
- king: {},
188
+ spk: { error: '', data: null },
189
+ king: { error: '', data: null },
164
190
  ethena: {},
191
+ uniswap: { error: '', data: null },
165
192
  };
166
193
  }
167
194
 
168
- await Promise.allSettled([
169
- // === MARKET DATA (needs to be fetched first) ===
170
- ...morphoMarkets.map(async (market) => {
171
- const marketData = await _getMorphoBluePortfolioMarketData(client, network, market);
172
- morphoMarketsData[market.value] = marketData;
173
- }),
174
- ...morphoMidnightMarkets.map(async (market) => {
175
- const marketData = await _getMorphoMidnightMarketData(client, network, market);
176
- morphoMidnightMarketsData[market.value] = marketData;
177
- }),
178
- ...compoundV3Markets.map(async (market) => {
179
- const marketData = await _getCompoundV3MarketsData(client, network, market, defaultClient);
180
- compoundV3MarketsData[market.value] = marketData;
181
- }),
182
- ...sparkMarkets.map(async (market) => {
183
- const marketData = await _getSparkMarketsData(client, network, market);
184
- sparkMarketsData[market.value] = marketData;
185
- }),
186
- ...aaveV3Markets.map(async (market) => {
187
- const marketData = await _getAaveV3MarketData(client, network, market);
188
- aaveV3MarketsData[market.value] = marketData;
189
- }),
190
- ...aaveV4Spokes.map(async (spoke) => {
191
- const spokeData = await _getAaveV4SpokeData(client, network, spoke);
192
- aaveV4SpokesData[spoke.value] = spokeData;
193
- }),
194
- ...aaveV2Markets.map(async (market) => {
195
- const marketData = await _getAaveV2MarketsData(client, network, market);
196
- aaveV2MarketsData[market.value] = marketData;
197
- }),
198
- ...compoundV2Markets.map(async (market) => {
199
- const marketData = await _getCompoundV2MarketsData(client, network);
200
- compoundV2MarketsData[market.value] = marketData;
201
- }),
202
- ...crvUsdMarkets.map(async (market) => {
203
- const marketData = await _getCurveUsdGlobalData(client, network, market);
204
- crvUsdMarketsData[market.value] = marketData;
205
- }),
206
- ...llamaLendMarkets.map(async (market) => {
207
- const marketData = await _getLlamaLendGlobalData(client, network, market);
208
- llamaLendMarketsData[market.value] = marketData;
209
- }),
210
- ...liquityV2Markets.map(async (market) => {
211
- const marketData = await _getLiquityV2MarketData(client, network, market);
212
- liquityV2MarketsData[market.value] = marketData;
213
- }),
214
-
215
- // === INDEPENDENT USER DATA (doesn't depend on market data) ===
216
- ...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) => {
217
201
  if (!isMainnet) return; // Maker CDPs are only available on mainnet
218
- const makerCdp = await _getUserCdps(client, network, address);
219
- 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
+ }));
220
221
  }),
221
- ...addresses.map(async (address) => {
222
+ ...positionsAddresses.map(async (address) => {
222
223
  try {
223
224
  if (!isFluidSupported) return; // Fluid is not available on Optimism
224
225
  const userPositions = (await _getUserPositionsPortfolio(client, network, address));
@@ -235,71 +236,94 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
235
236
  };
236
237
  }
237
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
+ }),
238
249
 
239
- // === STAKING DATA (independent of market data) ===
240
- ...addresses.map(async (address) => {
250
+ // === STAKING DATA (independent of market data; Morpho Earn is with the positions below) ===
251
+ ...stakingAddresses.map(async (address) => {
241
252
  try {
242
253
  if (!isFluidSupported) return;
243
- stakingPositions[address.toLowerCase()].fluid = await _getAllUserEarnPositionsWithFTokens(client, network, address);
254
+ stakingPositions[address.toLowerCase() as EthAddress].fluid = await _getAllUserEarnPositionsWithFTokens(client, network, address);
244
255
  } catch (error) {
245
256
  console.error(`Error fetching Fluid lend data for address ${address}:`, error);
246
- stakingPositions[address.toLowerCase()].fluid = { error: `Error fetching Fluid lend data for address ${address}`, data: null };
257
+ stakingPositions[address.toLowerCase() as EthAddress].fluid = { error: `Error fetching Fluid lend data for address ${address}`, data: null };
247
258
  }
248
259
  }),
249
- ...addresses.map(async (address) => {
260
+ ...stakingAddresses.map(async (address) => {
250
261
  try {
251
262
  if (!isMainnet) return;
252
- stakingPositions[address.toLowerCase()].liquity = await getLiquityStakingData(client, network, address);
263
+ stakingPositions[address.toLowerCase() as EthAddress].liquity = await getLiquityStakingData(client, network, address);
253
264
  } catch (error) {
254
265
  console.error(`Error fetching Liquity staking data for address ${address}:`, error);
255
- stakingPositions[address.toLowerCase()].liquity = { error: `Error fetching Liquity staking data for address ${address}`, data: null };
266
+ stakingPositions[address.toLowerCase() as EthAddress].liquity = { error: `Error fetching Liquity staking data for address ${address}`, data: null };
256
267
  }
257
268
  }),
258
- ...addresses.map(async (address) => {
269
+ ...stakingAddresses.map(async (address) => {
259
270
  try {
260
271
  if (!isMainnet) return;
261
- stakingPositions[address.toLowerCase()].aaveV3 = await getStakeAaveData(client, network, address);
272
+ stakingPositions[address.toLowerCase() as EthAddress].aaveV3 = await getStakeAaveData(client, network, address);
262
273
  } catch (error) {
263
274
  console.error(`Error fetching Aave V3 staking data for address ${address}:`, error);
264
- stakingPositions[address.toLowerCase()].aaveV3 = { error: `Error fetching Aave V3 staking data for address ${address}`, data: null };
275
+ stakingPositions[address.toLowerCase() as EthAddress].aaveV3 = { error: `Error fetching Aave V3 staking data for address ${address}`, data: null };
265
276
  }
266
277
  }),
267
- ...addresses.map(async (address) => {
278
+ ...stakingAddresses.map(async (address) => {
268
279
  try {
269
280
  if (!isMainnet) return;
270
- stakingPositions[address.toLowerCase()].umbrella = await getUmbrellaData(client, network, address);
281
+ stakingPositions[address.toLowerCase() as EthAddress].umbrella = await getUmbrellaData(client, network, address);
271
282
  } catch (error) {
272
283
  console.error(`Error fetching Umbrella staking data for address ${address}:`, error);
273
- stakingPositions[address.toLowerCase()].umbrella = { error: `Error fetching Umbrella staking data for address ${address}`, data: null };
284
+ stakingPositions[address.toLowerCase() as EthAddress].umbrella = { error: `Error fetching Umbrella staking data for address ${address}`, data: null };
274
285
  }
275
286
  }),
276
- // Liquity V2 staking
277
- ...liquityV2MarketsStaking.map(market => addresses.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
+ }));
278
302
  try {
279
303
  if (!isMainnet) {
280
- stakingPositions[address.toLowerCase()].liquityV2[market.value] = { error: '', data: null };
304
+ stakingPositions[address.toLowerCase() as EthAddress].liquityV2SBoldYBold = { error: '', data: null };
281
305
  return;
282
306
  }
283
- const liquityV2StakingData = await getLiquityV2Staking(client, network, market.value, address);
284
- stakingPositions[address.toLowerCase()].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 };
285
309
  } catch (error) {
286
- console.error(`Error fetching Liquity V2 staking data for address ${address}, market ${market.value}:`, error);
287
- stakingPositions[address.toLowerCase()].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 };
288
312
  }
289
- })).flat(),
313
+ }),
290
314
 
291
315
  // === REWARDS DATA (independent of market data) ===
292
316
  // Batch King rewards
293
317
  (async () => {
294
318
  try {
295
319
  if (!isMainnet) {
296
- for (const address of addresses) {
297
- rewardsData[address.toLowerCase()].king = { error: '', data: [] };
320
+ for (const address of rewardsAddresses) {
321
+ rewardsData[address.toLowerCase() as EthAddress].king = { error: '', data: [] };
298
322
  }
299
323
  return;
300
324
  }
301
- const kingRewards = await getKingRewards(client, network, addresses);
302
- for (const address of addresses) {
325
+ const kingRewards = await getKingRewards(client, network, rewardsAddresses);
326
+ for (const address of rewardsAddresses) {
303
327
  const lowerAddress = address.toLowerCase() as EthAddress;
304
328
  rewardsData[lowerAddress].king = {
305
329
  error: '',
@@ -308,7 +332,7 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
308
332
  }
309
333
  } catch (error) {
310
334
  console.error('Error fetching King rewards data in batch:', error);
311
- for (const address of addresses) {
335
+ for (const address of rewardsAddresses) {
312
336
  rewardsData[address.toLowerCase() as EthAddress].king = {
313
337
  error: 'Error fetching King rewards data in batch',
314
338
  data: null,
@@ -320,13 +344,13 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
320
344
  (async () => {
321
345
  try {
322
346
  if (!isMainnet) {
323
- for (const address of addresses) {
324
- rewardsData[address.toLowerCase()].uniswap = { error: '', data: [] };
347
+ for (const address of rewardsAddresses) {
348
+ rewardsData[address.toLowerCase() as EthAddress].uniswap = { error: '', data: [] };
325
349
  }
326
350
  return;
327
351
  }
328
- const uniswapRewards = await getUniswapRewards(client, network, addresses);
329
- for (const address of addresses) {
352
+ const uniswapRewards = await getUniswapRewards(client, network, rewardsAddresses);
353
+ for (const address of rewardsAddresses) {
330
354
  const lowerAddress = address.toLowerCase() as EthAddress;
331
355
  rewardsData[lowerAddress].uniswap = {
332
356
  error: '',
@@ -335,7 +359,7 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
335
359
  }
336
360
  } catch (error) {
337
361
  console.error('Error fetching Uniswap rewards data in batch:', error);
338
- for (const address of addresses) {
362
+ for (const address of rewardsAddresses) {
339
363
  rewardsData[address.toLowerCase() as EthAddress].uniswap = {
340
364
  error: 'Error fetching Uniswap rewards data in batch',
341
365
  data: null,
@@ -343,10 +367,10 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
343
367
  }
344
368
  }
345
369
  })(),
346
- ...sparkMarkets.map((market) => addresses.map(async address => {
370
+ ...sparkLending.markets(network).map((market) => rewardsAddresses.map(async address => {
347
371
  try {
348
372
  if (!isMainnet) {
349
- rewardsData[address.toLowerCase()].spark[market.value] = { error: '', data: [] };
373
+ rewardsData[address.toLowerCase() as EthAddress].spark[market.value] = { error: '', data: [] };
350
374
  return;
351
375
  }
352
376
  const sparkData = await fetchSparkRewards(client, network, address, market.providerAddress);
@@ -356,7 +380,7 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
356
380
  rewardsData[address.toLowerCase() as EthAddress].spark[market.value] = { error: `Error fetching Spark rewards data for address ${address}`, data: null };
357
381
  }
358
382
  })).flat(),
359
- ...addresses.map(async (address) => {
383
+ ...rewardsAddresses.map(async (address) => {
360
384
  try {
361
385
  const merklData = await getMerklUnclaimedRewards(address, network);
362
386
  rewardsData[address.toLowerCase() as EthAddress].merkl = { error: '', data: merklData };
@@ -365,27 +389,28 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
365
389
  rewardsData[address.toLowerCase() as EthAddress].merkl = { error: `Error fetching Merkl rewards data for address ${address}`, data: null };
366
390
  }
367
391
  }),
368
- ...aaveV3Markets.map(market => addresses.map(async (address) => {
392
+ ...aaveV3Lending.markets(network).map(market => rewardsAddresses.map(async (address) => {
369
393
  try {
370
394
  const aaveData = await getUnclaimedRewardsForAllMarkets(client, network, address, market.providerAddress);
371
395
  rewardsData[address.toLowerCase() as EthAddress].aaveV3[market.value] = { error: '', data: aaveData };
372
396
  } catch (error) {
373
397
  console.error(`Error fetching Aave V3 Merit rewards data for address ${address}:`, error);
374
- rewardsData[address.toLowerCase() as EthAddress].aaveV3 = { error: `Error fetching Aave V3 rewards data for address ${address}`, data: null };
398
+ // This market only: the other markets keep their rewards.
399
+ rewardsData[address.toLowerCase() as EthAddress].aaveV3[market.value] = { error: `Error fetching Aave V3 rewards data for address ${address} on market ${market.value}`, data: null };
375
400
  }
376
401
  })).flat(),
377
402
  // Batch Spark Airdrop rewards
378
403
  (async () => {
379
404
  try {
380
405
  if (!isMainnet) {
381
- for (const address of addresses) {
382
- rewardsData[address.toLowerCase()].spk = { error: '', data: [] };
406
+ for (const address of rewardsAddresses) {
407
+ rewardsData[address.toLowerCase() as EthAddress].spk = { error: '', data: [] };
383
408
  }
384
409
  return;
385
410
  }
386
411
 
387
- const sparkAirdropRewards = await fetchSparkAirdropRewards(client, network, addresses);
388
- for (const address of addresses) {
412
+ const sparkAirdropRewards = await fetchSparkAirdropRewards(client, network, rewardsAddresses);
413
+ for (const address of rewardsAddresses) {
389
414
  const lowerAddress = address.toLowerCase() as EthAddress;
390
415
  rewardsData[lowerAddress].spk = {
391
416
  error: '',
@@ -394,7 +419,7 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
394
419
  }
395
420
  } catch (error) {
396
421
  console.error('Error fetching Spark Airdrop rewards data in batch:', error);
397
- for (const address of addresses) {
422
+ for (const address of rewardsAddresses) {
398
423
  rewardsData[address.toLowerCase() as EthAddress].spk = {
399
424
  error: 'Error fetching Spark Airdrop rewards data in batch',
400
425
  data: null,
@@ -408,8 +433,8 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
408
433
  return;
409
434
  }
410
435
 
411
- const ethenaAirdropRewards = await fetchEthenaAirdropRewards(addresses);
412
- for (const address of addresses) {
436
+ const ethenaAirdropRewards = await fetchEthenaAirdropRewards(rewardsAddresses);
437
+ for (const address of rewardsAddresses) {
413
438
  const lowerAddress = address.toLowerCase() as EthAddress;
414
439
  rewardsData[lowerAddress].ethena = {
415
440
  error: '',
@@ -418,7 +443,7 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
418
443
  }
419
444
  } catch (error) {
420
445
  console.error('Error fetching Ethena Airdrop rewards data:', error);
421
- for (const address of addresses) {
446
+ for (const address of rewardsAddresses) {
422
447
  rewardsData[address.toLowerCase() as EthAddress].ethena = {
423
448
  error: 'Error fetching Ethena Airdrop rewards data in batch',
424
449
  data: null,
@@ -426,162 +451,63 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
426
451
  }
427
452
  }
428
453
  })(),
429
- ]);
430
454
 
431
- await Promise.all([
432
- ...aaveV3Markets.map((market) => allAddresses.map(async (address) => {
433
- try {
434
- const accData = await _getAaveV3AccountData(client, network, address, { selectedMarket: market, ...aaveV3MarketsData[market.value] });
435
- if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].aaveV3[market.value] = { error: '', data: accData };
436
- } catch (error) {
437
- console.error(`Error fetching AaveV3 account data for address ${address} on market ${market.value}:`, error);
438
- positions[address.toLowerCase() as EthAddress].aaveV3[market.value] = { error: `Error fetching AaveV3 account data for address ${address} on market ${market.value}`, data: null };
439
- }
440
- })).flat(),
441
- ...aaveV4Spokes.map((spoke) => allAddresses.map(async (address) => {
442
- try {
443
- const accData = await _getAaveV4AccountData(client, network, aaveV4SpokesData[spoke.value], address);
444
- if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].aaveV4[spoke.value] = { error: '', data: accData };
445
- } catch (error) {
446
- console.error(`Error fetching AaveV4 account data for address ${address} on spoke ${spoke.value}:`, error);
447
- positions[address.toLowerCase() as EthAddress].aaveV4[spoke.value] = { error: `Error fetching AaveV4 account data for address ${address} on spoke ${spoke.value}`, data: null };
448
- }
449
- })).flat(),
450
- ...morphoMarkets.map((market) => addresses.map(async (address) => {
451
- try {
452
- const [accDataPromise, earnDataPromise] = await Promise.allSettled([
453
- _getMorphoBlueAccountData(client, network, address, market, morphoMarketsData[market.value]),
454
- getMorphoEarn(client, network, address, market, morphoMarketsData[market.value]),
455
- ]);
456
- if (accDataPromise.status === 'rejected') {
457
- console.error(`Error fetching MorphoBlue account data for address ${address} on market ${market.value}:`, accDataPromise.reason);
458
- positions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = { error: `Error fetching MorphoBlue account data for address ${address} on market ${market.value}`, data: null };
459
- }
460
- if (earnDataPromise.status === 'rejected') {
461
- console.error(`Error fetching MorphoBlue account data for address ${address} on market ${market.value}:`, earnDataPromise.reason);
462
- positions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = { error: `Error fetching MorphoBlue account data for address ${address} on market ${market.value}`, data: null };
463
- }
464
- if (accDataPromise.status !== 'rejected') {
465
- const accData = accDataPromise.value;
466
- if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = { error: '', data: accData };
467
- }
468
- if (earnDataPromise.status !== 'rejected') {
469
- const earnData = earnDataPromise.value;
470
- if (earnData && new Dec(earnData.amount).gt(0)) {
471
- stakingPositions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = {
472
- error: '',
473
- data: earnData,
474
- };
475
- }
476
- }
477
- } catch (error) {
478
- console.error(`Error fetching MorphoBlue account data for address ${address} on market ${market.value}:`, error);
479
- positions[address.toLowerCase() as EthAddress].morphoBlue[market.value] = { error: `Error fetching MorphoBlue account data for address ${address} on market ${market.value}`, data: null };
480
- }
481
- })).flat(),
482
- ...morphoMidnightMarkets.map((market) => addresses.map(async (address) => {
483
- try {
484
- // Markets are created lazily on the first position, so an uncreated one can hold no position.
485
- if (!morphoMidnightMarketsData[market.value]?.isCreated) return;
486
- const accData = await _getMorphoMidnightAccountData(client, network, address, market, morphoMidnightMarketsData[market.value]);
487
- if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].morphoMidnight[market.value] = { error: '', data: accData };
488
- } catch (error) {
489
- console.error(`Error fetching MorphoMidnight account data for address ${address} on market ${market.value}:`, error);
490
- positions[address.toLowerCase() as EthAddress].morphoMidnight[market.value] = { error: `Error fetching MorphoMidnight account data for address ${address} on market ${market.value}`, data: null };
491
- }
492
- })).flat(),
493
- ...compoundV3Markets.map((market) => addresses.map(async (address) => {
494
- try {
495
- const accData = await _getCompoundV3AccountData(client, network, address, ZERO_ADDRESS, { selectedMarket: market, assetsData: compoundV3MarketsData[market.value].assetsData });
496
- if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].compoundV3[market.value] = { error: '', data: accData };
497
- } catch (error) {
498
- console.error(`Error fetching CompoundV3 account data for address ${address} on market ${market.value}:`, error);
499
- positions[address.toLowerCase() as EthAddress].compoundV3[market.value] = { error: `Error fetching CompoundV3 account data for address ${address} on market ${market.value}`, data: null };
500
- }
501
- })).flat(),
502
- ...sparkMarkets.map((market) => allAddresses.map(async (address) => {
503
- try {
504
- const accData = await _getSparkAccountData(client, network, address, { selectedMarket: market, assetsData: sparkMarketsData[market.value].assetsData, eModeCategoriesData: sparkMarketsData[market.value].eModeCategoriesData });
505
- if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].spark[market.value] = { error: '', data: accData };
506
- } catch (error) {
507
- console.error(`Error fetching Spark account data for address ${address} on market ${market.value}:`, error);
508
- positions[address.toLowerCase() as EthAddress].spark[market.value] = { error: `Error fetching Spark account data for address ${address} on market ${market.value}`, data: null };
509
- }
510
- })).flat(),
511
- ...addresses.map((address) => (makerCdps[address.toLowerCase() as EthAddress] || []).map(async (cdpInfo) => {
512
- try {
513
- const cdpData = await _getMakerCdpData(client, network, cdpInfo);
514
- if (cdpData) {
515
- positions[address.toLowerCase() as EthAddress].maker[cdpInfo.id] = { error: '', data: cdpData };
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 };
516
464
  }
517
465
  } catch (error) {
518
- console.error(`Error fetching Maker CDP data for address ${address} with ID ${cdpInfo.id}:`, error);
519
- positions[address.toLowerCase() as EthAddress].maker[cdpInfo.id] = { error: `Error fetching Maker CDP data for address ${address} with ID ${cdpInfo.id}`, data: null };
520
- }
521
- })).flat(),
522
- ...aaveV2Markets.map((market) => addresses.map(async (address) => {
523
- try {
524
- const accData = await _getAaveV2AccountData(client, network, address, aaveV2MarketsData[market.value].assetsData, market);
525
- if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].aaveV2[market.value] = { error: '', data: accData };
526
- } catch (error) {
527
- console.error(`Error fetching AaveV2 account data for address ${address}:`, error);
528
- positions[address.toLowerCase() as EthAddress].aaveV2[market.value] = { error: `Error fetching AaveV2 account data for address ${address}`, data: null };
529
- }
530
- })).flat(),
531
- ...compoundV2Markets.map((market) => addresses.map(async (address) => {
532
- try {
533
- const accData = await _getCompoundV2AccountData(client, network, address, compoundV2MarketsData[market.value].assetsData);
534
- if (new Dec(accData.suppliedUsd).gt(0)) positions[address.toLowerCase() as EthAddress].compoundV2[market.value] = { error: '', data: accData };
535
- } catch (error) {
536
- console.error(`Error fetching CompoundV2 account data for address ${address}:`, error);
537
- positions[address.toLowerCase() as EthAddress].compoundV2[market.value] = { error: `Error fetching CompoundV2 account data for address ${address}`, data: null };
538
- }
539
- })).flat(),
540
- ...addresses.map(async (address) => {
541
- try {
542
- if (!isMainnet) return; // Liquity trove info is only available on mainnet
543
- const troveInfo = await _getLiquityTroveInfo(client, network, address);
544
- if (new Dec(troveInfo.collateral).gt(0)) positions[address.toLowerCase() as EthAddress].liquity = { error: '', data: troveInfo };
545
- } catch (error) {
546
- console.error(`Error fetching Liquity trove info for address ${address}:`, error);
547
- positions[address.toLowerCase() as EthAddress].liquity = { error: `Error fetching Liquity trove info for address ${address}`, data: null };
548
- }
549
- }),
550
- ...crvUsdMarkets.map((market) => addresses.map(async (address) => {
551
- try {
552
- const accData = await _getCurveUsdUserData(client, network, address, market, crvUsdMarketsData[market.value].activeBand, crvUsdMarketsData[market.value].borrowRate);
553
- if (new Dec(accData.suppliedUsd).gt(0) || new Dec(accData.borrowedUsd).gt(0)) {
554
- positions[address.toLowerCase() as EthAddress].crvUsd[market.value] = { error: '', data: accData };
555
- }
556
- } catch (error) {
557
- console.error(`Error fetching Curve USD account data for address ${address} on market ${market.value}:`, error);
558
- positions[address.toLowerCase() as EthAddress].crvUsd[market.value] = { error: `Error fetching Curve USD account data for address ${address} on market ${market.value}`, data: null };
559
- }
560
- })).flat(),
561
- ...llamaLendMarkets.map((market) => addresses.map(async (address) => {
562
- try {
563
- const accData = await _getLlamaLendUserData(client, network, address, market, llamaLendMarketsData[market.value]);
564
- if (new Dec(accData.suppliedUsd).gt(0) || new Dec(accData.borrowedUsd).gt(0)) {
565
- positions[address.toLowerCase() as EthAddress].llamaLend[market.value] = { error: '', data: { ...accData, borrowRate: llamaLendMarketsData[market.value].borrowRate } };
566
- }
567
- } catch (error) {
568
- console.error(`Error fetching LlamaLend account data for address ${address} on market ${market.value}:`, error);
569
- positions[address.toLowerCase() as EthAddress].llamaLend[market.value] = { error: `Error fetching LlamaLend account data for address ${address} on market ${market.value}`, data: null };
570
- }
571
- })).flat(),
572
- // liquity sBold/yBold and staking options
573
- ...addresses.map(async (address) => {
574
- try {
575
- if (!isMainnet) {
576
- stakingPositions[address.toLowerCase() as EthAddress].liquityV2SBoldYBold = { error: '', data: null };
577
- return;
578
- }
579
- const data = await getLiquitySAndYBold(client, network, stakingPositions[address.toLowerCase()].liquityV2, address);
580
- stakingPositions[address.toLowerCase() as EthAddress].liquityV2SBoldYBold = { error: '', data };
581
- } catch (error) {
582
- console.error(`Error fetching SBold/YBold data for address ${address}:`, error);
583
- stakingPositions[address.toLowerCase() as EthAddress].liquityV2SBoldYBold = { error: `Error fetching sBold/yBold data for address ${address}`, data: null };
584
- }
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
+ }));
585
511
  }),
586
512
  ]);
587
513
 
@@ -589,14 +515,63 @@ export async function getPortfolioData(provider: EthereumProvider, network: Netw
589
515
  positions,
590
516
  stakingPositions,
591
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,
592
563
  markets,
564
+ errors: {
565
+ ...(Object.keys(marketsErrors).length ? { markets: marketsErrors } : {}),
566
+ ...userData.errors,
567
+ },
593
568
  };
594
569
  }
595
570
 
596
571
 
597
572
  export async function getShifterPortfolioData(provider: EthereumProvider, network: NetworkNumber, defaultProvider: EthereumProvider, addresses: EthAddress[], isSim = false): Promise<{
598
573
  positions: PortfolioPositionsData;
599
- markets: PortfolioMarketsData;
574
+ markets: ShifterPortfolioMarketsData;
600
575
  }> {
601
576
  const isMainnet = network === NetworkNumber.Eth;
602
577
  const isFluidSupported = [NetworkNumber.Eth, NetworkNumber.Arb, NetworkNumber.Base, NetworkNumber.Plasma].includes(network);
@@ -616,7 +591,7 @@ export async function getShifterPortfolioData(provider: EthereumProvider, networ
616
591
  const client = getViemProvider(provider, ...args);
617
592
  const defaultClient = getViemProvider(defaultProvider, ...args);
618
593
 
619
- const markets: PortfolioMarketsData = {
594
+ const markets: ShifterPortfolioMarketsData = {
620
595
  morphoMarketsData: {},
621
596
  morphoMidnightMarketsData: {}, // Morpho Midnight is not fetched for the shifter; key kept so the payload shape is stable
622
597
  compoundV3MarketsData: {},