@moonwell-fi/moonwell-sdk 0.12.2 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/_cjs/actions/core/markets/common.js +11 -4
  3. package/_cjs/actions/core/markets/common.js.map +1 -1
  4. package/_cjs/actions/core/user-rewards/common.js +6 -5
  5. package/_cjs/actions/core/user-rewards/common.js.map +1 -1
  6. package/_cjs/actions/governance/getDelegates.js.map +1 -1
  7. package/_cjs/actions/governance/getStakingInfo.js +113 -31
  8. package/_cjs/actions/governance/getStakingInfo.js.map +1 -1
  9. package/_cjs/actions/governance/getUserStakingInfo.js +106 -18
  10. package/_cjs/actions/governance/getUserStakingInfo.js.map +1 -1
  11. package/_cjs/actions/governance/getUserVotingPowers.js +21 -6
  12. package/_cjs/actions/governance/getUserVotingPowers.js.map +1 -1
  13. package/_cjs/actions/governance/getWellPrice.js +26 -0
  14. package/_cjs/actions/governance/getWellPrice.js.map +1 -0
  15. package/_cjs/actions/morpho/user-rewards/common.js +87 -12
  16. package/_cjs/actions/morpho/user-rewards/common.js.map +1 -1
  17. package/_cjs/actions/morpho/user-rewards/getMorphoUserRewards.js +46 -10
  18. package/_cjs/actions/morpho/user-rewards/getMorphoUserRewards.js.map +1 -1
  19. package/_cjs/actions/morpho/vaults/common.js +19 -6
  20. package/_cjs/actions/morpho/vaults/common.js.map +1 -1
  21. package/_cjs/environments/definitions/ethereum/contracts.js +12 -0
  22. package/_cjs/environments/definitions/ethereum/contracts.js.map +1 -0
  23. package/_cjs/environments/definitions/ethereum/custom.js +18 -0
  24. package/_cjs/environments/definitions/ethereum/custom.js.map +1 -0
  25. package/_cjs/environments/definitions/ethereum/environment.js +9 -5
  26. package/_cjs/environments/definitions/ethereum/environment.js.map +1 -1
  27. package/_cjs/environments/definitions/ethereum/tokens.js +12 -0
  28. package/_cjs/environments/definitions/ethereum/tokens.js.map +1 -1
  29. package/_cjs/environments/definitions/governance.js +1 -1
  30. package/_cjs/environments/definitions/governance.js.map +1 -1
  31. package/_cjs/errors/version.js +1 -1
  32. package/_cjs/index.js +5 -1
  33. package/_cjs/index.js.map +1 -1
  34. package/_esm/actions/core/markets/common.js +11 -4
  35. package/_esm/actions/core/markets/common.js.map +1 -1
  36. package/_esm/actions/core/user-rewards/common.js +6 -5
  37. package/_esm/actions/core/user-rewards/common.js.map +1 -1
  38. package/_esm/actions/governance/getDelegates.js +2 -0
  39. package/_esm/actions/governance/getDelegates.js.map +1 -1
  40. package/_esm/actions/governance/getStakingInfo.js +136 -32
  41. package/_esm/actions/governance/getStakingInfo.js.map +1 -1
  42. package/_esm/actions/governance/getUserStakingInfo.js +120 -19
  43. package/_esm/actions/governance/getUserStakingInfo.js.map +1 -1
  44. package/_esm/actions/governance/getUserVotingPowers.js +24 -6
  45. package/_esm/actions/governance/getUserVotingPowers.js.map +1 -1
  46. package/_esm/actions/governance/getWellPrice.js +50 -0
  47. package/_esm/actions/governance/getWellPrice.js.map +1 -0
  48. package/_esm/actions/morpho/user-rewards/common.js +93 -12
  49. package/_esm/actions/morpho/user-rewards/common.js.map +1 -1
  50. package/_esm/actions/morpho/user-rewards/getMorphoUserRewards.js +51 -11
  51. package/_esm/actions/morpho/user-rewards/getMorphoUserRewards.js.map +1 -1
  52. package/_esm/actions/morpho/vaults/common.js +19 -6
  53. package/_esm/actions/morpho/vaults/common.js.map +1 -1
  54. package/_esm/environments/definitions/ethereum/contracts.js +10 -0
  55. package/_esm/environments/definitions/ethereum/contracts.js.map +1 -0
  56. package/_esm/environments/definitions/ethereum/custom.js +15 -0
  57. package/_esm/environments/definitions/ethereum/custom.js.map +1 -0
  58. package/_esm/environments/definitions/ethereum/environment.js +11 -5
  59. package/_esm/environments/definitions/ethereum/environment.js.map +1 -1
  60. package/_esm/environments/definitions/ethereum/tokens.js +12 -0
  61. package/_esm/environments/definitions/ethereum/tokens.js.map +1 -1
  62. package/_esm/environments/definitions/governance.js +2 -2
  63. package/_esm/environments/definitions/governance.js.map +1 -1
  64. package/_esm/errors/version.js +1 -1
  65. package/_esm/index.js +2 -0
  66. package/_esm/index.js.map +1 -1
  67. package/_types/actions/core/markets/common.d.ts.map +1 -1
  68. package/_types/actions/core/user-rewards/common.d.ts.map +1 -1
  69. package/_types/actions/governance/getDelegates.d.ts.map +1 -1
  70. package/_types/actions/governance/getStakingInfo.d.ts +1 -1
  71. package/_types/actions/governance/getStakingInfo.d.ts.map +1 -1
  72. package/_types/actions/governance/getUserStakingInfo.d.ts.map +1 -1
  73. package/_types/actions/governance/getUserVotingPowers.d.ts.map +1 -1
  74. package/_types/actions/governance/getWellPrice.d.ts +29 -0
  75. package/_types/actions/governance/getWellPrice.d.ts.map +1 -0
  76. package/_types/actions/morpho/user-rewards/common.d.ts +23 -0
  77. package/_types/actions/morpho/user-rewards/common.d.ts.map +1 -1
  78. package/_types/actions/morpho/user-rewards/getMorphoUserRewards.d.ts +22 -0
  79. package/_types/actions/morpho/user-rewards/getMorphoUserRewards.d.ts.map +1 -1
  80. package/_types/actions/morpho/vaults/common.d.ts.map +1 -1
  81. package/_types/client/createMoonwellClient.d.ts +62 -2
  82. package/_types/client/createMoonwellClient.d.ts.map +1 -1
  83. package/_types/environments/definitions/ethereum/contracts.d.ts +4 -0
  84. package/_types/environments/definitions/ethereum/contracts.d.ts.map +1 -0
  85. package/_types/environments/definitions/ethereum/custom.d.ts +18 -0
  86. package/_types/environments/definitions/ethereum/custom.d.ts.map +1 -0
  87. package/_types/environments/definitions/ethereum/environment.d.ts +52 -2
  88. package/_types/environments/definitions/ethereum/environment.d.ts.map +1 -1
  89. package/_types/environments/definitions/ethereum/tokens.d.ts +12 -0
  90. package/_types/environments/definitions/ethereum/tokens.d.ts.map +1 -1
  91. package/_types/environments/definitions/governance.d.ts.map +1 -1
  92. package/_types/environments/index.d.ts +34 -4
  93. package/_types/environments/index.d.ts.map +1 -1
  94. package/_types/errors/version.d.ts +1 -1
  95. package/_types/index.d.ts +2 -0
  96. package/_types/index.d.ts.map +1 -1
  97. package/actions/core/markets/common.ts +11 -6
  98. package/actions/core/user-rewards/common.ts +6 -5
  99. package/actions/governance/getDelegates.ts +2 -0
  100. package/actions/governance/getStakingInfo.ts +195 -87
  101. package/actions/governance/getUserStakingInfo.ts +168 -54
  102. package/actions/governance/getUserVotingPowers.ts +30 -15
  103. package/actions/governance/getWellPrice.ts +66 -0
  104. package/actions/morpho/user-rewards/common.ts +91 -14
  105. package/actions/morpho/user-rewards/getMorphoUserRewards.ts +77 -12
  106. package/actions/morpho/vaults/common.ts +19 -12
  107. package/environments/definitions/ethereum/contracts.ts +10 -0
  108. package/environments/definitions/ethereum/custom.ts +15 -0
  109. package/environments/definitions/ethereum/environment.ts +15 -6
  110. package/environments/definitions/ethereum/tokens.ts +12 -0
  111. package/environments/definitions/governance.ts +2 -2
  112. package/errors/version.ts +1 -1
  113. package/index.ts +3 -0
  114. package/package.json +1 -1
