@moonwell-fi/moonwell-sdk 0.13.0 → 0.14.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 (85) hide show
  1. package/CHANGELOG.md +20 -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/getUserVoteReceipt.js +39 -25
  11. package/_cjs/actions/governance/getUserVoteReceipt.js.map +1 -1
  12. package/_cjs/actions/governance/getWellPrice.js +26 -0
  13. package/_cjs/actions/governance/getWellPrice.js.map +1 -0
  14. package/_cjs/actions/governance/governor-api-client.js +70 -33
  15. package/_cjs/actions/governance/governor-api-client.js.map +1 -1
  16. package/_cjs/actions/governance/proposals/common.js +29 -1
  17. package/_cjs/actions/governance/proposals/common.js.map +1 -1
  18. package/_cjs/actions/governance/proposals/getProposal.js +24 -17
  19. package/_cjs/actions/governance/proposals/getProposal.js.map +1 -1
  20. package/_cjs/actions/governance/proposals/getProposals.js +27 -9
  21. package/_cjs/actions/governance/proposals/getProposals.js.map +1 -1
  22. package/_cjs/actions/morpho/user-rewards/common.js +10 -3
  23. package/_cjs/actions/morpho/user-rewards/common.js.map +1 -1
  24. package/_cjs/actions/morpho/vaults/common.js +19 -6
  25. package/_cjs/actions/morpho/vaults/common.js.map +1 -1
  26. package/_cjs/errors/version.js +1 -1
  27. package/_esm/actions/core/markets/common.js +11 -4
  28. package/_esm/actions/core/markets/common.js.map +1 -1
  29. package/_esm/actions/core/user-rewards/common.js +6 -5
  30. package/_esm/actions/core/user-rewards/common.js.map +1 -1
  31. package/_esm/actions/governance/getStakingInfo.js +136 -32
  32. package/_esm/actions/governance/getStakingInfo.js.map +1 -1
  33. package/_esm/actions/governance/getUserStakingInfo.js +120 -19
  34. package/_esm/actions/governance/getUserStakingInfo.js.map +1 -1
  35. package/_esm/actions/governance/getUserVoteReceipt.js +48 -26
  36. package/_esm/actions/governance/getUserVoteReceipt.js.map +1 -1
  37. package/_esm/actions/governance/getWellPrice.js +50 -0
  38. package/_esm/actions/governance/getWellPrice.js.map +1 -0
  39. package/_esm/actions/governance/governor-api-client.js +87 -35
  40. package/_esm/actions/governance/governor-api-client.js.map +1 -1
  41. package/_esm/actions/governance/proposals/common.js +44 -1
  42. package/_esm/actions/governance/proposals/common.js.map +1 -1
  43. package/_esm/actions/governance/proposals/getProposal.js +36 -23
  44. package/_esm/actions/governance/proposals/getProposal.js.map +1 -1
  45. package/_esm/actions/governance/proposals/getProposals.js +44 -10
  46. package/_esm/actions/governance/proposals/getProposals.js.map +1 -1
  47. package/_esm/actions/morpho/user-rewards/common.js +10 -3
  48. package/_esm/actions/morpho/user-rewards/common.js.map +1 -1
  49. package/_esm/actions/morpho/vaults/common.js +19 -6
  50. package/_esm/actions/morpho/vaults/common.js.map +1 -1
  51. package/_esm/errors/version.js +1 -1
  52. package/_types/actions/core/markets/common.d.ts.map +1 -1
  53. package/_types/actions/core/user-rewards/common.d.ts.map +1 -1
  54. package/_types/actions/governance/getStakingInfo.d.ts +1 -1
  55. package/_types/actions/governance/getStakingInfo.d.ts.map +1 -1
  56. package/_types/actions/governance/getUserStakingInfo.d.ts.map +1 -1
  57. package/_types/actions/governance/getUserVoteReceipt.d.ts +16 -0
  58. package/_types/actions/governance/getUserVoteReceipt.d.ts.map +1 -1
  59. package/_types/actions/governance/getWellPrice.d.ts +29 -0
  60. package/_types/actions/governance/getWellPrice.d.ts.map +1 -0
  61. package/_types/actions/governance/governor-api-client.d.ts +37 -12
  62. package/_types/actions/governance/governor-api-client.d.ts.map +1 -1
  63. package/_types/actions/governance/proposals/common.d.ts +14 -1
  64. package/_types/actions/governance/proposals/common.d.ts.map +1 -1
  65. package/_types/actions/governance/proposals/getProposal.d.ts +6 -1
  66. package/_types/actions/governance/proposals/getProposal.d.ts.map +1 -1
  67. package/_types/actions/governance/proposals/getProposals.d.ts +1 -1
  68. package/_types/actions/governance/proposals/getProposals.d.ts.map +1 -1
  69. package/_types/actions/morpho/user-rewards/common.d.ts.map +1 -1
  70. package/_types/actions/morpho/vaults/common.d.ts.map +1 -1
  71. package/_types/errors/version.d.ts +1 -1
  72. package/actions/core/markets/common.ts +11 -6
  73. package/actions/core/user-rewards/common.ts +6 -5
  74. package/actions/governance/getStakingInfo.ts +195 -87
  75. package/actions/governance/getUserStakingInfo.ts +168 -54
  76. package/actions/governance/getUserVoteReceipt.ts +71 -31
  77. package/actions/governance/getWellPrice.ts +66 -0
  78. package/actions/governance/governor-api-client.ts +136 -62
  79. package/actions/governance/proposals/common.ts +51 -1
  80. package/actions/governance/proposals/getProposal.ts +46 -26
  81. package/actions/governance/proposals/getProposals.ts +48 -14
  82. package/actions/morpho/user-rewards/common.ts +10 -3
  83. package/actions/morpho/vaults/common.ts +19 -12
  84. package/errors/version.ts +1 -1
  85. package/package.json +1 -1
