@moonwell-fi/moonwell-sdk 0.21.0 → 0.22.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 (167) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +9 -3
  3. package/_cjs/actions/beam/common.js +1 -2
  4. package/_cjs/actions/beam/common.js.map +1 -1
  5. package/_cjs/actions/core/markets/common.js +146 -96
  6. package/_cjs/actions/core/markets/common.js.map +1 -1
  7. package/_cjs/actions/governance/getDelegates.js +2 -7
  8. package/_cjs/actions/governance/getDelegates.js.map +1 -1
  9. package/_cjs/actions/governance/getGovernanceTokenInfo.js +7 -12
  10. package/_cjs/actions/governance/getGovernanceTokenInfo.js.map +1 -1
  11. package/_cjs/actions/governance/getUserVoteReceipt.js +5 -4
  12. package/_cjs/actions/governance/getUserVoteReceipt.js.map +1 -1
  13. package/_cjs/actions/governance/governor-api-client.js.map +1 -1
  14. package/_cjs/actions/governance/proposals/common.js +73 -6
  15. package/_cjs/actions/governance/proposals/common.js.map +1 -1
  16. package/_cjs/actions/governance/proposals/getProposal.js +6 -73
  17. package/_cjs/actions/governance/proposals/getProposal.js.map +1 -1
  18. package/_cjs/actions/governance/proposals/getProposals.js +12 -92
  19. package/_cjs/actions/governance/proposals/getProposals.js.map +1 -1
  20. package/_cjs/client/createMoonwellClient.js +17 -15
  21. package/_cjs/client/createMoonwellClient.js.map +1 -1
  22. package/_cjs/common/index.js +4 -4
  23. package/_cjs/common/index.js.map +1 -1
  24. package/_cjs/environments/definitions/governance.js +1 -8
  25. package/_cjs/environments/definitions/governance.js.map +1 -1
  26. package/_cjs/environments/index.js +28 -42
  27. package/_cjs/environments/index.js.map +1 -1
  28. package/_cjs/environments/types/config.js.map +1 -1
  29. package/_cjs/errors/version.js +1 -1
  30. package/_esm/actions/beam/common.js +3 -2
  31. package/_esm/actions/beam/common.js.map +1 -1
  32. package/_esm/actions/core/markets/common.js +192 -104
  33. package/_esm/actions/core/markets/common.js.map +1 -1
  34. package/_esm/actions/governance/getDelegates.js +8 -8
  35. package/_esm/actions/governance/getDelegates.js.map +1 -1
  36. package/_esm/actions/governance/getGovernanceTokenInfo.js +20 -13
  37. package/_esm/actions/governance/getGovernanceTokenInfo.js.map +1 -1
  38. package/_esm/actions/governance/getUserVoteReceipt.js +16 -5
  39. package/_esm/actions/governance/getUserVoteReceipt.js.map +1 -1
  40. package/_esm/actions/governance/governor-api-client.js +14 -5
  41. package/_esm/actions/governance/governor-api-client.js.map +1 -1
  42. package/_esm/actions/governance/proposals/common.js +122 -10
  43. package/_esm/actions/governance/proposals/common.js.map +1 -1
  44. package/_esm/actions/governance/proposals/getProposal.js +16 -93
  45. package/_esm/actions/governance/proposals/getProposal.js.map +1 -1
  46. package/_esm/actions/governance/proposals/getProposals.js +37 -132
  47. package/_esm/actions/governance/proposals/getProposals.js.map +1 -1
  48. package/_esm/client/createMoonwellClient.js +24 -16
  49. package/_esm/client/createMoonwellClient.js.map +1 -1
  50. package/_esm/common/index.js +15 -4
  51. package/_esm/common/index.js.map +1 -1
  52. package/_esm/environments/definitions/governance.js +5 -9
  53. package/_esm/environments/definitions/governance.js.map +1 -1
  54. package/_esm/environments/index.js +2 -14
  55. package/_esm/environments/index.js.map +1 -1
  56. package/_esm/environments/types/config.js.map +1 -1
  57. package/_esm/errors/version.js +1 -1
  58. package/_types/actions/beam/common.d.ts +1 -1
  59. package/_types/actions/beam/common.d.ts.map +1 -1
  60. package/_types/actions/core/markets/common.d.ts.map +1 -1
  61. package/_types/actions/governance/getGovernanceTokenInfo.d.ts.map +1 -1
  62. package/_types/actions/governance/getUserVoteReceipt.d.ts.map +1 -1
  63. package/_types/actions/governance/governor-api-client.d.ts +14 -5
  64. package/_types/actions/governance/governor-api-client.d.ts.map +1 -1
  65. package/_types/actions/governance/proposals/common.d.ts +41 -6
  66. package/_types/actions/governance/proposals/common.d.ts.map +1 -1
  67. package/_types/actions/governance/proposals/getProposal.d.ts +1 -1
  68. package/_types/actions/governance/proposals/getProposal.d.ts.map +1 -1
  69. package/_types/actions/governance/proposals/getProposals.d.ts +1 -1
  70. package/_types/actions/governance/proposals/getProposals.d.ts.map +1 -1
  71. package/_types/client/createMoonwellClient.d.ts +9 -839
  72. package/_types/client/createMoonwellClient.d.ts.map +1 -1
  73. package/_types/common/index.d.ts +11 -0
  74. package/_types/common/index.d.ts.map +1 -1
  75. package/_types/environments/definitions/governance.d.ts +0 -7
  76. package/_types/environments/definitions/governance.d.ts.map +1 -1
  77. package/_types/environments/index.d.ts +5 -516
  78. package/_types/environments/index.d.ts.map +1 -1
  79. package/_types/environments/types/config.d.ts +14 -0
  80. package/_types/environments/types/config.d.ts.map +1 -1
  81. package/_types/errors/version.d.ts +1 -1
  82. package/actions/beam/common.ts +4 -9
  83. package/actions/core/markets/common.ts +307 -166
  84. package/actions/governance/getDelegates.ts +8 -8
  85. package/actions/governance/getGovernanceTokenInfo.ts +23 -15
  86. package/actions/governance/getUserVoteReceipt.ts +19 -5
  87. package/actions/governance/governor-api-client.ts +14 -5
  88. package/actions/governance/proposals/common.ts +143 -10
  89. package/actions/governance/proposals/getProposal.ts +29 -112
  90. package/actions/governance/proposals/getProposals.ts +52 -153
  91. package/client/createMoonwellClient.ts +32 -40
  92. package/common/index.ts +17 -6
  93. package/environments/definitions/governance.ts +5 -9
  94. package/environments/index.ts +28 -70
  95. package/environments/types/config.ts +14 -0
  96. package/errors/version.ts +1 -1
  97. package/package.json +1 -1
  98. package/_cjs/environments/definitions/moonbeam/contracts.js +0 -22
  99. package/_cjs/environments/definitions/moonbeam/contracts.js.map +0 -1
  100. package/_cjs/environments/definitions/moonbeam/core-markets.js +0 -65
  101. package/_cjs/environments/definitions/moonbeam/core-markets.js.map +0 -1
  102. package/_cjs/environments/definitions/moonbeam/custom.js +0 -24
  103. package/_cjs/environments/definitions/moonbeam/custom.js.map +0 -1
  104. package/_cjs/environments/definitions/moonbeam/environment.js +0 -38
  105. package/_cjs/environments/definitions/moonbeam/environment.js.map +0 -1
  106. package/_cjs/environments/definitions/moonbeam/tokens.js +0 -176
  107. package/_cjs/environments/definitions/moonbeam/tokens.js.map +0 -1
  108. package/_cjs/environments/definitions/moonriver/contracts.js +0 -19
  109. package/_cjs/environments/definitions/moonriver/contracts.js.map +0 -1
  110. package/_cjs/environments/definitions/moonriver/core-markets.js +0 -46
  111. package/_cjs/environments/definitions/moonriver/core-markets.js.map +0 -1
  112. package/_cjs/environments/definitions/moonriver/custom.js +0 -12
  113. package/_cjs/environments/definitions/moonriver/custom.js.map +0 -1
  114. package/_cjs/environments/definitions/moonriver/environment.js +0 -36
  115. package/_cjs/environments/definitions/moonriver/environment.js.map +0 -1
  116. package/_cjs/environments/definitions/moonriver/tokens.js +0 -110
  117. package/_cjs/environments/definitions/moonriver/tokens.js.map +0 -1
  118. package/_esm/environments/definitions/moonbeam/contracts.js +0 -19
  119. package/_esm/environments/definitions/moonbeam/contracts.js.map +0 -1
  120. package/_esm/environments/definitions/moonbeam/core-markets.js +0 -62
  121. package/_esm/environments/definitions/moonbeam/core-markets.js.map +0 -1
  122. package/_esm/environments/definitions/moonbeam/custom.js +0 -21
  123. package/_esm/environments/definitions/moonbeam/custom.js.map +0 -1
  124. package/_esm/environments/definitions/moonbeam/environment.js +0 -32
  125. package/_esm/environments/definitions/moonbeam/environment.js.map +0 -1
  126. package/_esm/environments/definitions/moonbeam/tokens.js +0 -173
  127. package/_esm/environments/definitions/moonbeam/tokens.js.map +0 -1
  128. package/_esm/environments/definitions/moonriver/contracts.js +0 -16
  129. package/_esm/environments/definitions/moonriver/contracts.js.map +0 -1
  130. package/_esm/environments/definitions/moonriver/core-markets.js +0 -43
  131. package/_esm/environments/definitions/moonriver/core-markets.js.map +0 -1
  132. package/_esm/environments/definitions/moonriver/custom.js +0 -9
  133. package/_esm/environments/definitions/moonriver/custom.js.map +0 -1
  134. package/_esm/environments/definitions/moonriver/environment.js +0 -31
  135. package/_esm/environments/definitions/moonriver/environment.js.map +0 -1
  136. package/_esm/environments/definitions/moonriver/tokens.js +0 -107
  137. package/_esm/environments/definitions/moonriver/tokens.js.map +0 -1
  138. package/_types/environments/definitions/moonbeam/contracts.d.ts +0 -14
  139. package/_types/environments/definitions/moonbeam/contracts.d.ts.map +0 -1
  140. package/_types/environments/definitions/moonbeam/core-markets.d.ts +0 -57
  141. package/_types/environments/definitions/moonbeam/core-markets.d.ts.map +0 -1
  142. package/_types/environments/definitions/moonbeam/custom.d.ts +0 -25
  143. package/_types/environments/definitions/moonbeam/custom.d.ts.map +0 -1
  144. package/_types/environments/definitions/moonbeam/environment.d.ts +0 -265
  145. package/_types/environments/definitions/moonbeam/environment.d.ts.map +0 -1
  146. package/_types/environments/definitions/moonbeam/tokens.d.ts +0 -171
  147. package/_types/environments/definitions/moonbeam/tokens.d.ts.map +0 -1
  148. package/_types/environments/definitions/moonriver/contracts.d.ts +0 -11
  149. package/_types/environments/definitions/moonriver/contracts.d.ts.map +0 -1
  150. package/_types/environments/definitions/moonriver/core-markets.d.ts +0 -38
  151. package/_types/environments/definitions/moonriver/core-markets.d.ts.map +0 -1
  152. package/_types/environments/definitions/moonriver/custom.d.ts +0 -8
  153. package/_types/environments/definitions/moonriver/custom.d.ts.map +0 -1
  154. package/_types/environments/definitions/moonriver/environment.d.ts +0 -159
  155. package/_types/environments/definitions/moonriver/environment.d.ts.map +0 -1
  156. package/_types/environments/definitions/moonriver/tokens.d.ts +0 -105
  157. package/_types/environments/definitions/moonriver/tokens.d.ts.map +0 -1
  158. package/environments/definitions/moonbeam/contracts.ts +0 -19
  159. package/environments/definitions/moonbeam/core-markets.ts +0 -62
  160. package/environments/definitions/moonbeam/custom.ts +0 -21
  161. package/environments/definitions/moonbeam/environment.ts +0 -39
  162. package/environments/definitions/moonbeam/tokens.ts +0 -173
  163. package/environments/definitions/moonriver/contracts.ts +0 -16
  164. package/environments/definitions/moonriver/core-markets.ts +0 -43
  165. package/environments/definitions/moonriver/custom.ts +0 -9
  166. package/environments/definitions/moonriver/environment.ts +0 -38
  167. package/environments/definitions/moonriver/tokens.ts +0 -107