@@ -7,14 +7,10 @@ import {
7
7
  getEnvironmentsFromArgs,
8
8
  } from "../../common/index.js";
9
9
  import type { NetworkParameterType } from "../../common/types.js";
10
- import {
11
- type Chain,
12
- type Environment,
13
- type TokensType,
14
- publicEnvironments,
15
- } from "../../environments/index.js";
10
+ import type { Chain, Environment } from "../../environments/index.js";
16
11
  import type { StakingInfo } from "../../types/staking.js";
17
12
  import { getMerklStakingApr } from "./common.js";
13
+ import { getGovernanceTokenPriceFor } from "./getWellPrice.js";
18
14
 
19
15
  export type GetStakingInfoParameters<
20
16
  environments,
@@ -23,6 +19,108 @@ export type GetStakingInfoParameters<
23
19
 
24
20
  export type GetStakingInfoReturnType = Promise<StakingInfo[]>;
25
21
 
22
+ type StakingInfoStruct = {
23
+ cooldown: bigint;
24
+ distributionEnd: bigint;
25
+ emissionPerSecond: bigint;
26
+ totalSupply: bigint;
27
+ unstakeWindow: bigint;
28
+ };
29
+
30
+ const isStakingInfoStruct = (value: unknown): value is StakingInfoStruct => {
31
+ if (typeof value !== "object" || value === null) return false;
32
+ const v = value as Record<string, unknown>;
33
+ return (
34
+ typeof v.cooldown === "bigint" &&
35
+ typeof v.distributionEnd === "bigint" &&
36
+ typeof v.emissionPerSecond === "bigint" &&
37
+ typeof v.totalSupply === "bigint" &&
38
+ typeof v.unstakeWindow === "bigint"
39
+ );
40
+ };
41
+
42
+ /**
43
+ * Reads the staking fields directly from the stkWELL token contract when the
44
+ * core views' getStakingInfo() is unavailable or returns zeroed data
45
+ * (e.g. reverts on Moonbeam). Reads each field independently so a single
46
+ * transient RPC failure doesn't erase the whole fallback.
47
+ *
48
+ * The `assets` mapping is keyed by the stkWELL contract's own address — that's
49
+ * the Aave-fork convention (`address(this)` in StakedAave._initialize), not
50
+ * the underlying staked token. Verified on Moonbeam: assets(stkWELL) returns
51
+ * non-zero emissionPerSecond; assets(WELL) returns zero.
52
+ */
53
+ async function getStakingInfoFromStkWell(
54
+ environment: Environment,
55
+ ): Promise<StakingInfoStruct | undefined> {
56
+ const stakingToken = environment.contracts.stakingToken;
57
+ const stakingTokenKey = environment.config.contracts.stakingToken;
58
+ const tokens = environment.config.tokens as Record<
59
+ string,
60
+ { address: `0x${string}` } | undefined
61
+ >;
62
+ const stakingTokenAddress = stakingTokenKey
63
+ ? tokens[stakingTokenKey]?.address
64
+ : undefined;
65
+ if (!stakingToken || !stakingTokenAddress) {
66
+ environment.onError?.(
67
+ new Error("getStakingInfoFromStkWell: missing stkWELL config"),
68
+ { source: "staking-fallback", chainId: environment.chainId },
69
+ );
70
+ return undefined;
71
+ }
72
+
73
+ const [
74
+ cooldownR,
75
+ unstakeWindowR,
76
+ distributionEndR,
77
+ totalSupplyR,
78
+ assetDataR,
79
+ ] = await Promise.allSettled([
80
+ stakingToken.read.COOLDOWN_SECONDS(),
81
+ stakingToken.read.UNSTAKE_WINDOW(),
82
+ stakingToken.read.DISTRIBUTION_END(),
83
+ stakingToken.read.totalSupply(),
84
+ stakingToken.read.assets([stakingTokenAddress]),
85
+ ]);
86
+
87
+ // Surface every rejection so operators don't have to guess which read failed.
88
+ for (const r of [
89
+ cooldownR,
90
+ unstakeWindowR,
91
+ distributionEndR,
92
+ totalSupplyR,
93
+ assetDataR,
94
+ ]) {
95
+ if (r.status === "rejected") {
96
+ environment.onError?.(r.reason, {
97
+ source: "staking-fallback",
98
+ chainId: environment.chainId,
99
+ });
100
+ }
101
+ }
102
+
103
+ // totalSupply is load-bearing for APR; if its read failed (vs. legitimately
104
+ // returning 0n on an empty new chain) we can't produce a sensible struct.
105
+ if (totalSupplyR.status === "rejected") return undefined;
106
+
107
+ const assetData =
108
+ assetDataR.status === "fulfilled" ? assetDataR.value : undefined;
109
+ // viem returns multi-output reads as a tuple (Readonly<[bigint, bigint, bigint]>)
110
+ // even when the ABI names the outputs.
111
+ const emissionPerSecond = assetData ? assetData[0] : 0n;
112
+
113
+ return {
114
+ cooldown: cooldownR.status === "fulfilled" ? cooldownR.value : 0n,
115
+ unstakeWindow:
116
+ unstakeWindowR.status === "fulfilled" ? unstakeWindowR.value : 0n,
117
+ distributionEnd:
118
+ distributionEndR.status === "fulfilled" ? distributionEndR.value : 0n,
119
+ totalSupply: totalSupplyR.value,
120
+ emissionPerSecond,
121
+ };
122
+ }
123
+
26
124
  export async function getStakingInfo<
