@moonwell-fi/moonwell-sdk 0.23.0 → 0.25.0

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 (199) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/_cjs/actions/axiosWithRetry.js +10 -11
  3. package/_cjs/actions/axiosWithRetry.js.map +1 -1
  4. package/_cjs/actions/core/getUserBalances.js +63 -32
  5. package/_cjs/actions/core/getUserBalances.js.map +1 -1
  6. package/_cjs/actions/core/markets/common.js +31 -42
  7. package/_cjs/actions/core/markets/common.js.map +1 -1
  8. package/_cjs/actions/core/markets/getMarkets.js +7 -3
  9. package/_cjs/actions/core/markets/getMarkets.js.map +1 -1
  10. package/_cjs/actions/core/user-positions/common.js +56 -60
  11. package/_cjs/actions/core/user-positions/common.js.map +1 -1
  12. package/_cjs/actions/core/user-positions/getUserPositions.js +9 -6
  13. package/_cjs/actions/core/user-positions/getUserPositions.js.map +1 -1
  14. package/_cjs/actions/governance/getStakingInfo.js +1 -7
  15. package/_cjs/actions/governance/getStakingInfo.js.map +1 -1
  16. package/_cjs/actions/governance/getUserStakingInfo.js +1 -7
  17. package/_cjs/actions/governance/getUserStakingInfo.js.map +1 -1
  18. package/_cjs/actions/governance/getWellPrice.js +51 -1
  19. package/_cjs/actions/governance/getWellPrice.js.map +1 -1
  20. package/_cjs/actions/governance/proposals/common.js +34 -15
  21. package/_cjs/actions/governance/proposals/common.js.map +1 -1
  22. package/_cjs/actions/governance/proposals/getProposals.js +5 -4
  23. package/_cjs/actions/governance/proposals/getProposals.js.map +1 -1
  24. package/_cjs/actions/index.js +3 -1
  25. package/_cjs/actions/index.js.map +1 -1
  26. package/_cjs/actions/morpho/markets/common.js +155 -26
  27. package/_cjs/actions/morpho/markets/common.js.map +1 -1
  28. package/_cjs/actions/morpho/markets/getMorphoMarket.js +1 -0
  29. package/_cjs/actions/morpho/markets/getMorphoMarket.js.map +1 -1
  30. package/_cjs/actions/morpho/markets/getMorphoMarkets.js +1 -0
  31. package/_cjs/actions/morpho/markets/getMorphoMarkets.js.map +1 -1
  32. package/_cjs/actions/morpho/markets/getMorphoMarketsSharedLiquidity.js +46 -0
  33. package/_cjs/actions/morpho/markets/getMorphoMarketsSharedLiquidity.js.map +1 -0
  34. package/_cjs/actions/morpho/user-positions/common.js +26 -24
  35. package/_cjs/actions/morpho/user-positions/common.js.map +1 -1
  36. package/_cjs/actions/morpho/user-positions/getMorphoMarketUserPositions.js +7 -8
  37. package/_cjs/actions/morpho/user-positions/getMorphoMarketUserPositions.js.map +1 -1
  38. package/_cjs/actions/morpho/user-positions/getMorphoVaultUserPositions.js +7 -8
  39. package/_cjs/actions/morpho/user-positions/getMorphoVaultUserPositions.js.map +1 -1
  40. package/_cjs/actions/morpho/user-rewards/common.js +1 -7
  41. package/_cjs/actions/morpho/user-rewards/common.js.map +1 -1
  42. package/_cjs/actions/morpho/vaults/common.js +104 -87
  43. package/_cjs/actions/morpho/vaults/common.js.map +1 -1
  44. package/_cjs/actions/morpho/vaults/lunarIndexerTransform.js +31 -3
  45. package/_cjs/actions/morpho/vaults/lunarIndexerTransform.js.map +1 -1
  46. package/_cjs/actions/retry.js +43 -13
  47. package/_cjs/actions/retry.js.map +1 -1
  48. package/_cjs/client/createActions.js +2 -0
  49. package/_cjs/client/createActions.js.map +1 -1
  50. package/_cjs/common/error.js +37 -1
  51. package/_cjs/common/error.js.map +1 -1
  52. package/_cjs/common/index.js +2 -1
  53. package/_cjs/common/index.js.map +1 -1
  54. package/_cjs/common/readAcrossEnvironments.js +17 -0
  55. package/_cjs/common/readAcrossEnvironments.js.map +1 -0
  56. package/_cjs/environments/types/config.js.map +1 -1
  57. package/_cjs/errors/version.js +1 -1
  58. package/_cjs/index.js +3 -1
  59. package/_cjs/index.js.map +1 -1
  60. package/_esm/actions/axiosWithRetry.js +11 -13
  61. package/_esm/actions/axiosWithRetry.js.map +1 -1
  62. package/_esm/actions/core/getUserBalances.js +72 -35
  63. package/_esm/actions/core/getUserBalances.js.map +1 -1
  64. package/_esm/actions/core/markets/common.js +36 -43
  65. package/_esm/actions/core/markets/common.js.map +1 -1
  66. package/_esm/actions/core/markets/getMarkets.js +7 -3
  67. package/_esm/actions/core/markets/getMarkets.js.map +1 -1
  68. package/_esm/actions/core/user-positions/common.js +65 -63
  69. package/_esm/actions/core/user-positions/common.js.map +1 -1
  70. package/_esm/actions/core/user-positions/getUserPositions.js +14 -6
  71. package/_esm/actions/core/user-positions/getUserPositions.js.map +1 -1
  72. package/_esm/actions/governance/getStakingInfo.js +2 -8
  73. package/_esm/actions/governance/getStakingInfo.js.map +1 -1
  74. package/_esm/actions/governance/getUserStakingInfo.js +2 -8
  75. package/_esm/actions/governance/getUserStakingInfo.js.map +1 -1
  76. package/_esm/actions/governance/getWellPrice.js +67 -7
  77. package/_esm/actions/governance/getWellPrice.js.map +1 -1
  78. package/_esm/actions/governance/proposals/common.js +37 -12
  79. package/_esm/actions/governance/proposals/common.js.map +1 -1
  80. package/_esm/actions/governance/proposals/getProposals.js +7 -5
  81. package/_esm/actions/governance/proposals/getProposals.js.map +1 -1
  82. package/_esm/actions/index.js +1 -0
  83. package/_esm/actions/index.js.map +1 -1
  84. package/_esm/actions/morpho/markets/common.js +168 -27
  85. package/_esm/actions/morpho/markets/common.js.map +1 -1
  86. package/_esm/actions/morpho/markets/getMorphoMarket.js +1 -0
  87. package/_esm/actions/morpho/markets/getMorphoMarket.js.map +1 -1
  88. package/_esm/actions/morpho/markets/getMorphoMarkets.js +1 -0
  89. package/_esm/actions/morpho/markets/getMorphoMarkets.js.map +1 -1
  90. package/_esm/actions/morpho/markets/getMorphoMarketsSharedLiquidity.js +48 -0
  91. package/_esm/actions/morpho/markets/getMorphoMarketsSharedLiquidity.js.map +1 -0
  92. package/_esm/actions/morpho/user-positions/common.js +31 -24
  93. package/_esm/actions/morpho/user-positions/common.js.map +1 -1
  94. package/_esm/actions/morpho/user-positions/getMorphoMarketUserPositions.js +12 -8
  95. package/_esm/actions/morpho/user-positions/getMorphoMarketUserPositions.js.map +1 -1
  96. package/_esm/actions/morpho/user-positions/getMorphoVaultUserPositions.js +12 -8
  97. package/_esm/actions/morpho/user-positions/getMorphoVaultUserPositions.js.map +1 -1
  98. package/_esm/actions/morpho/user-rewards/common.js +2 -8
  99. package/_esm/actions/morpho/user-rewards/common.js.map +1 -1
  100. package/_esm/actions/morpho/vaults/common.js +110 -91
  101. package/_esm/actions/morpho/vaults/common.js.map +1 -1
  102. package/_esm/actions/morpho/vaults/lunarIndexerTransform.js +36 -4
  103. package/_esm/actions/morpho/vaults/lunarIndexerTransform.js.map +1 -1
  104. package/_esm/actions/retry.js +45 -15
  105. package/_esm/actions/retry.js.map +1 -1
  106. package/_esm/client/createActions.js +2 -0
  107. package/_esm/client/createActions.js.map +1 -1
  108. package/_esm/common/error.js +49 -0
  109. package/_esm/common/error.js.map +1 -1
  110. package/_esm/common/index.js +1 -1
  111. package/_esm/common/index.js.map +1 -1
  112. package/_esm/common/readAcrossEnvironments.js +30 -0
  113. package/_esm/common/readAcrossEnvironments.js.map +1 -0
  114. package/_esm/environments/types/config.js.map +1 -1
  115. package/_esm/errors/version.js +1 -1
  116. package/_esm/index.js +1 -0
  117. package/_esm/index.js.map +1 -1
  118. package/_types/actions/axiosWithRetry.d.ts +1 -0
  119. package/_types/actions/axiosWithRetry.d.ts.map +1 -1
  120. package/_types/actions/core/getUserBalances.d.ts.map +1 -1
  121. package/_types/actions/core/markets/common.d.ts.map +1 -1
  122. package/_types/actions/core/markets/getMarkets.d.ts.map +1 -1
  123. package/_types/actions/core/user-positions/common.d.ts.map +1 -1
  124. package/_types/actions/core/user-positions/getUserPositions.d.ts +5 -0
  125. package/_types/actions/core/user-positions/getUserPositions.d.ts.map +1 -1
  126. package/_types/actions/governance/getStakingInfo.d.ts.map +1 -1
  127. package/_types/actions/governance/getUserStakingInfo.d.ts.map +1 -1
  128. package/_types/actions/governance/getWellPrice.d.ts +9 -1
  129. package/_types/actions/governance/getWellPrice.d.ts.map +1 -1
  130. package/_types/actions/governance/proposals/common.d.ts.map +1 -1
  131. package/_types/actions/governance/proposals/getProposals.d.ts +10 -1
  132. package/_types/actions/governance/proposals/getProposals.d.ts.map +1 -1
  133. package/_types/actions/index.d.ts +1 -0
  134. package/_types/actions/index.d.ts.map +1 -1
  135. package/_types/actions/morpho/markets/common.d.ts +7 -10
  136. package/_types/actions/morpho/markets/common.d.ts.map +1 -1
  137. package/_types/actions/morpho/markets/getMorphoMarket.d.ts +2 -0
  138. package/_types/actions/morpho/markets/getMorphoMarket.d.ts.map +1 -1
  139. package/_types/actions/morpho/markets/getMorphoMarkets.d.ts +2 -0
  140. package/_types/actions/morpho/markets/getMorphoMarkets.d.ts.map +1 -1
  141. package/_types/actions/morpho/markets/getMorphoMarketsSharedLiquidity.d.ts +17 -0
  142. package/_types/actions/morpho/markets/getMorphoMarketsSharedLiquidity.d.ts.map +1 -0
  143. package/_types/actions/morpho/user-positions/common.d.ts.map +1 -1
  144. package/_types/actions/morpho/user-positions/getMorphoMarketUserPositions.d.ts +5 -0
  145. package/_types/actions/morpho/user-positions/getMorphoMarketUserPositions.d.ts.map +1 -1
  146. package/_types/actions/morpho/user-positions/getMorphoVaultUserPositions.d.ts +5 -0
  147. package/_types/actions/morpho/user-positions/getMorphoVaultUserPositions.d.ts.map +1 -1
  148. package/_types/actions/morpho/user-rewards/common.d.ts.map +1 -1
  149. package/_types/actions/morpho/vaults/common.d.ts.map +1 -1
  150. package/_types/actions/morpho/vaults/lunarIndexerTransform.d.ts.map +1 -1
  151. package/_types/actions/retry.d.ts +5 -4
  152. package/_types/actions/retry.d.ts.map +1 -1
  153. package/_types/client/createActions.d.ts +2 -0
  154. package/_types/client/createActions.d.ts.map +1 -1
  155. package/_types/common/error.d.ts +31 -0
  156. package/_types/common/error.d.ts.map +1 -1
  157. package/_types/common/index.d.ts +2 -2
  158. package/_types/common/index.d.ts.map +1 -1
  159. package/_types/common/readAcrossEnvironments.d.ts +23 -0
  160. package/_types/common/readAcrossEnvironments.d.ts.map +1 -0
  161. package/_types/environments/types/config.d.ts +11 -1
  162. package/_types/environments/types/config.d.ts.map +1 -1
  163. package/_types/errors/version.d.ts +1 -1
  164. package/_types/index.d.ts +3 -1
  165. package/_types/index.d.ts.map +1 -1
  166. package/_types/types/morphoMarket.d.ts +9 -0
  167. package/_types/types/morphoMarket.d.ts.map +1 -1
  168. package/actions/axiosWithRetry.ts +19 -13
  169. package/actions/core/getUserBalances.ts +98 -51
  170. package/actions/core/markets/common.ts +49 -51
  171. package/actions/core/markets/getMarkets.ts +9 -5
  172. package/actions/core/user-positions/common.ts +107 -111
  173. package/actions/core/user-positions/getUserPositions.ts +11 -10
  174. package/actions/governance/getStakingInfo.ts +2 -9
  175. package/actions/governance/getUserStakingInfo.ts +2 -8
  176. package/actions/governance/getWellPrice.ts +99 -7
  177. package/actions/governance/proposals/common.ts +44 -19
  178. package/actions/governance/proposals/getProposals.ts +22 -5
  179. package/actions/index.ts +5 -0
  180. package/actions/morpho/markets/common.ts +324 -125
  181. package/actions/morpho/markets/getMorphoMarket.ts +3 -0
  182. package/actions/morpho/markets/getMorphoMarkets.ts +3 -0
  183. package/actions/morpho/markets/getMorphoMarketsSharedLiquidity.ts +93 -0
  184. package/actions/morpho/user-positions/common.ts +48 -42
  185. package/actions/morpho/user-positions/getMorphoMarketUserPositions.ts +14 -15
  186. package/actions/morpho/user-positions/getMorphoVaultUserPositions.ts +14 -15
  187. package/actions/morpho/user-rewards/common.ts +2 -8
  188. package/actions/morpho/vaults/common.ts +161 -127
  189. package/actions/morpho/vaults/lunarIndexerTransform.ts +41 -6
  190. package/actions/retry.ts +53 -18
  191. package/client/createActions.ts +13 -0
  192. package/common/error.ts +45 -0
  193. package/common/index.ts +2 -2
  194. package/common/readAcrossEnvironments.ts +45 -0
  195. package/environments/types/config.ts +11 -1
  196. package/errors/version.ts +1 -1
  197. package/index.ts +3 -0
  198. package/package.json +1 -1
  199. package/types/morphoMarket.ts +10 -0