@@ -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
  };
@@ -3,7 +3,13 @@ import type { MoonwellClient } from "../../client/createMoonwellClient.js";
3
3
  import { Amount, getEnvironmentFromArgs } from "../../common/index.js";
4
4
  import type { NetworkParameterType } from "../../common/types.js";
5
5
  import type { VoteReceipt } from "../../types/voteReceipt.js";
6
- import { fetchUserVoteReceipt } from "./governor-api-client.js";
6
+ import {
7
+ type ApiVoteReceipt,
8
+ GovernorNotFoundError,
9
+ SUPPORTED_GOVERNOR_CHAIN_IDS,
10
+ fetchUserVoteReceipt,
11
+ isNotFoundError,
12
+ } from "./governor-api-client.js";
7
13
 
8
14
  export type GetUserVoteReceiptParameters<
9
15
  environments,
@@ -14,10 +20,27 @@ export type GetUserVoteReceiptParameters<
14
20
 
15
21
  /** User address*/
16
22
  userAddress: Address;
23
+
24
+ /**
25
+ * The chain the proposal lives on (1 = Ethereum multigov,
26
+ * 1284 = Moonbeam historical). When omitted, every supported chain is queried
27
+ * and non-empty receipts are concatenated — proposalIds may collide across
28
+ * chains (they represent different proposals), so a single bare proposalId
29
+ * can have votes on both Ethereum and Moonbeam.
30
+ */
31
+ chainId?: number;
17
32
  };
18
33
 
19
34
  export type GetUserVoteReceiptReturnType = Promise<VoteReceipt[]>;
20
35
 
36
+ /**
37
+ * Fetch a user's vote receipts for a proposal.
38
+ *
39
+ * Returns the "didn't vote" stub when the proposal exists on at least one
40
+ * queried chain but the user hasn't voted there. Throws `GovernorNotFoundError`
41
+ * when the proposal doesn't exist on any queried chain — callers can use that
42
+ * to distinguish "the user didn't vote" from "this proposal isn't visible".
43
+ */
21
44
  export async function getUserVoteReceipt<
22
45
  environments,
23
46
  Network extends Chain | undefined,
@@ -25,44 +48,61 @@ export async function getUserVoteReceipt<
25
48
  client: MoonwellClient,
26
49
  args: GetUserVoteReceiptParameters<environments, Network>,
27
50
  ): GetUserVoteReceiptReturnType {
28
- const { proposalId, userAddress } = args;
51
+ const { proposalId, userAddress, chainId } = args;
29
52
 
30
53
  const environment = getEnvironmentFromArgs(client, args);
31
-
32
54
  if (!environment) {
33
55
  return [];
34
56
  }
35
57
 
36
- try {
37
- const apiVoteReceipts = await fetchUserVoteReceipt(
38
- environment,
39
- `${proposalId}`,
40
- userAddress,
41
- );
58
+ const tryChains = chainId ? [chainId] : SUPPORTED_GOVERNOR_CHAIN_IDS;
42
59
 
43
- if (apiVoteReceipts.length === 0) {
44
- return [
45
- {
46
- chainId: environment.chainId,
47
- proposalId,
48
- account: userAddress,
49
- voted: false,
50
- option: 0,
51
- votes: new Amount(0, 18),
52
- },
53
- ];
60
+ const collected: ApiVoteReceipt[] = [];
61
+ let anyChainAcknowledged = false;
62
+ const notFoundChainIds: number[] = [];
63
+
64
+ for (const cid of tryChains) {
65
+ try {
66
+ const apiVoteReceipts = await fetchUserVoteReceipt(
67
+ environment,
68
+ cid,
69
+ proposalId,
70
+ userAddress,
71
+ );
72
+ anyChainAcknowledged = true;
73
+ collected.push(...apiVoteReceipts);
74
+ } catch (error) {
75
+ if (isNotFoundError(error)) {
76
+ notFoundChainIds.push(cid);
77
+ continue;
78
+ }
79
+ throw error;
54
80
  }
81
+ }
55
82
 
56
- return apiVoteReceipts.map((apiReceipt) => ({
57
- chainId: apiReceipt.chainId,
58
- proposalId,
59
- account: userAddress as Address,
60
- voted: true,
61
- option: apiReceipt.voteValue,
62
- votes: new Amount(BigInt(apiReceipt.votes), 18),
63
- }));
64
- } catch (error) {
65
- const message = error instanceof Error ? error.message : String(error);
66
- throw new Error(`Failed to fetch user vote receipt: ${message}`);
83
+ if (!anyChainAcknowledged) {
84
+ throw new GovernorNotFoundError(notFoundChainIds[0] ?? 0, proposalId);
67
85
  }
86
+
87
+ if (collected.length === 0) {
88
+ return [
89
+ {
90
+ chainId: environment.chainId,
91
+ proposalId,
92
+ account: userAddress,
93
+ voted: false,
94
+ option: 0,
95
+ votes: new Amount(0, 18),
96
+ },
97
+ ];
98
+ }
99
+
100
+ return collected.map((apiReceipt) => ({
101
+ chainId: apiReceipt.chainId,
102
+ proposalId,
103
+ account: userAddress,
104
+ voted: true,
105
+ option: apiReceipt.voteValue,
106
+ votes: new Amount(BigInt(apiReceipt.votes), 18),
107
+ }));
68
108
  }
@@ -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
+ }