27
125
  environments,
28
126
  Network extends Chain | undefined,
@@ -36,85 +134,104 @@ export async function getStakingInfo<
36
134
  (env) => env.config.contracts.stakingToken,
37
135
  );
38
136
 
137
+ const baseEnvironment = (
138
+ client.environments as { base?: Environment } | undefined
139
+ )?.base;
140
+
39
141
  const envStakingInfoSettlements = await Promise.allSettled(
40
142
  envsWithStaking.map(async (environment) => {
41
- const homeEnvironment =
42
- (Object.values(publicEnvironments) as Environment[]).find((e) =>
43
- e.custom?.governance?.chainIds?.includes(environment.chainId),
44
- ) || environment;
45
-
46
143
  const isBase = environment.chainId === base.id;
47
144
 
48
- const settlements = await Promise.allSettled([
49
- environment.contracts.views?.read.getStakingInfo(),
50
- homeEnvironment.contracts.views?.read.getGovernanceTokenPrice(),
51
- ...(isBase
52
- ? [
53
- environment.contracts.views?.read.getStakingInfo({
145
+ const [viewsStakingResult, historicalStakingResult, priceResult] =
146
+ await Promise.allSettled([
147
+ environment.contracts.views?.read.getStakingInfo(),
148
+ isBase
149
+ ? environment.contracts.views?.read.getStakingInfo({
54
150
  blockNumber: BigInt(34149943),
55
- }),
56
- ]
57
- : []),
58
- ]);
59
-
60
- return settlements.map((s) =>
61
- s.status === "fulfilled" ? s.value : undefined,
62
- );
151
+ })
152
+ : Promise.resolve(undefined),
153
+ getGovernanceTokenPriceFor(environment, baseEnvironment),
154
+ ]);
155
+
156
+ const viewsStaking =
157
+ viewsStakingResult.status === "fulfilled"
158
+ ? viewsStakingResult.value
159
+ : undefined;
160
+
161
+ // Fall back to direct stkWELL reads when the views call rejected (the
162
+ // known Moonbeam failure mode) or returned a fully-zeroed struct
163
+ // (suspicious — a real deployment always has cooldown and unstake
164
+ // window configured > 0). A new chain with zero stakers but real
165
+ // schedule constants still goes through views.
166
+ const allZeroed =
167
+ isStakingInfoStruct(viewsStaking) &&
168
+ viewsStaking.cooldown === 0n &&
169
+ viewsStaking.unstakeWindow === 0n &&
170
+ viewsStaking.totalSupply === 0n &&
171
+ viewsStaking.emissionPerSecond === 0n;
172
+ const viewsValid = isStakingInfoStruct(viewsStaking) && !allZeroed;
173
+ const stakingInfo: StakingInfoStruct | undefined = viewsValid
174
+ ? viewsStaking
175
+ : await getStakingInfoFromStkWell(environment);
176
+
177
+ const historicalStaking =
178
+ historicalStakingResult.status === "fulfilled"
179
+ ? historicalStakingResult.value
180
+ : undefined;
181
+
182
+ const price = priceResult.status === "fulfilled" ? priceResult.value : 0n;
183
+
184
+ if (priceResult.status === "rejected") {
185
+ environment.onError?.(priceResult.reason, {
186
+ source: "governance-token-price",
187
+ chainId: environment.chainId,
188
+ });
189
+ }
190
+
191
+ return { stakingInfo, historicalStaking, price };
63
192
  }),
64
193
  );