@@ -30,49 +30,74 @@ export type GetUserBalancesReturnType = Promise<UserBalance[]>;
30
30
  * empty" and disabled the confirm button for 59 users (Sentry
31
31
  * MOONWELL-FRONTEND-195). Callers settle these with `Promise.allSettled` and
32
32
  * omit the rejected tokens, so a consumer that finds no entry knows the balance
33
- * is UNKNOWN rather than zero. The failure is routed to `environment.onError`
34
- * (source `user-balances-token-read`) so Sentry-wired consumers can see the
35
- * degraded read.
33
+ * is UNKNOWN rather than zero. Failures are routed to `environment.onError`
34
+ * (source `user-balances-token-read`) once per chain by `getUserBalances`, so
35
+ * Sentry-wired consumers can see the degraded read.
36
36
  */
37
37
  const getTokenBalance = async (
38
38
  environment: Environment,
39
39
  userAddress: Address,
40
40
  tokenAddress: Address,
41
41
  ): Promise<{ amount: bigint; token: `0x${string}` }> => {
42
- try {
43
- if (tokenAddress === zeroAddress) {
44
- const balance = await environment.publicClient.getBalance({
45
- address: userAddress,
46
- });
47
- return { amount: BigInt(balance), token: tokenAddress };
48
- }
49
-
50
- const erc20Abi = parseAbi([
51
- "function balanceOf(address owner) view returns (uint256)",
52
- ]);
53
-
54
- const erc20Contract = getContract({
55
- address: tokenAddress,
56
- abi: erc20Abi,
57
- client: environment.publicClient,
42
+ if (tokenAddress === zeroAddress) {
43
+ const balance = await environment.publicClient.getBalance({
44
+ address: userAddress,
58
45
  });
59
-
60
- const balance = await erc20Contract.read.balanceOf([userAddress]);
61
46
  return { amount: BigInt(balance), token: tokenAddress };
62
- } catch (error) {
63
- environment.onError?.(error, {
64
- source: "user-balances-token-read",
65
- chainId: environment.chainId,
66
- token: tokenAddress,
67
- });
68
- throw error;
69
47
  }
48
+
49
+ const erc20Abi = parseAbi([
50
+ "function balanceOf(address owner) view returns (uint256)",
51
+ ]);
52
+
53
+ const erc20Contract = getContract({
54
+ address: tokenAddress,
55
+ abi: erc20Abi,
56
+ client: environment.publicClient,
57
+ });
58
+
59
+ const balance = await erc20Contract.read.balanceOf([userAddress]);
60
+ return { amount: BigInt(balance), token: tokenAddress };
61
+ };
62
+
63
+ type TokenBalanceReads = {
64
+ balances: { amount: bigint; token: `0x${string}` }[];
65
+ failures: { token: Address; error: unknown }[];
66
+ total: number;
67
+ };
68
+
69
+ /**
70
+ * Reads each token balance, keeping failures instead of reporting them one by
71
+ * one: viem batches these reads into one multicall, so a single failed request
72
+ * rejects every token on the chain.
73
+ */
74
+ const readTokenBalances = async (
75
+ environment: Environment,
76
+ userAddress: Address,
77
+ tokenAddresses: Address[],
78
+ ): Promise<TokenBalanceReads> => {
79
+ const results = await Promise.all(
80
+ tokenAddresses.map(async (token) => {
81
+ try {
82
+ return {
83
+ balance: await getTokenBalance(environment, userAddress, token),
84
+ };
85
+ } catch (error) {
86
+ return { failure: { token, error } };
87
+ }
88
+ }),
89
+ );
90
+ return {
91
+ balances: results.flatMap((r) => (r.balance ? [r.balance] : [])),
92
+ failures: results.flatMap((r) => (r.failure ? [r.failure] : [])),
93
+ total: tokenAddresses.length,
94
+ };
70
95
  };
71
96
 
72
97
  async function getTokenBalancesFromEnvironment(
73
98
  environment: Environment,
74
99
  userAddress: Address,
75
- ): Promise<{ amount: bigint; token: `0x${string}` }[]> {
100
+ ): Promise<TokenBalanceReads> {
76
101
  // Try the views multicall first (single RPC for N tokens). Some chains ship a
77
102
  // staking-only views contract (Ethereum's at the time of writing) that doesn't
78
103
  // implement getTokensBalances and reverts on the call — in that case fall
@@ -87,7 +112,7 @@ async function getTokenBalancesFromEnvironment(
87
112
  userAddress,
88
113
  ]);
89
114
 
90
- return [...tokenBalancesFromView];
115
+ return { balances: [...tokenBalancesFromView], failures: [], total: 0 };
91
116
  } catch (error) {
92
117
  environment.onError?.(error, {
93
118
  source: "user-balances-views-fallback",
@@ -96,14 +121,10 @@ async function getTokenBalancesFromEnvironment(
96
121
  }
97
122
  }
98
123
 
99
- const tokenBalancesSettled = await Promise.allSettled(
100
- Object.values(environment.config.tokens).map((token) =>
101
- getTokenBalance(environment, userAddress, token.address),
102
- ),
103
- );
104
-
105
- return tokenBalancesSettled.flatMap((s) =>
106
- s.status === "fulfilled" ? s.value : [],
124
+ return readTokenBalances(
125
+ environment,
126
+ userAddress,
127
+ Object.values(environment.config.tokens).map((token) => token.address),
107
128
  );
108
129
  }
109
130
 
@@ -124,32 +145,58 @@ export async function getUserBalances<
124
145
  ),
125
146
  );
126
147
 
127
- const environmentsTokensBalances = environmentsTokensBalancesSettled.map(
128
- (s) => (s.status === "fulfilled" ? [...s.value] : []),
148
+ const environmentsTokenReads = environmentsTokensBalancesSettled.map((s) =>
149
+ s.status === "fulfilled"
150
+ ? s.value
151
+ : { balances: [], failures: [], total: 0 },
129
152
  );
153
+ const environmentsTokensBalances = environmentsTokenReads.map((r) => [
154
+ ...r.balances,
155
+ ]);
130
156
 
131
157
  // Fetch morpho staking balances
132
158
  await Promise.all(
133
159
  environments.map(async (env, index) => {
134
160
  if (!env.config.vaults) return;
135
161
 
136
- const vaultBalancesSettled = await Promise.allSettled(
137
- Object.values(env.config.vaults)
138
- .filter((vault) => vault.multiReward)
139
- .map((vault) =>
140
- getTokenBalance(env, userAddress, vault.multiReward!),
141
- ),
142
- );
143
-
144
- const vaultBalances = vaultBalancesSettled.flatMap((s) =>
145
- s.status === "fulfilled" ? s.value : [],
162
+ const vaultReads = await readTokenBalances(
163
+ env,
164
+ userAddress,
165
+ Object.values(env.config.vaults).flatMap((vault) =>
166
+ vault.multiReward ? [vault.multiReward] : [],
167
+ ),
146
168
  );
147
169
 
148
170
  const envBalances = environmentsTokensBalances[index] || [];
149
- environmentsTokensBalances[index] = [...envBalances, ...vaultBalances];
171
+ environmentsTokensBalances[index] = [
172
+ ...envBalances,
173
+ ...vaultReads.balances,
174
+ ];
175
+
176
+ const tokenReads = environmentsTokenReads[index];
177
+ if (tokenReads) {
178
+ tokenReads.failures.push(...vaultReads.failures);
179
+ tokenReads.total += vaultReads.total;
180
+ }
150
181
  }),
151
182
  );
152
183
 
184
+ // One report per chain for every token read that failed.
185
+ environments.forEach((env, index) => {
186
+ const reads = environmentsTokenReads[index];
187
+ const [firstFailure] = reads?.failures ?? [];
188
+ if (!reads || !firstFailure) return;
189
+ env.onError?.(firstFailure.error, {
190
+ source: "user-balances-token-read",
191
+ chainId: env.chainId,
192
+ ...(reads.failures.length === 1 && { token: firstFailure.token }),
193
+ operation: "token-balance-read",
194
+ failedCount: reads.failures.length,
195
+ totalCount: reads.total,
196
+ items: reads.failures.map((f) => f.token),
197
+ });
198
+ });
199
+
153
200
  const result = environments.flatMap((env, index) => {
154
201
  const balances = environmentsTokensBalances[index] || [];
155
202
 
@@ -17,7 +17,10 @@ import {
17
17
  findTokenByAddress,
18
18
  } from "../../../environments/utils/index.js";
19
19
  import type { Market } from "../../../types/market.js";
20
- import { getGovernanceTokenPriceFor } from "../../governance/getWellPrice.js";
20
+ import {
21
+ getGovernanceTokenPriceFor,
22
+ getGovernanceTokenPriceOrZero,
23
+ } from "../../governance/getWellPrice.js";
21
24
 
22
25
  export const getMarketsData = async (environment: Environment) => {
23
26
  // Moonriver (chainId 1285) should always use on-chain data
@@ -530,13 +533,7 @@ async function fetchMarketsFromLunar(
530
533
 
531
534
  const [nativeTokenPriceRaw, governanceTokenPriceRaw] = await Promise.all([
532
535
  homeEnvironment.contracts.views?.read.getNativeTokenPrice(),
533
- getGovernanceTokenPriceFor(environment).catch((err) => {
534
- environment.onError?.(err, {
535
- source: "governance-token-price",
536
- chainId: environment.chainId,
537
- });
538
- return 0n;
539
- }),
536
+ getGovernanceTokenPriceOrZero(environment),
540
537
  ]);
541
538
 
542
539
  if (nativeTokenPriceRaw === undefined) {
@@ -876,54 +873,55 @@ async function fetchMarketsFromLunar(
876
873
  }
877
874
 
878
875
  const fetchFromGenericCacheApi = async <T>(uri: string): Promise<T> => {
879
- const response = await fetch(
880
- "https://generic-api-cache.moonwell.workers.dev/",
881
- {
882
- method: "POST",
883
- body: `{"uri":"${uri}","cacheDuration":"300"}`,
884
- headers: {
885
- ...MOONWELL_FETCH_JSON_HEADERS,
886
- "Content-Type": "text/plain",
876
+ // AbortSignal.timeout is not available in every browser supported by SDK
877
+ // consumers. Keep the timeout local and release it after reading the body.
878
+ const controller = new AbortController();
879
+ const timer = setTimeout(() => controller.abort(), 5_000);
880
+ try {
881
+ const response = await fetch(
882
+ "https://generic-api-cache.moonwell.workers.dev/",
883
+ {
884
+ method: "POST",
885
+ body: `{"uri":"${uri}","cacheDuration":"300"}`,
886
+ signal: controller.signal,
887
+ headers: {
888
+ ...MOONWELL_FETCH_JSON_HEADERS,
889
+ "Content-Type": "text/plain",
890
+ },
887
891
  },
888
- },
889
- );
892
+ );
890
893
 
891
- return response.json() as T;
894
+ if (!response.ok)
895
+ throw new Error(`Staking APR request failed: ${response.status}`);
896
+ return await response.json();
897
+ } finally {
898
+ clearTimeout(timer);
899
+ }
892
900
  };
893
901
 
894
- export const fetchLiquidStakingRewards = async () => {
895
- const result = {
896
- cbETH: 0,
897
- rETH: 0,
898
- wstETH: 0,
899
- };
900
-
901
- try {
902
- const cbETH = await fetchFromGenericCacheApi<{ apy: string }>(
902
+ export const fetchLiquidStakingRewards = async (): Promise<{
903
+ cbETH: number;
904
+ rETH: number;
905
+ wstETH: number;
906
+ }> => {
907
+ // Each provider can succeed independently, and each has a bounded request.
908
+ // Preserve the existing zero fallback for callers of this optional enrichment.
909
+ const [cbETH, rETH, wstETH] = await Promise.all([
910
+ fetchFromGenericCacheApi<{ apy: string }>(
903
911
  "https://api.exchange.coinbase.com/wrapped-assets/CBETH",
904
- );
905
- result.cbETH = Number(cbETH.apy) * 100;
906
- } catch (error) {
907
- result.cbETH = 0;
908
- }
909
-
910
- try {
911
- const rETH = await fetchFromGenericCacheApi<{ rethAPR: string }>(
912
+ )
913
+ .then((result) => Number(result.apy) * 100)
914
+ .catch(() => 0),
915
+ fetchFromGenericCacheApi<{ rethAPR: string }>(
912
916
  "https://rocketpool.net/api/mainnet/payload",
913
- );
914
- result.rETH = Number(rETH.rethAPR);
915
- } catch (error) {
916
- result.rETH = 0;
917
- }
918
-
919
- try {
920
- const stETH = await fetchFromGenericCacheApi<{ data: { apr: number } }>(
917
+ )
918
+ .then((result) => Number(result.rethAPR))
919
+ .catch(() => 0),
920
+ fetchFromGenericCacheApi<{ data: { apr: number } }>(
921
921
  "https://eth-api.lido.fi/v1/protocol/steth/apr/last",
922
- );
923
- result.wstETH = stETH.data.apr;
924
- } catch (error) {
925
- result.wstETH = 0;
926
- }
927
-
928
- return result;
922
+ )
923
+ .then((result) => result.data.apr)
924
+ .catch(() => 0),
925
+ ]);
926
+ return { cbETH, rETH, wstETH };
929
927
  };
@@ -25,9 +25,14 @@ export async function getMarkets<
25
25
  const environments = getEnvironmentsFromArgs(client, args);
26
26
  const logId = logger.start("getMarkets", "Starting to get markets...");
27
27
 
28
- const settlements = await Promise.allSettled(
29
- environments.map((environment) => getMarketsData(environment)),
30
- );
28
+ const [settlements, liquidStakingRewards] = await Promise.all([
29
+ Promise.allSettled(
30
+ environments.map((environment) => getMarketsData(environment)),
31
+ ),
32
+ args?.includeLiquidStakingRewards === true
33
+ ? fetchLiquidStakingRewards()
34
+ : undefined,
35
+ ]);
31
36
 
32
37
  const result = settlements
33
38
  .filter(
@@ -35,8 +40,7 @@ export async function getMarkets<
35
40
  )
36
41
  .map((s) => s.value);
37
42
 
38
- if (args?.includeLiquidStakingRewards === true) {
39
- const liquidStakingRewards = await fetchLiquidStakingRewards();
43
+ if (liquidStakingRewards) {
40
44
  for (const item of result.flat()) {
41
45
  if (item.underlyingToken.symbol.toLowerCase() === "cbeth") {
42
46
  item.rewards.push({
@@ -15,117 +15,113 @@ export const getUserPositionData = async (params: {
15
15
  return [];
16
16
  }
17
17
 
18
- try {
19
- const [allMarketsResult, balancesResult, borrowsResult, membershipsResult] =
20
- await Promise.allSettled([
21
- viewsContract.read.getAllMarketsInfo(),
22
- viewsContract.read.getUserBalances([params.account]),
23
- viewsContract.read.getUserBorrowsBalances([params.account]),
24
- viewsContract.read.getUserMarketsMemberships([params.account]),
25
- ]);
26
-
27
- const balances =
28
- balancesResult.status === "fulfilled" ? balancesResult.value : [];
29
- const borrows =
30
- borrowsResult.status === "fulfilled" ? borrowsResult.value : [];
31
- const memberships =
32
- membershipsResult.status === "fulfilled" ? membershipsResult.value : [];
33
-
34
- // If getAllMarketsInfo failed (e.g. broken on-chain oracle), fall back to
35
- // per-mToken exchange rate calls. The user balance/borrow/membership calls
36
- // don't touch the oracle so they can still succeed.
37
- if (allMarketsResult.status === "rejected") {
38
- return getUserPositionsFromMTokenFallback(
39
- params,
40
- balances as { amount: bigint; token: `0x${string}` }[],
41
- borrows as { amount: bigint; token: `0x${string}` }[],
42
- memberships as { membership: boolean; token: `0x${string}` }[],
43
- );
44
- }
18
+ // Only getAllMarketsInfo is settled, because it has the mToken fallback
19
+ // below. The three user reads have no fallback: Promise.all lets their first
20
+ // rejection propagate instead of being treated as an empty list, which would
21
+ // misreport the user as having no position (MOO-885).
22
+ const [[allMarketsResult], balances, borrows, memberships] =
23
+ await Promise.all([
24
+ Promise.allSettled([viewsContract.read.getAllMarketsInfo()]),
25
+ viewsContract.read.getUserBalances([params.account]),
26
+ viewsContract.read.getUserBorrowsBalances([params.account]),
27
+ viewsContract.read.getUserMarketsMemberships([params.account]),
28
+ ]);
29
+
30
+ // If getAllMarketsInfo failed (e.g. broken on-chain oracle), fall back to
31
+ // per-mToken exchange rate calls. The user balance/borrow/membership calls
32
+ // don't touch the oracle so they can still succeed. The fallback reports
33
+ // every USD value as 0, so the degraded read is surfaced through onError
34
+ // instead of passing as a healthy result.
35
+ if (allMarketsResult.status === "rejected") {
36
+ params.environment.onError?.(allMarketsResult.reason, {
37
+ source: "user-positions-oracle-fallback",
38
+ chainId: params.environment.chainId,
39
+ });
40
+ return getUserPositionsFromMTokenFallback(
41
+ params,
42
+ balances,
43
+ borrows,
44
+ memberships,
45
+ );
46
+ }
45
47
 
46
- const allMarkets = allMarketsResult.value;
48
+ const allMarkets = allMarketsResult.value;
49
+
50
+ const markets = allMarkets
51
+ ?.map((marketInfo) => {
52
+ const market = findMarketByAddress(params.environment, marketInfo.market);
53
+ if (market) {
54
+ const { marketToken, underlyingToken } = market;
55
+
56
+ const underlyingPrice = new Amount(
57
+ marketInfo.underlyingPrice,
58
+ 36 - underlyingToken.decimals,
59
+ ).value;
60
+ const collateralFactor = new Amount(marketInfo.collateralFactor, 18)
61
+ .value;
62
+ const exchangeRate = new Amount(
63
+ marketInfo.exchangeRate,
64
+ 10 + underlyingToken.decimals,
65
+ ).value;
66
+
67
+ const marketCollateralEnabled =
68
+ memberships?.find((r) => r.token === marketInfo.market)
69
+ ?.membership === true;
70
+ const marketBorrowedRaw =
71
+ borrows?.find((r) => r.token === marketInfo.market)?.amount || 0n;
72
+ const marketSuppliedRaw =
73
+ balances?.find((r) => r.token === marketInfo.market)?.amount || 0n;
74
+
75
+ const borrowed = new Amount(
76
+ marketBorrowedRaw,
77
+ market.underlyingToken.decimals,
78
+ );
79
+ const borrowedUsd = borrowed.value * underlyingPrice;
47
80
 
48
- const markets = allMarkets
49
- ?.map((marketInfo) => {
50
- const market = findMarketByAddress(
51
- params.environment,
52
- marketInfo.market,
81
+ const marketSupplied = new Amount(
82
+ marketSuppliedRaw,
83
+ marketToken.decimals,
53
84
  );
54
- if (market) {
55
- const { marketToken, underlyingToken } = market;
56
-
57
- const underlyingPrice = new Amount(
58
- marketInfo.underlyingPrice,
59
- 36 - underlyingToken.decimals,
60
- ).value;
61
- const collateralFactor = new Amount(marketInfo.collateralFactor, 18)
62
- .value;
63
- const exchangeRate = new Amount(
64
- marketInfo.exchangeRate,
65
- 10 + underlyingToken.decimals,
66
- ).value;
67
-
68
- const marketCollateralEnabled =
69
- memberships?.find((r) => r.token === marketInfo.market)
70
- ?.membership === true;
71
- const marketBorrowedRaw =
72
- borrows?.find((r) => r.token === marketInfo.market)?.amount || 0n;
73
- const marketSuppliedRaw =
74
- balances?.find((r) => r.token === marketInfo.market)?.amount || 0n;
75
-
76
- const borrowed = new Amount(
77
- marketBorrowedRaw,
78
- market.underlyingToken.decimals,
79
- );
80
- const borrowedUsd = borrowed.value * underlyingPrice;
81
-
82
- const marketSupplied = new Amount(
83
- marketSuppliedRaw,
84
- marketToken.decimals,
85
- );
86
-
87
- const supplied = new Amount(
88
- marketSupplied.value * exchangeRate,
89
- underlyingToken.decimals,
90
- );
91
- const suppliedUsd = supplied.value * underlyingPrice;
92
-
93
- const collateral = marketCollateralEnabled
94
- ? new Amount(
95
- supplied.value * collateralFactor,
96
- underlyingToken.decimals,
97
- )
98
- : new Amount(0n, underlyingToken.decimals);
99
-
100
- const collateralUsd = collateral.value * underlyingPrice;
101
-
102
- const result: UserPosition = {
103
- chainId: params.environment.chainId,
104
- account: params.account,
105
- market: market.marketToken,
106
- collateralEnabled: marketCollateralEnabled,
107
- borrowed,
108
- borrowedUsd,
109
- collateral,
110
- collateralUsd,
111
- supplied,
112
- suppliedUsd,
113
- };
114
-
115
- return result;
116
- } else {
117
- return;
118
- }
119
- })
120
- .filter((r) => r !== undefined)
121
- .filter((r) =>
122
- params.markets ? params.markets.includes(r!.market.address) : true,
123
- ) as UserPosition[];
124
-
125
- return markets;
126
- } catch {
127
- return [];
128
- }
85
+
86
+ const supplied = new Amount(
87
+ marketSupplied.value * exchangeRate,
88
+ underlyingToken.decimals,
89
+ );
90
+ const suppliedUsd = supplied.value * underlyingPrice;
91
+
92
+ const collateral = marketCollateralEnabled
93
+ ? new Amount(
94
+ supplied.value * collateralFactor,
95
+ underlyingToken.decimals,
96
+ )
97
+ : new Amount(0n, underlyingToken.decimals);
98
+
99
+ const collateralUsd = collateral.value * underlyingPrice;
100
+
101
+ const result: UserPosition = {
102
+ chainId: params.environment.chainId,
103
+ account: params.account,
104
+ market: market.marketToken,
105
+ collateralEnabled: marketCollateralEnabled,
106
+ borrowed,
107
+ borrowedUsd,
108
+ collateral,
109
+ collateralUsd,
110
+ supplied,
111
+ suppliedUsd,
112
+ };
113
+
114
+ return result;
115
+ } else {
116
+ return;
117
+ }
118
+ })
119
+ .filter((r) => r !== undefined)
120
+ .filter((r) =>
121
+ params.markets ? params.markets.includes(r!.market.address) : true,
122
+ ) as UserPosition[];
123
+
124
+ return markets;
129
125
  };
130
126
 
131
127
  /**
@@ -141,9 +137,9 @@ async function getUserPositionsFromMTokenFallback(
141
137
  account: Address;
142
138
  markets?: string[] | undefined;
143
139
  },
144
- balances: { amount: bigint; token: `0x${string}` }[],
145
- borrows: { amount: bigint; token: `0x${string}` }[],
146
- memberships: { membership: boolean; token: `0x${string}` }[],
140
+ balances: readonly { amount: bigint; token: `0x${string}` }[],
141
+ borrows: readonly { amount: bigint; token: `0x${string}` }[],
142
+ memberships: readonly { membership: boolean; token: `0x${string}` }[],
147
143
  ): Promise<UserPosition[]> {
148
144
  const positions: UserPosition[] = [];
149
145
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1,6 +1,7 @@
1
1
  import type { Address, Chain } from "viem";
2
2
  import type { MoonwellClient } from "../../../client/createMoonwellClient.js";
3
3
  import { getEnvironmentsFromArgs } from "../../../common/index.js";
4
+ import { readAcrossEnvironments } from "../../../common/readAcrossEnvironments.js";
4
5
  import type { OptionalNetworkParameterType } from "../../../common/types.js";
5
6
  import type { UserPosition } from "../../../types/userPosition.js";
6
7
  import { getUserPositionData } from "./common.js";
@@ -15,6 +16,11 @@ export type GetUserPositionsParameters<
15
16
 
16
17
  export type GetUserPositionsReturnType = Promise<UserPosition[]>;
17
18
 
19
+ /**
20
+ * Rejects with a `ChainReadError` when the read fails on any requested chain,
21
+ * so a failed RPC never looks like an empty position list. The error lists the
22
+ * failed chains and carries the positions from the chains that succeeded.
23
+ */
18
24
  export async function getUserPositions<
19
25
  environments,
20
26
  Network extends Chain | undefined,
@@ -26,18 +32,13 @@ export async function getUserPositions<
26
32
 
27
33
  const environments = getEnvironmentsFromArgs(client, args);
28
34
 
29
- const settled = await Promise.allSettled(
30
- environments.map((environment) =>
35
+ return readAcrossEnvironments({
36
+ environments,
37
+ source: "getUserPositions",
38
+ read: (environment) =>
31
39
  getUserPositionData({
32
40
  environment,
33
41
  account: userAddress,
34
42
  }),
35
- ),
36
- );
37
-
38
- const result = settled.flatMap((s) =>
39
- s.status === "fulfilled" ? s.value : [],
40
- );
41
-
42
- return result;
43
+ });
43
44
  }
@@ -10,7 +10,7 @@ import type { NetworkParameterType } from "../../common/types.js";
10
10
  import type { Chain, Environment } from "../../environments/index.js";
11
11
  import type { StakingInfo } from "../../types/staking.js";
12
12
  import { getMerklStakingApr } from "./common.js";
13
- import { getGovernanceTokenPriceFor } from "./getWellPrice.js";
13
+ import { getGovernanceTokenPriceOrZero } from "./getWellPrice.js";
14
14
 
15
15
  export type GetStakingInfoParameters<
16
16
  environments,
@@ -150,7 +150,7 @@ export async function getStakingInfo<
150
150
  blockNumber: BigInt(34149943),
151
151
  })
152
152
  : Promise.resolve(undefined),
153
- getGovernanceTokenPriceFor(environment, baseEnvironment),
153
+ getGovernanceTokenPriceOrZero(environment, baseEnvironment),
154
154
  ]);
155
155
 
156
156
  const viewsStaking =
@@ -181,13 +181,6 @@ export async function getStakingInfo<
181
181
 
182
182
  const price = priceResult.status === "fulfilled" ? priceResult.value : 0n;
183
183
 
184
- if (priceResult.status === "rejected") {
185
- environment.onError?.(priceResult.reason, {
186
- source: "governance-token-price",
187
- chainId: environment.chainId,
188
- });
189
- }
190
-
191
184
  return { stakingInfo, historicalStaking, price };
192
185
  }),
193
186
  );