@@ -1,5 +1,5 @@
1
1
  import { isAddress } from "viem";
2
- import { base, mainnet, moonbeam, optimism } from "viem/chains";
2
+ import { base, mainnet, optimism } from "viem/chains";
3
3
  import type { MoonwellClient } from "../../client/createMoonwellClient.js";
4
4
  import {
5
5
  type Environment,
@@ -23,17 +23,17 @@ export async function getDelegates(
23
23
  ): GetDelegatesReturnType {
24
24
  const logId = logger.start("getDelegates", "Starting to get delegates...");
25
25
 
26
- const governanceEnvironment = publicEnvironments.moonbeam;
26
+ // Any environment resolves the same `governanceIndexerUrl`; the Ethereum hub
27
+ // replaces Moonbeam here now that the sunset chain is gone (MOO-551).
28
+ const governanceEnvironment = publicEnvironments.ethereum;
27
29
 
28
30
  const apiVoters = await fetchAllVoters(governanceEnvironment);
29
31
  const forumProfiles = await getForumProfiles();
30
32
 
31
- const targetChainIds = [
32
- moonbeam.id,
33
- base.id,
34
- optimism.id,
35
- mainnet.id,
36
- ] as const;
33
+ // Moonbeam dropped with the sunset: the chain is halted, so it can no longer
34
+ // serve `getUserVotingPower`. Delegate totals therefore no longer include any
35
+ // WELL that remained on Moonbeam.
36
+ const targetChainIds = [base.id, optimism.id, mainnet.id] as const;
37
37
  const envs = Object.values(client.environments as Environment[]).filter(
38
38
  (env) =>
39
39
  env.contracts.views !== undefined &&
@@ -19,6 +19,8 @@ export type GetGovernanceTokenInfoReturnType = Promise<
19
19
 
20
20
  export async function getGovernanceTokenInfo(
21
21
  _client: MoonwellClient,
22
+ // Retained for API compatibility, but WELL is the only governance token left
23
+ // now that MFAM went with Moonriver (MOO-551), so there is nothing to branch on.
22
24
  args: GetGovernanceTokenInfoParameters,
23
25
  ): GetGovernanceTokenInfoReturnType {
24
26
  const logId = logger.start(
@@ -26,22 +28,28 @@ export async function getGovernanceTokenInfo(
26
28
  "Starting to get governance token info...",
27
29
  );
28
30
 
29
- if (args.governanceToken === "WELL") {
30
- const totalSupply =
31
- await publicEnvironments.moonbeam.contracts.governanceToken.read.totalSupply();
32
-
31
+ // TypeScript already narrows `GovernanceToken` to "WELL", but a JS consumer
32
+ // still passing "MFAM" would otherwise receive WELL's supply as a normal
33
+ // success — silently wrong data where TS consumers get a compile error.
34
+ // Reject it rather than let the removed token look like it still resolves.
35
+ if (args.governanceToken !== undefined && args.governanceToken !== "WELL") {
33
36
  logger.end(logId);
37
+ throw new Error(
38
+ `Unsupported governance token "${args.governanceToken}". MFAM was removed with the Moonriver sunset (MOO-551); WELL is the only governance token.`,
39
+ );
40
+ }
34
41
 
35
- return {
36
- totalSupply: new Amount(totalSupply || 0n, 18),
37
- };
38
- } else {
39
- const totalSupply =
40
- await publicEnvironments.moonriver.contracts.governanceToken.read.totalSupply();
42
+ // WELL supply used to be read from Moonbeam, the original mint. That chain is
43
+ // halted (MOO-551), so the read now targets the Ethereum multigov hub — the
44
+ // home of live governance. Note this reports the hub's xWELL supply rather
45
+ // than Moonbeam's historical total, so the number can differ from pre-sunset
46
+ // releases. MFAM is gone entirely: Apollo governance ended with Moonriver.
47
+ const totalSupply =
48
+ await publicEnvironments.ethereum.contracts.governanceToken.read.totalSupply();
41
49
 
42
- logger.end(logId);
43
- return {
44
- totalSupply: new Amount(totalSupply || 0n, 18),
45
- };
46
- }
50
+ logger.end(logId);
51
+
52
+ return {
53
+ totalSupply: new Amount(totalSupply || 0n, 18),
54
+ };
47
55
  }
@@ -1,7 +1,8 @@
1
1
  import type { Address, Chain } from "viem";
2
2
  import type { MoonwellClient } from "../../client/createMoonwellClient.js";
3
- import { Amount, getEnvironmentFromArgs } from "../../common/index.js";
3
+ import { Amount } from "../../common/index.js";
4
4
  import type { NetworkParameterType } from "../../common/types.js";
5
+ import type { Environment } from "../../environments/index.js";
5
6
  import type { VoteReceipt } from "../../types/voteReceipt.js";
6
7
  import {
7
8
  type ApiVoteReceipt,
@@ -10,6 +11,7 @@ import {
10
11
  fetchUserVoteReceipt,
11
12
  isNotFoundError,
12
13
  } from "./governor-api-client.js";
14
+ import { resolveGovernanceEnvironment } from "./proposals/common.js";
13
15
 
14
16
  export type GetUserVoteReceiptParameters<
15
17
  environments,
@@ -50,8 +52,17 @@ export async function getUserVoteReceipt<
50
52
  ): GetUserVoteReceiptReturnType {
51
53
  const { proposalId, userAddress, chainId } = args;
52
54
 
53
- const environment = getEnvironmentFromArgs(client, args);
54
- if (!environment) {
55
+ // `args.chainId` identifies the chain the PROPOSAL lives on, not the
56
+ // environment we read through — the same distinction `getProposal` draws
57
+ // (MOO-551). Resolving the environment from it meant an archive lookup
58
+ // (1284/1285) matched no registered environment post-sunset and returned `[]`,
59
+ // a third shape callers don't handle: every historical vote read as "didn't
60
+ // vote". Resolve the indexer source independently and pass `chainId` through
61
+ // untouched.
62
+ const governanceEnvironment = resolveGovernanceEnvironment(
63
+ Object.values(client.environments as Record<string, Environment>),
64
+ );
65
+ if (!governanceEnvironment) {
55
66
  return [];
56
67
  }
57
68
 
@@ -64,7 +75,7 @@ export async function getUserVoteReceipt<
64
75
  for (const cid of tryChains) {
65
76
  try {
66
77
  const apiVoteReceipts = await fetchUserVoteReceipt(
67
- environment,
78
+ governanceEnvironment,
68
79
  cid,
69
80
  proposalId,
70
81
  userAddress,
@@ -87,7 +98,10 @@ export async function getUserVoteReceipt<
87
98
  if (collected.length === 0) {
88
99
  return [
89
100
  {
90
- chainId: environment.chainId,
101
+ // An explicit request names the chain the stub is about; a bare lookup
102
+ // queried every supported chain, so the source we read through is the
103
+ // only chain it can honestly claim.
104
+ chainId: chainId ?? governanceEnvironment.chainId,
91
105
  proposalId,
92
106
  account: userAddress,
93
107
  voted: false,
@@ -15,11 +15,20 @@ const getGovernorApiUrl = (environment: Environment): string => {
15
15
  * active multigov contract lives; Moonbeam follows for the historical archive,
16
16
  * then Moonriver (legacy standalone governor).
17
17
  *
18
- * This is the fan-out set for `getUserVoteReceipt` — a bare proposalId is
19
- * queried on every chain and non-empty receipts are concatenated. Do NOT use it
20
- * as the single-proposal fallback in `getProposal`: Moonriver has its own
21
- * explicit route there, and reaching a 1285 proposal through a Moonbeam env
22
- * would surface a degraded result (see `MULTIGOV_PROPOSAL_FALLBACK_CHAIN_IDS`).
18
+ * The fan-out set for two consumers: `getUserVoteReceipt` (a bare proposalId is
19
+ * queried on every chain and non-empty receipts are concatenated) and
20
+ * `getProposals` (every chain's list is fetched and merged, so the archive stays
21
+ * listable after the sunset).
22
+ *
23
+ * Since MOO-551 no Moonbeam/Moonriver environment exists, so 1284 and 1285 are
24
+ * served through whichever governance environment resolved — reporting
25
+ * `quorum: 0` and no `eta` because nothing on-chain can be read for a halted
26
+ * chain. That degradation is the accepted post-sunset design here, not a bug.
27
+ *
28
+ * Still do NOT use this set as the *single-proposal* fallback in `getProposal`:
29
+ * a bare lookup must not silently resolve to a 1285 proposal it wasn't asked
30
+ * for — that stays an explicit `chainId` route (see
31
+ * `MULTIGOV_PROPOSAL_FALLBACK_CHAIN_IDS`).
23
32
  */
24
33
  export const SUPPORTED_GOVERNOR_CHAIN_IDS = [1, 1284, 1285] as const;
25
34
 
@@ -1,10 +1,12 @@
1
1
  import axios from "axios";
2
+ import { mainnet } from "viem/chains";
2
3
  import { Amount } from "../../../common/index.js";
3
4
  import type { Environment } from "../../../environments/index.js";
4
5
  import { publicEnvironments } from "../../../environments/index.js";
5
6
  import {
6
7
  MultichainProposalState,
7
8
  MultichainProposalStateMapping,
9
+ type Proposal,
8
10
  ProposalState,
9
11
  } from "../../../types/proposal.js";
10
12
  import type { ApiProposal } from "../governor-api-client.js";
@@ -176,14 +178,24 @@ export type ApiProposalFormatted = {
176
178
  /**
177
179
  * Derive a ProposalState value from API data alone (no on-chain read).
178
180
  *
179
- * Used for proposals whose chainId doesn't match the governance environment's
180
- * chainId — e.g. chainId=1 (Ethereum multigov) proposals reached through the
181
- * Moonbeam governance environment, where on-chain reads against Moonbeam's
182
- * governor would be meaningless.
181
+ * Used for proposals whose chainId has no resolvable environment, or whose
182
+ * on-chain state read failed. Since the sunset (MOO-551) this is the permanent
183
+ * — not transient — route for every Moonbeam (1284) and Moonriver (1285)
184
+ * proposal, because no environment exists for those chains any more.
183
185
  *
184
- * Precedence (highest wins): Executed → Canceled → Queued → Active → Pending.
186
+ * Precedence (highest wins):
187
+ * Executed → Canceled → Queued → Active → Succeeded/Defeated → Pending.
185
188
  * Executed wins over Canceled because the SDK treats EXECUTED state changes as
186
189
  * the terminal truth even when an earlier CANCELED event is present.
190
+ *
191
+ * Once voting has closed with no terminal state change, the outcome is decided
192
+ * from the tallies. Without that branch every defeated archive proposal fell
193
+ * through to `Pending` and listed as perpetually pending — the pre-sunset
194
+ * Moonbeam governor read used to report the real `Defeated`. The tallies are
195
+ * the only signal available here: quorum needs an RPC and reports 0 for the
196
+ * archive, so a for-majority that missed quorum reads as Succeeded rather than
197
+ * Defeated. That is the same degradation quorum already carries, and it is
198
+ * strictly closer to the truth than Pending.
187
199
  */
188
200
  export const deriveProposalStateFromApi = (
189
201
  formatted: ApiProposalFormatted,
@@ -199,6 +211,11 @@ export const deriveProposalStateFromApi = (
199
211
  if (now >= apiProposal.votingStartTime && now <= apiProposal.votingEndTime) {
200
212
  return ProposalState.Active;
201
213
  }
214
+ if (now > apiProposal.votingEndTime) {
215
+ return formatted.forVotes.exponential > formatted.againstVotes.exponential
216
+ ? ProposalState.Succeeded
217
+ : ProposalState.Defeated;
218
+ }
202
219
  return ProposalState.Pending;
203
220
  };
204
221
 
@@ -332,6 +349,26 @@ export const getEnvironmentByChainId = (
332
349
  (e) => e.chainId === chainId,
333
350
  );
334
351
 
352
+ /**
353
+ * The environment that drives Governor-API reads for the whole proposal surface.
354
+ *
355
+ * Before the sunset this was the caller's Moonbeam/Moonriver environment, and
356
+ * both `getProposals` and `getProposal` gated on one of those being registered.
357
+ * Those chains are gone (MOO-551), so governance is now homed on the Ethereum
358
+ * multigov hub. Every environment resolves the same `governanceIndexerUrl`, so
359
+ * any registered chain can serve the indexer — Ethereum is merely preferred
360
+ * because its `multichainGovernor` also answers the live quorum/state reads.
361
+ *
362
+ * Consequence for the historical archive: Moonbeam (1284) and Moonriver (1285)
363
+ * proposals still list, because the indexer keeps serving them, but nothing
364
+ * on-chain can be read for them any more. They surface with indexer-derived
365
+ * state and no quorum or eta — see `getProposalsOnChainData`'s `!homeEnv` path.
366
+ */
367
+ export const resolveGovernanceEnvironment = (
368
+ environments: Environment[],
369
+ ): Environment | undefined =>
370
+ environments.find((e) => e.chainId === mainnet.id) ?? environments[0];
371
+
335
372
  export const readCrossChainQuorums = async (
336
373
  apiProposals: ApiProposal[],
337
374
  governanceEnvironment: Environment,
@@ -376,12 +413,18 @@ export const getProposalsOnChainData = async (
376
413
  ): Promise<ProposalOnChainData[]> => {
377
414
  let quorum = 0n;
378
415
 
379
- if (governanceEnvironment.contracts.governor) {
416
+ // The governance environment is now the Ethereum multigov hub (MOO-551), which
417
+ // has a `multichainGovernor` and no legacy `governor`. Read whichever the env
418
+ // actually wires, otherwise a hub-local proposal would report quorum 0.
419
+ const localQuorumRead = governanceEnvironment.contracts.governor
420
+ ? () => governanceEnvironment.contracts.governor?.read.getQuorum()
421
+ : governanceEnvironment.contracts.multichainGovernor
422
+ ? () => governanceEnvironment.contracts.multichainGovernor?.read.quorum()
423
+ : undefined;
424
+
425
+ if (localQuorumRead) {
380
426
  try {
381
- quorum =
382
- governanceEnvironment.chainId === 1284
383
- ? await governanceEnvironment.contracts.governor.read.quorumVotes()
384
- : await governanceEnvironment.contracts.governor.read.getQuorum();
427
+ quorum = (await localQuorumRead()) ?? 0n;
385
428
  } catch (error) {
386
429
  console.warn("Failed to fetch quorum:", error);
387
430
  // Report so a swallowed quorum failure (which leaves quorum at 0n) is
@@ -571,3 +614,93 @@ export const getProposalsOnChainData = async (
571
614
 
572
615
  return onChainDataList;
573
616
  };
617
+
618
+ /**
619
+ * Map an indexer proposal plus its on-chain data onto the public `Proposal`.
620
+ *
621
+ * `getProposal` and `getProposals` both used to carry their own copy of this
622
+ * block, and both carried a comment warning the other not to drift from it —
623
+ * jscpd measured 69 duplicated lines between them. One implementation removes
624
+ * the drift risk those comments were describing.
625
+ */
626
+ export const mapApiProposalToProposal = (
627
+ apiProposal: ApiProposal,
628
+ onChainData: ProposalOnChainData,
629
+ governanceEnvironment: Environment,
630
+ ): Proposal => {
631
+ const formattedData = formatApiProposalData(apiProposal);
632
+ // getProposalsOnChainData already classified this proposal and routed its
633
+ // reads accordingly; re-classifying here would drift and drop `multichain`.
634
+ const isMultichain = onChainData.isMultichain;
635
+
636
+ const now = Math.floor(Date.now() / 1000);
637
+ let proposalState = onChainData.state;
638
+
639
+ if (
640
+ proposalState === ProposalState.Pending &&
641
+ now >= apiProposal.votingStartTime &&
642
+ now <= apiProposal.votingEndTime
643
+ ) {
644
+ proposalState = ProposalState.Active;
645
+ }
646
+
647
+ if (formattedData.executed) {
648
+ proposalState = ProposalState.Executed;
649
+ } else if (
650
+ isMultichain &&
651
+ onChainData.votesCollected &&
652
+ now > apiProposal.votingEndTime &&
653
+ proposalState === ProposalState.Succeeded
654
+ ) {
655
+ // Succeeded with collection done means "awaiting execution" — surface as
656
+ // Queued so the frontend renders the "Ready to Execute" timeline step.
657
+ // Defeated/Canceled/Executed must NOT be promoted: under the
658
+ // state-machine-based votesCollected those terminal states also satisfy
659
+ // `votesCollected: true`, so a `< Queued` gate would mislabel them.
660
+ proposalState = ProposalState.Queued;
661
+ }
662
+
663
+ const proposal: Proposal = {
664
+ id: apiProposal.proposalId,
665
+ chainId: apiProposal.chainId,
666
+ proposalId: apiProposal.proposalId,
667
+ proposer: apiProposal.proposer as `0x${string}`,
668
+ eta: onChainData.eta,
669
+ startTimestamp: apiProposal.votingStartTime,
670
+ endTimestamp: apiProposal.votingEndTime,
671
+ startBlock: Number(apiProposal.blockNumber),
672
+ forVotes: formattedData.forVotes,
673
+ againstVotes: formattedData.againstVotes,
674
+ abstainVotes: formattedData.abstainVotes,
675
+ totalVotes: formattedData.totalVotes,
676
+ canceled: formattedData.canceled,
677
+ executed: formattedData.executed,
678
+ quorum: new Amount(onChainData.quorum, 18),
679
+ state: proposalState,
680
+ // Extended data
681
+ title: formattedData.title,
682
+ subtitle: formattedData.subtitle,
683
+ description: apiProposal.description,
684
+ targets: apiProposal.targets,
685
+ calldatas: apiProposal.calldatas,
686
+ // Legacy-governor proposals (Moonriver, early Moonbeam) carry the function
687
+ // signature separately from the selector-less calldata; pass it through so
688
+ // consumers can decode the call. Empty for multichain-governor proposals.
689
+ signatures: apiProposal.signatures ?? [],
690
+ stateChanges: formattedData.stateChanges,
691
+ environment: governanceEnvironment,
692
+ };
693
+
694
+ if (apiProposal.snapshotBlocks) {
695
+ proposal.snapshotBlocks = apiProposal.snapshotBlocks;
696
+ }
697
+
698
+ if (isMultichain) {
699
+ proposal.multichain = {
700
+ id: apiProposal.proposalId,
701
+ votesCollected: onChainData.votesCollected,
702
+ };
703
+ }
704
+
705
+ return proposal;
706
+ };
@@ -1,9 +1,7 @@
1
- import { mainnet, moonbeam, moonriver } from "viem/chains";
2
1
  import type { MoonwellClient } from "../../../client/createMoonwellClient.js";
3
- import { Amount, getEnvironmentFromArgs } from "../../../common/index.js";
4
2
  import type { NetworkParameterType } from "../../../common/types.js";
5
3
  import type { Chain, Environment } from "../../../environments/index.js";
6
- import { type Proposal, ProposalState } from "../../../types/proposal.js";
4
+ import type { Proposal } from "../../../types/proposal.js";
7
5
  import {
8
6
  type ApiProposal,
9
7
  MULTIGOV_PROPOSAL_FALLBACK_CHAIN_IDS,
@@ -12,9 +10,10 @@ import {
12
10
  } from "../governor-api-client.js";
13
11
  import { resolveIpfsDescriptions } from "../ipfs.js";
14
12
  import {
15
- formatApiProposalData,
16
13
  getProposalsOnChainData,
14
+ mapApiProposalToProposal,
17
15
  readCrossChainQuorums,
16
+ resolveGovernanceEnvironment,
18
17
  } from "./common.js";
19
18
 
20
19
  export type GetProposalParameters<
@@ -41,42 +40,24 @@ export async function getProposal<
41
40
  ): GetProposalReturnType {
42
41
  const { proposalId } = args;
43
42
 
44
- const environment = getEnvironmentFromArgs(client, args);
45
- if (!environment) {
46
- return undefined;
47
- }
48
-
49
- // Ethereum-home multigov proposals are served by the same Governor API as
50
- // historical Moonbeam ones (the lunar indexer fans out both chainIds), so
51
- // route them through `getGovernorApiProposal` using the Moonbeam env as the
52
- // indexer source. Without this, a caller resolving the env by `chainId: 1`
53
- // would bail out on the `!moonbeam && !moonriver` check below and the page
54
- // reload path returns undefined.
55
- if (environment.chainId === mainnet.id) {
56
- const moonbeamEnv = Object.values(
57
- client.environments as Record<string, Environment>,
58
- ).find((e) => e.chainId === moonbeam.id);
59
- if (!moonbeamEnv) {
60
- return undefined;
61
- }
62
- return getGovernorApiProposal(
63
- moonbeamEnv,
64
- proposalId,
65
- args.chainId ?? mainnet.id,
66
- );
67
- }
68
-
69
- if (
70
- environment.chainId !== moonbeam.id &&
71
- environment.chainId !== moonriver.id
72
- ) {
43
+ // `args.chainId` identifies the chain the PROPOSAL lives on, which is no longer
44
+ // the same thing as the environment we read through. Before the sunset it was
45
+ // both, and the action resolved a Moonbeam/Moonriver environment from it —
46
+ // which now means a `chainId: 1284` lookup would resolve nothing and the whole
47
+ // historical archive would 404 (MOO-551). Resolve the indexer source
48
+ // independently and pass `chainId` through untouched.
49
+ const governanceEnvironment = resolveGovernanceEnvironment(
50
+ Object.values(client.environments as Record<string, Environment>),
51
+ );
52
+ if (!governanceEnvironment) {
73
53
  return undefined;
74
54
  }
75
55
 
76
- if (environment.chainId === moonbeam.id) {
77
- return getGovernorApiProposal(environment, proposalId, args.chainId);
78
- }
79
- return getGovernorApiProposal(environment, proposalId, moonriver.id);
56
+ return getGovernorApiProposal(
57
+ governanceEnvironment,
58
+ proposalId,
59
+ args.chainId,
60
+ );
80
61
  }
81
62
 
82
63
  /**
@@ -118,87 +99,23 @@ async function getGovernorApiProposal(
118
99
  readCrossChainQuorums([apiProposal], governanceEnvironment),
119
100
  ]);
120
101
 
121
- const formattedData = formatApiProposalData(apiProposal);
122
102
  const onChainDataList = await getProposalsOnChainData(
123
103
  [apiProposal],
124
104
  governanceEnvironment,
125
105
  { crossChainQuorums },
126
106
  );
127
- const onChainData = onChainDataList[0]!;
128
- // Single source of truth: getProposalsOnChainData already classified this
129
- // proposal with the caller env's Artemis cutoff and used it to route the
130
- // on-chain reads. Reusing it avoids the drift that left Moonbeam-homed
131
- // local-target proposals (and hub-local Ethereum ones) without `multichain`.
132
- const isMultichain = onChainData.isMultichain;
133
-
134
- const now = Math.floor(Date.now() / 1000);
135
- let proposalState = onChainData.state;
136
107
 
137
- if (
138
- proposalState === ProposalState.Pending &&
139
- now >= apiProposal.votingStartTime &&
140
- now <= apiProposal.votingEndTime
141
- ) {
142
- proposalState = ProposalState.Active;
143
- }
144
-
145
- if (formattedData.executed) {
146
- proposalState = ProposalState.Executed;
147
- } else if (
148
- isMultichain &&
149
- onChainData.votesCollected &&
150
- now > apiProposal.votingEndTime &&
151
- proposalState === ProposalState.Succeeded
152
- ) {
153
- // Succeeded with collection done means "awaiting execution" — surface as
154
- // Queued so the frontend renders the "Ready to Execute" timeline step.
155
- // Defeated/Canceled/Executed must NOT be promoted: under the new
156
- // state-machine-based votesCollected, those terminal states also satisfy
157
- // `votesCollected: true`, so a `< Queued` gate would mislabel them.
158
- proposalState = ProposalState.Queued;
159
- }
160
-
161
- const proposal: Proposal = {
162
- id: apiProposal.proposalId,
163
- chainId: apiProposal.chainId,
164
- proposalId: apiProposal.proposalId,
165
- proposer: apiProposal.proposer as `0x${string}`,
166
- eta: onChainData.eta,
167
- startTimestamp: apiProposal.votingStartTime,
168
- endTimestamp: apiProposal.votingEndTime,
169
- startBlock: Number(apiProposal.blockNumber),
170
- forVotes: formattedData.forVotes,
171
- againstVotes: formattedData.againstVotes,
172
- abstainVotes: formattedData.abstainVotes,
173
- totalVotes: formattedData.totalVotes,
174
- canceled: formattedData.canceled,
175
- executed: formattedData.executed,
176
- quorum: new Amount(onChainData.quorum, 18),
177
- state: proposalState,
178
- // Extended data
179
- title: formattedData.title,
180
- subtitle: formattedData.subtitle,
181
- description: apiProposal.description,
182
- targets: apiProposal.targets,
183
- calldatas: apiProposal.calldatas,
184
- // Legacy-governor proposals (Moonriver, early Moonbeam) carry the function
185
- // signature separately from the selector-less calldata; pass it through so
186
- // consumers can decode the call. Empty for multichain-governor proposals.
187
- signatures: apiProposal.signatures ?? [],
188
- stateChanges: formattedData.stateChanges,
189
- environment: governanceEnvironment,
190
- };
191
-
192
- if (apiProposal.snapshotBlocks) {
193
- proposal.snapshotBlocks = apiProposal.snapshotBlocks;
194
- }
195
-
196
- if (isMultichain) {
197
- proposal.multichain = {
198
- id: apiProposal.proposalId,
199
- votesCollected: onChainData.votesCollected,
200
- };
108
+ // `getProposalsOnChainData` maps 1:1 over its input, so the single entry is
109
+ // always present — guard rather than assert, so the invariant is enforced
110
+ // instead of asserted away if that ever stops holding.
111
+ const onChainData = onChainDataList[0];
112
+ if (!onChainData) {
113
+ return undefined;
201
114
  }
202
115
 
203
- return proposal;
116
+ return mapApiProposalToProposal(
117
+ apiProposal,
118
+ onChainData,
119
+ governanceEnvironment,
120
+ );
204
121
  }