65
194
 
66
- const envStakingInfo = envStakingInfoSettlements
67
- .filter((s) => s.status === "fulfilled")
68
- .map(
69
- (s) =>
70
- (
71
- s as PromiseFulfilledResult<
72
- (
73
- | bigint
74
- | {
75
- cooldown: bigint;
76
- unstakeWindow: bigint;
77
- distributionEnd: bigint;
78
- totalSupply: bigint;
79
- emissionPerSecond: bigint;
80
- lastUpdateTimestamp: bigint;
81
- index: bigint;
82
- }
83
- | undefined
84
- )[]
85
- >
86
- ).value,
87
- )
88
- .filter((val) => val !== undefined);
195
+ const envStakingInfo = envStakingInfoSettlements.map((s) =>
196
+ s.status === "fulfilled"
197
+ ? s.value
198
+ : { stakingInfo: undefined, historicalStaking: undefined, price: 0n },
199
+ );
89
200
 
90
201
  const baseEnv = envsWithStaking.find((env) => env.chainId === base.id);
91
- const stakingTokenKey = baseEnv?.config.contracts.stakingToken;
92
- const baseStkToken =
93
- baseEnv != null && stakingTokenKey != null
94
- ? baseEnv.config.tokens[stakingTokenKey]
95
- : undefined;
96
- const baseStakingApr =
97
- baseStkToken != null ? await getMerklStakingApr(baseStkToken.address) : 0;
202
+ const baseStakingTokenKey = baseEnv?.config.contracts.stakingToken;
203
+ const baseTokens = baseEnv?.config.tokens as
204
+ | Record<string, { address: `0x${string}` } | undefined>
205
+ | undefined;
206
+ const baseStkTokenAddress = baseStakingTokenKey
207
+ ? baseTokens?.[baseStakingTokenKey]?.address
208
+ : undefined;
209
+ const baseStakingApr = baseStkTokenAddress
210
+ ? await getMerklStakingApr(baseStkTokenAddress)
211
+ : 0;
98
212
 
