@moonwell-fi/moonwell-sdk 0.13.0 → 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 (50) hide show
  1. package/CHANGELOG.md +10 -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/getStakingInfo.js +113 -31
  7. package/_cjs/actions/governance/getStakingInfo.js.map +1 -1
  8. package/_cjs/actions/governance/getUserStakingInfo.js +106 -18
  9. package/_cjs/actions/governance/getUserStakingInfo.js.map +1 -1
  10. package/_cjs/actions/governance/getWellPrice.js +26 -0
  11. package/_cjs/actions/governance/getWellPrice.js.map +1 -0
  12. package/_cjs/actions/morpho/user-rewards/common.js +10 -3
  13. package/_cjs/actions/morpho/user-rewards/common.js.map +1 -1
  14. package/_cjs/actions/morpho/vaults/common.js +19 -6
  15. package/_cjs/actions/morpho/vaults/common.js.map +1 -1
  16. package/_cjs/errors/version.js +1 -1
  17. package/_esm/actions/core/markets/common.js +11 -4
  18. package/_esm/actions/core/markets/common.js.map +1 -1
  19. package/_esm/actions/core/user-rewards/common.js +6 -5
  20. package/_esm/actions/core/user-rewards/common.js.map +1 -1
  21. package/_esm/actions/governance/getStakingInfo.js +136 -32
  22. package/_esm/actions/governance/getStakingInfo.js.map +1 -1
  23. package/_esm/actions/governance/getUserStakingInfo.js +120 -19
  24. package/_esm/actions/governance/getUserStakingInfo.js.map +1 -1
  25. package/_esm/actions/governance/getWellPrice.js +50 -0
  26. package/_esm/actions/governance/getWellPrice.js.map +1 -0
  27. package/_esm/actions/morpho/user-rewards/common.js +10 -3
  28. package/_esm/actions/morpho/user-rewards/common.js.map +1 -1
  29. package/_esm/actions/morpho/vaults/common.js +19 -6
  30. package/_esm/actions/morpho/vaults/common.js.map +1 -1
  31. package/_esm/errors/version.js +1 -1
  32. package/_types/actions/core/markets/common.d.ts.map +1 -1
  33. package/_types/actions/core/user-rewards/common.d.ts.map +1 -1
  34. package/_types/actions/governance/getStakingInfo.d.ts +1 -1
  35. package/_types/actions/governance/getStakingInfo.d.ts.map +1 -1
  36. package/_types/actions/governance/getUserStakingInfo.d.ts.map +1 -1
  37. package/_types/actions/governance/getWellPrice.d.ts +29 -0
  38. package/_types/actions/governance/getWellPrice.d.ts.map +1 -0
  39. package/_types/actions/morpho/user-rewards/common.d.ts.map +1 -1
  40. package/_types/actions/morpho/vaults/common.d.ts.map +1 -1
  41. package/_types/errors/version.d.ts +1 -1
  42. package/actions/core/markets/common.ts +11 -6
  43. package/actions/core/user-rewards/common.ts +6 -5
  44. package/actions/governance/getStakingInfo.ts +195 -87
  45. package/actions/governance/getUserStakingInfo.ts +168 -54
  46. package/actions/governance/getWellPrice.ts +66 -0
  47. package/actions/morpho/user-rewards/common.ts +10 -3
  48. package/actions/morpho/vaults/common.ts +19 -12
  49. package/errors/version.ts +1 -1
  50. 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
  };
@@ -0,0 +1,66 @@
1
+ import {
2
+ type Environment,
3
+ publicEnvironments,
4
+ } from "../../environments/index.js";
5
+
6
+ /**
7
+ * Reads WELL/USD from Base's lending oracle via getUnderlyingPrice(mWELL).
8
+ *
9
+ * The Base oracle is Chainlink-fed and shared by the lending markets, so it's
10
+ * the authoritative WELL/USD source. Used in place of the per-chain
11
+ * views.getGovernanceTokenPrice() which is unreliable on Moonbeam (returns
12
+ * stale data) and Base (returns 0).
13
+ *
14
+ * Returns a uint256 already scaled to 18 decimals.
15
+ *
16
+ * @param baseEnvironment Pass the caller's Base environment when available so
17
+ * user-configured RPCs / onError handlers are honored. Falls back to the
18
+ * SDK's default public Base environment otherwise.
19
+ */
20
+ export async function getWellPriceFromBaseOracle(
21
+ baseEnvironment?: Environment,
22
+ ): Promise<bigint> {
23
+ const baseEnv = baseEnvironment ?? publicEnvironments.base;
24
+ const tokens = baseEnv.config.tokens as Record<
25
+ string,
26
+ { address: `0x${string}` } | undefined
27
+ >;
28
+ const mWELL = tokens.MOONWELL_WELL?.address;
29
+ const oracle = baseEnv.contracts.oracle;
30
+ if (!mWELL || !oracle) {
31
+ // A custom Base env without MOONWELL_WELL or an oracle would silently
32
+ // zero out every WELL-priced read across the SDK. Surface it instead.
33
+ baseEnv.onError?.(
34
+ new Error(
35
+ `getWellPriceFromBaseOracle: missing ${!mWELL ? "MOONWELL_WELL token" : "oracle contract"} on Base env`,
36
+ ),
37
+ { source: "well-price", chainId: baseEnv.chainId },
38
+ );
39
+ return 0n;
40
+ }
41
+ return await oracle.read.getUnderlyingPrice([mWELL]);
42
+ }
43
+
44
+ /**
45
+ * Returns the governance-token-in-USD price for an environment.
46
+ *
47
+ * - For WELL-governed chains (Base, Optimism, Moonbeam), reads from the Base
48
+ * lending oracle's mWELL underlying price (authoritative, Chainlink-fed).
49
+ * - For non-WELL chains (currently only Moonriver / MFAM), reads from the
50
+ * env's own views.getGovernanceTokenPrice() — Moonriver has its own MFAM
51
+ * oracle and isn't priced from Base.
52
+ *
53
+ * Returns 0n if the lookup fails.
54
+ */
55
+ export async function getGovernanceTokenPriceFor(
56
+ environment: Environment,
57
+ baseEnvironment?: Environment,
58
+ ): Promise<bigint> {
59
+ if (environment.custom?.governance?.token === "WELL") {
60
+ return getWellPriceFromBaseOracle(baseEnvironment);
61
+ }
62
+ // Non-WELL (e.g. Moonriver / MFAM): the env itself is the governance "home".
63
+ const views = environment.contracts.views;
64
+ if (!views) return 0n;
65
+ return (await views.read.getGovernanceTokenPrice()) ?? 0n;
66
+ }