99
213
  const result = envsWithStaking.flatMap((curr, index) => {
100
- const token =
101
- curr.config.tokens[
102
- curr.config.contracts.governanceToken as keyof TokensType<typeof curr>
103
- ]!;
104
- const stakingToken =
105
- curr.config.tokens[
106
- curr.config.contracts.stakingToken as keyof TokensType<typeof curr>
107
- ]!;
108
-
109
- const envStakingInfoData = envStakingInfo[index]![0]!;
110
- const envGovernanceTokenPriceData = envStakingInfo[index]![1];
111
- const envStakingInfoDataAfterX28Proposal = envStakingInfo[index]![2]!;
214
+ const govKey = curr.config.contracts.governanceToken;
215
+ const stkKey = curr.config.contracts.stakingToken;
216
+ const currTokens = curr.config.tokens as Record<
217
+ string,
218
+ { address: `0x${string}`; decimals: number; name: string; symbol: string }
219
+ >;
220
+ if (!govKey || !stkKey) return [];
221
+ const token = currTokens[govKey];
222
+ const stakingToken = currTokens[stkKey];
223
+ if (!token || !stakingToken) return [];
224
+
225
+ // envStakingInfo is built via Promise.allSettled with an explicit
226
+ // fallback object at the .map below, so entries are always defined here.
227
+ const {
228
+ stakingInfo: envStakingInfoData,
229
+ historicalStaking,
230
+ price,
231
+ } = envStakingInfo[index];
112
232
  const isBase = curr.chainId === base.id;
113
233
 
114
- if (
115
- !envStakingInfoData ||
116
- (isBase && !envStakingInfoDataAfterX28Proposal)
117
- ) {
234
+ if (!envStakingInfoData || (isBase && !historicalStaking)) {
118
235
  return [];
119
236
  }
120
237
 
@@ -124,20 +241,9 @@ export async function getStakingInfo<
124
241
  emissionPerSecond: emissionPerSecondRaw,
125
242
  totalSupply: totalSupplyRaw,
126
243
  unstakeWindow,
127
- } = envStakingInfoData as {
128
- cooldown: bigint;
129
- distributionEnd: bigint;
130
- emissionPerSecond: bigint;
131
- totalSupply: bigint;
132
- unstakeWindow: bigint;
133
- };
134
-
135
- //Quick workaround to get governance token price from some other environment
136
- const governanceTokenPrice = new Amount(
137
- (envGovernanceTokenPriceData ?? 0n) as bigint,
138
- 18,
139
- );
244
+ } = envStakingInfoData;
140
245
 
246
+ const tokenPrice = new Amount(price, 18);
141
247
  const totalSupply = new Amount(totalSupplyRaw, 18);
142
248
  const emissionPerSecond = new Amount(emissionPerSecondRaw, 18);
143
249
 
@@ -145,7 +251,9 @@ export async function getStakingInfo<
145
251
  emissionPerSecond.value * SECONDS_PER_DAY * DAYS_PER_YEAR;
146
252
 
147
253
  const apr =
148
- ((emissionPerYear + totalSupply.value) / totalSupply.value - 1) * 100;
254
+ totalSupply.value > 0
255
+ ? ((emissionPerYear + totalSupply.value) / totalSupply.value - 1) * 100
256
+ : 0;
149
257
 
150
258
  const stakingInfo: StakingInfo = {
151
259
  apr: isBase ? baseStakingApr : apr,
@@ -153,10 +261,10 @@ export async function getStakingInfo<
153
261
  cooldown: Number(cooldown),
154
262
  distributionEnd: Number(distributionEnd),
155
263
  token,
156
- tokenPrice: governanceTokenPrice.value,
264
+ tokenPrice: tokenPrice.value,
157
265
  stakingToken,
158
266
  totalSupply,
159
- totalSupplyUSD: totalSupply.value * governanceTokenPrice.value,
267
+ totalSupplyUSD: totalSupply.value * tokenPrice.value,
160
268
  unstakeWindow: Number(unstakeWindow),
161
269
  };
162
270
 
@@ -3,13 +3,10 @@ import { base } from "viem/chains";
3
3
  import type { MoonwellClient } from "../../client/createMoonwellClient.js";
4
4
  import { Amount, getEnvironmentsFromArgs } from "../../common/index.js";
5
5
  import type { OptionalNetworkParameterType } from "../../common/types.js";
6
- import {
7
- type Environment,
8
- type TokensType,
9
- publicEnvironments,
10
- } from "../../environments/index.js";
6
+ import type { Environment } from "../../environments/index.js";
11
7
  import type { UserStakingInfo } from "../../types/staking.js";
12
8
  import { getMerklCampaignIds, getMerklRewardsData } from "./common.js";
9
+ import { getGovernanceTokenPriceFor } from "./getWellPrice.js";
13
10
 
14
11
  export type GetUserStakingInfoParameters<
15
12
  environments,
@@ -21,6 +18,113 @@ export type GetUserStakingInfoParameters<
21
18
 
22
19
  export type GetUserStakingInfoReturnType = Promise<UserStakingInfo[]>;
23
20
 
21
+ type UserStakingFields = {
22
+ cooldown: bigint;
23
+ pendingRewards: bigint;
24
+ totalStaked: bigint;
25
+ };
26
+
27
+ type StakingScheduleFields = {
28
+ cooldown: bigint;
29
+ unstakeWindow: bigint;
30
+ };
31
+
32
+ /**
33
+ * Reads per-user staking fields directly from stkWELL when views.getUserStakingInfo()
34
+ * is unavailable (e.g. reverts on Moonbeam).
35
+ */
36
+ async function readUserStakingFromStkWell(
37
+ environment: Environment,
38
+ userAddress: Address,
39
+ ): Promise<UserStakingFields | undefined> {
40
+ const stakingToken = environment.contracts.stakingToken;
41
+ if (!stakingToken) return undefined;
42
+
43
+ const [cooldownR, rewardsR, balanceR] = await Promise.allSettled([
44
+ stakingToken.read.stakersCooldowns([userAddress]),
45
+ stakingToken.read.getTotalRewardsBalance([userAddress]),
46
+ stakingToken.read.balanceOf([userAddress]),
47
+ ]);
48
+
49
+ // Surface every rejection so a single failed read (e.g. balanceOf throttled)
50
+ // doesn't silently zero a field that the UI would then display as "no stake".
51
+ for (const r of [cooldownR, rewardsR, balanceR]) {
52
+ if (r.status === "rejected") {
53
+ environment.onError?.(r.reason, {
54
+ source: "user-staking-fallback",
55
+ chainId: environment.chainId,
56
+ });
57
+ }
58
+ }
59
+
60
+ if (
61
+ cooldownR.status === "rejected" &&
62
+ rewardsR.status === "rejected" &&
63
+ balanceR.status === "rejected"
64
+ ) {
65
+ return undefined;
66
+ }
67
+
68
+ return {
69
+ cooldown: cooldownR.status === "fulfilled" ? cooldownR.value : 0n,
70
+ pendingRewards: rewardsR.status === "fulfilled" ? rewardsR.value : 0n,
71
+ totalStaked: balanceR.status === "fulfilled" ? balanceR.value : 0n,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Reads global cooldown/unstakeWindow constants from stkWELL when
77
+ * views.getStakingInfo() is unavailable.
78
+ */
79
+ async function readScheduleFromStkWell(
80
+ environment: Environment,
81
+ ): Promise<StakingScheduleFields | undefined> {
82
+ const stakingToken = environment.contracts.stakingToken;
83
+ if (!stakingToken) return undefined;
84
+
85
+ const [cooldownR, unstakeWindowR] = await Promise.allSettled([
86
+ stakingToken.read.COOLDOWN_SECONDS(),
87
+ stakingToken.read.UNSTAKE_WINDOW(),
88
+ ]);
89
+
90
+ // Surface every rejection. A silent cooldown=0n would corrupt downstream
91
+ // cooldownEnding math even if the unstakeWindow read succeeded.
92
+ for (const r of [cooldownR, unstakeWindowR]) {
93
+ if (r.status === "rejected") {
94
+ environment.onError?.(r.reason, {
95
+ source: "user-staking-schedule-fallback",
96
+ chainId: environment.chainId,
97
+ });
98
+ }
99
+ }
100
+
101
+ if (cooldownR.status === "rejected" && unstakeWindowR.status === "rejected") {
102
+ return undefined;
103
+ }
104
+
105
+ return {
106
+ cooldown: cooldownR.status === "fulfilled" ? cooldownR.value : 0n,
107
+ unstakeWindow:
108
+ unstakeWindowR.status === "fulfilled" ? unstakeWindowR.value : 0n,
109
+ };
110
+ }
111
+
112
+ const isUserStakingShape = (value: unknown): value is UserStakingFields => {
113
+ if (typeof value !== "object" || value === null) return false;
114
+ const v = value as Record<string, unknown>;
115
+ return (
116
+ typeof v.cooldown === "bigint" &&
117
+ typeof v.pendingRewards === "bigint" &&
118
+ typeof v.totalStaked === "bigint"
119
+ );
120
+ };
121
+
122
+ const isScheduleShape = (value: unknown): value is StakingScheduleFields => {
123
+ if (typeof value !== "object" || value === null) return false;
124
+ const v = value as Record<string, unknown>;
125
+ return typeof v.cooldown === "bigint" && typeof v.unstakeWindow === "bigint";
126
+ };
127
+
24
128
  export async function getUserStakingInfo<
25
129
  environments,
26
130
  Network extends Chain | undefined,
@@ -35,23 +139,50 @@ export async function getUserStakingInfo<
35
139
  const envsWithStaking = environments.filter(
36
140
  (env) => env.contracts.stakingToken,
37
141
  );
142
+
143
+ const baseEnvironment = (
144
+ client.environments as { base?: Environment } | undefined
145
+ )?.base;
146
+
38
147
  const envStakingInfo = await Promise.all(
39
148
  envsWithStaking.map(async (environment) => {
40
- const homeEnvironment =
41
- (Object.values(publicEnvironments) as Environment[]).find((e) =>
42
- e.custom?.governance?.chainIds?.includes(environment.chainId),
43
- ) || environment;
44
-
45
149
  const settled = await Promise.allSettled([
46
150
  environment.contracts.views?.read.getUserStakingInfo([userAddress]),
47
151
  environment.contracts.governanceToken?.read.balanceOf([userAddress]),
48
- homeEnvironment.contracts.views?.read.getGovernanceTokenPrice(),
49
152
  environment.contracts.views?.read.getStakingInfo(),
153
+ getGovernanceTokenPriceFor(environment, baseEnvironment),
50
154
  ]);
51
155
 
52
- return settled.map((s) =>
53
- s.status === "fulfilled" ? s.value : undefined,
54
- );
156
+ const [userStakingR, balanceR, stakingScheduleR, priceR] = settled;
157
+
158
+ const viewsUserStaking =
159
+ userStakingR.status === "fulfilled" ? userStakingR.value : undefined;
160
+ const userStaking = isUserStakingShape(viewsUserStaking)
161
+ ? viewsUserStaking
162
+ : await readUserStakingFromStkWell(environment, userAddress);
163
+
164
+ const viewsSchedule =
165
+ stakingScheduleR.status === "fulfilled"
166
+ ? stakingScheduleR.value
167
+ : undefined;
168
+ const schedule = isScheduleShape(viewsSchedule)
169
+ ? viewsSchedule
170
+ : await readScheduleFromStkWell(environment);
171
+
172
+ const tokenBalance =
173
+ balanceR.status === "fulfilled" && balanceR.value !== undefined
174
+ ? balanceR.value
175
+ : 0n;
176
+
177
+ const price = priceR.status === "fulfilled" ? priceR.value : 0n;
178
+ if (priceR.status === "rejected") {
179
+ environment.onError?.(priceR.reason, {
180
+ source: "governance-token-price",
181
+ chainId: environment.chainId,
182
+ });
183
+ }
184
+
185
+ return { userStaking, schedule, tokenBalance, price };
55
186
  }),
56
187
  );
57
188
 
@@ -63,28 +194,27 @@ export async function getUserStakingInfo<
63
194
  );
64
195
 
65
196
  const result = envsWithStaking.flatMap((curr, index) => {
66
- const token =
67
- curr.config.tokens[
68
- curr.config.contracts.governanceToken as keyof TokensType<typeof curr>
69
- ]!;
70
- const stakingToken =
71
- curr.config.tokens[
72
- curr.config.contracts.stakingToken as keyof TokensType<typeof curr>
73
- ]!;
74
-
75
- const userStakingInfoData = envStakingInfo[index]?.[0] as
76
- | {
77
- cooldown: bigint;
78
- pendingRewards: bigint;
79
- totalStaked: bigint;
80
- }
81
- | undefined;
82
-
83
- if (!userStakingInfoData) return [];
84
-
85
- const { cooldown, pendingRewards, totalStaked } = userStakingInfoData;
86
-
87
- // merkl rewards (only for base)
197
+ const govKey = curr.config.contracts.governanceToken;
198
+ const stkKey = curr.config.contracts.stakingToken;
199
+ const currTokens = curr.config.tokens as Record<
200
+ string,
201
+ { address: `0x${string}`; decimals: number; name: string; symbol: string }
202
+ >;
203
+ if (!govKey || !stkKey) return [];
204
+ const token = currTokens[govKey];
205
+ const stakingToken = currTokens[stkKey];
206
+ if (!token || !stakingToken) return [];
207
+
208
+ // envStakingInfo is built via Promise.all over the same envsWithStaking
209
+ // array, so index access is always defined.
210
+ const { userStaking, schedule, tokenBalance, price } =
211
+ envStakingInfo[index];
212
+
213
+ if (!userStaking || !schedule) return [];
214
+
215
+ const { cooldown, pendingRewards, totalStaked } = userStaking;
216
+ const { cooldown: cooldownSeconds, unstakeWindow } = schedule;
217
+
88
218
  const isBase = curr.chainId === base.id;
89
219
  const merklReward = merklRewards.reduce((acc, r) => {
90
220
  if (r.chain === curr.chainId) {
@@ -94,27 +224,11 @@ export async function getUserStakingInfo<
94
224
  }, 0n);
95
225
  const merklPendingRewards = isBase ? merklReward : 0n;
96
226
 
97
- const tokenBalance = (envStakingInfo[index]?.[1] ?? 0n) as bigint;
98
-
99
- const governanceTokenPriceRaw = (envStakingInfo[index]?.[2] ??
100
- 0n) as bigint;
101
-
102
- const stakingInfoData = envStakingInfo[index]?.[3] as
103
- | {
104
- cooldown: bigint;
105
- unstakeWindow: bigint;
106
- }
107
- | undefined;
108
-
109
- if (!stakingInfoData) return [];
110
-
111
- const { cooldown: cooldownSeconds, unstakeWindow } = stakingInfoData;
112
-
113
227
  const cooldownEnding = cooldown > 0n ? cooldown + cooldownSeconds : 0n;
114
228
  const unstakingEnding =
115
229
  cooldown > 0n ? cooldown + cooldownSeconds + unstakeWindow : 0n;
116
230
 
117
- const governanceTokenPrice = new Amount(governanceTokenPriceRaw, 18);
231
+ const tokenPrice = new Amount(price, 18);
118
232
 
119
233
  const userStakingInfo: UserStakingInfo = {
120
234
  chainId: curr.chainId,
@@ -128,7 +242,7 @@ export async function getUserStakingInfo<
128
242
  : new Amount(pendingRewards, 18),
129
243
  token,
130
244
  tokenBalance: new Amount(tokenBalance, 18),
131
- tokenPrice: governanceTokenPrice.value,
245
+ tokenPrice: tokenPrice.value,
132
246
  stakingToken,
133
247
  stakingTokenBalance: new Amount(totalStaked, 18),
134
248
  };
@@ -9,6 +9,8 @@ import type { OptionalNetworkParameterType } from "../../common/types.js";
9
9
  import type { Chain, GovernanceToken } from "../../environments/index.js";
10
10
  import type { UserVotingPowers } from "../../types/userVotingPowers.js";
11
11
 
12
+ const warnedNoViewsEnvs = new Set<string>();
13
+
12
14
  export type GetUserVotingPowersParameters<
13
15
  environments,
14
16
  network extends Chain | undefined,
@@ -49,14 +51,31 @@ export async function getUserVotingPowers<
49
51
 
50
52
  const environments = getEnvironmentsFromArgs(client, args);
51
53
 
52
- const tokenEnvironments = environments.filter(
53
- (env) => env.custom?.governance?.token === governanceToken,
54
- );
54
+ // A chain can hold a governance token without deploying a views contract
55
+ // (voting reads run on the hub). Skipping the no-views case here lets the
56
+ // read site below call views.read.getUserVotingPower directly.
57
+ const tokenEnvironments = environments.flatMap((env) => {
58
+ if (env.custom?.governance?.token !== governanceToken) {
59
+ return [];
60
+ }
61
+ const views = env.contracts.views;
62
+ if (views === undefined) {
63
+ const key = `${env.chainId}:${governanceToken}`;
64
+ if (!warnedNoViewsEnvs.has(key)) {
65
+ warnedNoViewsEnvs.add(key);
66
+ console.warn(
67
+ `[moonwell-sdk] getUserVotingPowers: skipping chainId=${env.chainId} for governanceToken=${governanceToken} — environment holds the token but has no views contract.`,
68
+ );
69
+ }
70
+ return [];
71
+ }
72
+ return [{ env, views }];
73
+ });
55
74
 
56
75
  const perChainBlockNumbers =
57
76
  snapshotTimestamp !== undefined
58
77
  ? await Promise.all(
59
- tokenEnvironments.map((env) =>
78
+ tokenEnvironments.map(({ env }) =>
60
79
  getBlockNumberAtTimestamp(
61
80
  env.publicClient,
62
81
  BigInt(snapshotTimestamp),
@@ -65,23 +84,19 @@ export async function getUserVotingPowers<
65
84
  )
66
85
  : undefined;
67
86
 
68
- const environmentsUserVotingPowers = await Promise.all(
69
- tokenEnvironments.map((environment, index) => {
87
+ const resolvedVotingPowers = await Promise.all(
88
+ tokenEnvironments.map(async ({ env, views }, index) => {
70
89
  const blockForChain = perChainBlockNumbers
71
90
  ? perChainBlockNumbers[index]
72
91
  : blockNumber;
73
- return environment.contracts.views?.read.getUserVotingPower(
74
- [userAddress],
75
- {
76
- blockNumber: blockForChain,
77
- },
78
- );
92
+ const votingPowers = await views.read.getUserVotingPower([userAddress], {
93
+ blockNumber: blockForChain,
94
+ });
95
+ return { env, votingPowers };
79
96
  }),
80
97
  );
81
98
 
82
- return tokenEnvironments.map((environment, index) => {
83
- const votingPowers = environmentsUserVotingPowers[index]!;
84
-
99
+ return resolvedVotingPowers.map(({ env: environment, votingPowers }) => {
85
100
  return {
86
101
  chainId: environment.chainId,
87
102