@moonwell-fi/moonwell-sdk 0.21.0 → 0.23.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 (173) hide show
  1. package/CHANGELOG.md +51 -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/getUserBalances.js +11 -24
  6. package/_cjs/actions/core/getUserBalances.js.map +1 -1
  7. package/_cjs/actions/core/markets/common.js +146 -96
  8. package/_cjs/actions/core/markets/common.js.map +1 -1
  9. package/_cjs/actions/governance/getDelegates.js +2 -7
  10. package/_cjs/actions/governance/getDelegates.js.map +1 -1
  11. package/_cjs/actions/governance/getGovernanceTokenInfo.js +7 -12
  12. package/_cjs/actions/governance/getGovernanceTokenInfo.js.map +1 -1
  13. package/_cjs/actions/governance/getUserVoteReceipt.js +5 -4
  14. package/_cjs/actions/governance/getUserVoteReceipt.js.map +1 -1
  15. package/_cjs/actions/governance/governor-api-client.js.map +1 -1
  16. package/_cjs/actions/governance/proposals/common.js +73 -6
  17. package/_cjs/actions/governance/proposals/common.js.map +1 -1
  18. package/_cjs/actions/governance/proposals/getProposal.js +6 -73
  19. package/_cjs/actions/governance/proposals/getProposal.js.map +1 -1
  20. package/_cjs/actions/governance/proposals/getProposals.js +12 -92
  21. package/_cjs/actions/governance/proposals/getProposals.js.map +1 -1
  22. package/_cjs/client/createMoonwellClient.js +17 -15
  23. package/_cjs/client/createMoonwellClient.js.map +1 -1
  24. package/_cjs/common/index.js +4 -4
  25. package/_cjs/common/index.js.map +1 -1
  26. package/_cjs/environments/definitions/governance.js +1 -8
  27. package/_cjs/environments/definitions/governance.js.map +1 -1
  28. package/_cjs/environments/index.js +28 -42
  29. package/_cjs/environments/index.js.map +1 -1
  30. package/_cjs/environments/types/config.js.map +1 -1
  31. package/_cjs/errors/version.js +1 -1
  32. package/_esm/actions/beam/common.js +3 -2
  33. package/_esm/actions/beam/common.js.map +1 -1
  34. package/_esm/actions/core/getUserBalances.js +23 -24
  35. package/_esm/actions/core/getUserBalances.js.map +1 -1
  36. package/_esm/actions/core/markets/common.js +192 -104
  37. package/_esm/actions/core/markets/common.js.map +1 -1
  38. package/_esm/actions/governance/getDelegates.js +8 -8
  39. package/_esm/actions/governance/getDelegates.js.map +1 -1
  40. package/_esm/actions/governance/getGovernanceTokenInfo.js +20 -13
  41. package/_esm/actions/governance/getGovernanceTokenInfo.js.map +1 -1
  42. package/_esm/actions/governance/getUserVoteReceipt.js +16 -5
  43. package/_esm/actions/governance/getUserVoteReceipt.js.map +1 -1
  44. package/_esm/actions/governance/governor-api-client.js +14 -5
  45. package/_esm/actions/governance/governor-api-client.js.map +1 -1
  46. package/_esm/actions/governance/proposals/common.js +122 -10
  47. package/_esm/actions/governance/proposals/common.js.map +1 -1
  48. package/_esm/actions/governance/proposals/getProposal.js +16 -93
  49. package/_esm/actions/governance/proposals/getProposal.js.map +1 -1
  50. package/_esm/actions/governance/proposals/getProposals.js +37 -132
  51. package/_esm/actions/governance/proposals/getProposals.js.map +1 -1
  52. package/_esm/client/createMoonwellClient.js +24 -16
  53. package/_esm/client/createMoonwellClient.js.map +1 -1
  54. package/_esm/common/index.js +15 -4
  55. package/_esm/common/index.js.map +1 -1
  56. package/_esm/environments/definitions/governance.js +5 -9
  57. package/_esm/environments/definitions/governance.js.map +1 -1
  58. package/_esm/environments/index.js +2 -14
  59. package/_esm/environments/index.js.map +1 -1
  60. package/_esm/environments/types/config.js.map +1 -1
  61. package/_esm/errors/version.js +1 -1
  62. package/_types/actions/beam/common.d.ts +1 -1
  63. package/_types/actions/beam/common.d.ts.map +1 -1
  64. package/_types/actions/core/getUserBalances.d.ts.map +1 -1
  65. package/_types/actions/core/markets/common.d.ts.map +1 -1
  66. package/_types/actions/governance/getGovernanceTokenInfo.d.ts.map +1 -1
  67. package/_types/actions/governance/getUserVoteReceipt.d.ts.map +1 -1
  68. package/_types/actions/governance/governor-api-client.d.ts +14 -5
  69. package/_types/actions/governance/governor-api-client.d.ts.map +1 -1
  70. package/_types/actions/governance/proposals/common.d.ts +41 -6
  71. package/_types/actions/governance/proposals/common.d.ts.map +1 -1
  72. package/_types/actions/governance/proposals/getProposal.d.ts +1 -1
  73. package/_types/actions/governance/proposals/getProposal.d.ts.map +1 -1
  74. package/_types/actions/governance/proposals/getProposals.d.ts +1 -1
  75. package/_types/actions/governance/proposals/getProposals.d.ts.map +1 -1
  76. package/_types/client/createMoonwellClient.d.ts +10 -843
  77. package/_types/client/createMoonwellClient.d.ts.map +1 -1
  78. package/_types/common/index.d.ts +11 -0
  79. package/_types/common/index.d.ts.map +1 -1
  80. package/_types/environments/definitions/governance.d.ts +0 -7
  81. package/_types/environments/definitions/governance.d.ts.map +1 -1
  82. package/_types/environments/index.d.ts +7 -518
  83. package/_types/environments/index.d.ts.map +1 -1
  84. package/_types/environments/types/config.d.ts +22 -8
  85. package/_types/environments/types/config.d.ts.map +1 -1
  86. package/_types/errors/version.d.ts +1 -1
  87. package/actions/beam/common.ts +4 -9
  88. package/actions/core/getUserBalances.ts +25 -31
  89. package/actions/core/markets/common.ts +307 -166
  90. package/actions/governance/getDelegates.ts +8 -8
  91. package/actions/governance/getGovernanceTokenInfo.ts +23 -15
  92. package/actions/governance/getUserVoteReceipt.ts +19 -5
  93. package/actions/governance/governor-api-client.ts +14 -5
  94. package/actions/governance/proposals/common.ts +143 -10
  95. package/actions/governance/proposals/getProposal.ts +29 -112
  96. package/actions/governance/proposals/getProposals.ts +52 -153
  97. package/client/createMoonwellClient.ts +34 -44
  98. package/common/index.ts +17 -6
  99. package/environments/definitions/governance.ts +5 -9
  100. package/environments/index.ts +34 -71
  101. package/environments/types/config.ts +24 -8
  102. package/errors/version.ts +1 -1
  103. package/package.json +1 -1
  104. package/_cjs/environments/definitions/moonbeam/contracts.js +0 -22
  105. package/_cjs/environments/definitions/moonbeam/contracts.js.map +0 -1
  106. package/_cjs/environments/definitions/moonbeam/core-markets.js +0 -65
  107. package/_cjs/environments/definitions/moonbeam/core-markets.js.map +0 -1
  108. package/_cjs/environments/definitions/moonbeam/custom.js +0 -24
  109. package/_cjs/environments/definitions/moonbeam/custom.js.map +0 -1
  110. package/_cjs/environments/definitions/moonbeam/environment.js +0 -38
  111. package/_cjs/environments/definitions/moonbeam/environment.js.map +0 -1
  112. package/_cjs/environments/definitions/moonbeam/tokens.js +0 -176
  113. package/_cjs/environments/definitions/moonbeam/tokens.js.map +0 -1
  114. package/_cjs/environments/definitions/moonriver/contracts.js +0 -19
  115. package/_cjs/environments/definitions/moonriver/contracts.js.map +0 -1
  116. package/_cjs/environments/definitions/moonriver/core-markets.js +0 -46
  117. package/_cjs/environments/definitions/moonriver/core-markets.js.map +0 -1
  118. package/_cjs/environments/definitions/moonriver/custom.js +0 -12
  119. package/_cjs/environments/definitions/moonriver/custom.js.map +0 -1
  120. package/_cjs/environments/definitions/moonriver/environment.js +0 -36
  121. package/_cjs/environments/definitions/moonriver/environment.js.map +0 -1
  122. package/_cjs/environments/definitions/moonriver/tokens.js +0 -110
  123. package/_cjs/environments/definitions/moonriver/tokens.js.map +0 -1
  124. package/_esm/environments/definitions/moonbeam/contracts.js +0 -19
  125. package/_esm/environments/definitions/moonbeam/contracts.js.map +0 -1
  126. package/_esm/environments/definitions/moonbeam/core-markets.js +0 -62
  127. package/_esm/environments/definitions/moonbeam/core-markets.js.map +0 -1
  128. package/_esm/environments/definitions/moonbeam/custom.js +0 -21
  129. package/_esm/environments/definitions/moonbeam/custom.js.map +0 -1
  130. package/_esm/environments/definitions/moonbeam/environment.js +0 -32
  131. package/_esm/environments/definitions/moonbeam/environment.js.map +0 -1
  132. package/_esm/environments/definitions/moonbeam/tokens.js +0 -173
  133. package/_esm/environments/definitions/moonbeam/tokens.js.map +0 -1
  134. package/_esm/environments/definitions/moonriver/contracts.js +0 -16
  135. package/_esm/environments/definitions/moonriver/contracts.js.map +0 -1
  136. package/_esm/environments/definitions/moonriver/core-markets.js +0 -43
  137. package/_esm/environments/definitions/moonriver/core-markets.js.map +0 -1
  138. package/_esm/environments/definitions/moonriver/custom.js +0 -9
  139. package/_esm/environments/definitions/moonriver/custom.js.map +0 -1
  140. package/_esm/environments/definitions/moonriver/environment.js +0 -31
  141. package/_esm/environments/definitions/moonriver/environment.js.map +0 -1
  142. package/_esm/environments/definitions/moonriver/tokens.js +0 -107
  143. package/_esm/environments/definitions/moonriver/tokens.js.map +0 -1
  144. package/_types/environments/definitions/moonbeam/contracts.d.ts +0 -14
  145. package/_types/environments/definitions/moonbeam/contracts.d.ts.map +0 -1
  146. package/_types/environments/definitions/moonbeam/core-markets.d.ts +0 -57
  147. package/_types/environments/definitions/moonbeam/core-markets.d.ts.map +0 -1
  148. package/_types/environments/definitions/moonbeam/custom.d.ts +0 -25
  149. package/_types/environments/definitions/moonbeam/custom.d.ts.map +0 -1
  150. package/_types/environments/definitions/moonbeam/environment.d.ts +0 -265
  151. package/_types/environments/definitions/moonbeam/environment.d.ts.map +0 -1
  152. package/_types/environments/definitions/moonbeam/tokens.d.ts +0 -171
  153. package/_types/environments/definitions/moonbeam/tokens.d.ts.map +0 -1
  154. package/_types/environments/definitions/moonriver/contracts.d.ts +0 -11
  155. package/_types/environments/definitions/moonriver/contracts.d.ts.map +0 -1
  156. package/_types/environments/definitions/moonriver/core-markets.d.ts +0 -38
  157. package/_types/environments/definitions/moonriver/core-markets.d.ts.map +0 -1
  158. package/_types/environments/definitions/moonriver/custom.d.ts +0 -8
  159. package/_types/environments/definitions/moonriver/custom.d.ts.map +0 -1
  160. package/_types/environments/definitions/moonriver/environment.d.ts +0 -159
  161. package/_types/environments/definitions/moonriver/environment.d.ts.map +0 -1
  162. package/_types/environments/definitions/moonriver/tokens.d.ts +0 -105
  163. package/_types/environments/definitions/moonriver/tokens.d.ts.map +0 -1
  164. package/environments/definitions/moonbeam/contracts.ts +0 -19
  165. package/environments/definitions/moonbeam/core-markets.ts +0 -62
  166. package/environments/definitions/moonbeam/custom.ts +0 -21
  167. package/environments/definitions/moonbeam/environment.ts +0 -39
  168. package/environments/definitions/moonbeam/tokens.ts +0 -173
  169. package/environments/definitions/moonriver/contracts.ts +0 -16
  170. package/environments/definitions/moonriver/core-markets.ts +0 -43
  171. package/environments/definitions/moonriver/custom.ts +0 -9
  172. package/environments/definitions/moonriver/environment.ts +0 -38
  173. package/environments/definitions/moonriver/tokens.ts +0 -107
@@ -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
  }
@@ -1,16 +1,20 @@
1
- import { moonbeam, moonriver } from "viem/chains";
2
1
  import type { MoonwellClient } from "../../../client/createMoonwellClient.js";
3
- import { Amount, getEnvironmentsFromArgs } from "../../../common/index.js";
2
+ import { getEnvironmentsFromArgs } from "../../../common/index.js";
4
3
  import type { OptionalNetworkParameterType } from "../../../common/types.js";
5
4
  import type { Chain, Environment } from "../../../environments/index.js";
6
5
  import * as logger from "../../../logger/console.js";
7
- import { type Proposal, ProposalState } from "../../../types/proposal.js";
8
- import { type ApiProposal, fetchAllProposals } from "../governor-api-client.js";
6
+ import type { Proposal } from "../../../types/proposal.js";
7
+ import {
8
+ type ApiProposal,
9
+ SUPPORTED_GOVERNOR_CHAIN_IDS,
10
+ fetchAllProposals,
11
+ } from "../governor-api-client.js";
9
12
  import { resolveIpfsDescriptions } from "../ipfs.js";
10
13
  import {
11
- formatApiProposalData,
12
14
  getProposalsOnChainData,
15
+ mapApiProposalToProposal,
13
16
  readCrossChainQuorums,
17
+ resolveGovernanceEnvironment,
14
18
  } from "./common.js";
15
19
 
16
20
  export type GetProposalsParameters<
@@ -31,30 +35,14 @@ export async function getProposals<
31
35
 
32
36
  const environments = getEnvironmentsFromArgs(client, args);
33
37
 
34
- const governanceEnvironments = environments.filter(
35
- (environment) =>
36
- environment.chainId === moonbeam.id ||
37
- environment.chainId === moonriver.id,
38
- );
38
+ const governanceEnvironment = resolveGovernanceEnvironment(environments);
39
39
 
40
- if (governanceEnvironments.length === 0) {
40
+ if (!governanceEnvironment) {
41
41
  logger.end(logId);
42
42
  return [];
43
43
  }
44
44
 
45
- const allProposals = await Promise.all(
46
- governanceEnvironments.map(async (governanceEnvironment) => {
47
- if (governanceEnvironment.chainId === moonbeam.id) {
48
- // Moonbeam + Ethereum: Governor API (chainIds 1 and 1284)
49
- return getMoonbeamProposals(governanceEnvironment);
50
- } else {
51
- // Moonriver: Governor API, single legacy governor (chainId 1285)
52
- return getMoonriverProposals(governanceEnvironment);
53
- }
54
- }),
55
- );
56
-
57
- const proposals = allProposals.flat();
45
+ const proposals = await fetchGovernorProposals(governanceEnvironment);
58
46
  // Newer multigov-ethereum proposals (chainId=1) and historical Moonbeam
59
47
  // proposals (chainId=1284) restart their proposalId counters from 1, so IDs
60
48
  // may collide across chains. Sort by proposalId desc with chainId as a
@@ -70,27 +58,34 @@ export async function getProposals<
70
58
  }
71
59
 
72
60
  /**
73
- * Fetch proposals for Moonbeam using the Governor API.
61
+ * Fetch every proposal the Governor API serves, across all governance chains.
74
62
  *
75
- * The same indexer DO serves two chains:
76
- * - chainId=1 (Ethereum) — the active multigov contract
77
- * - chainId=1284 (Moonbeam) — historical proposals
63
+ * One indexer DO serves them all:
64
+ * - chainId=1 (Ethereum) — the active multigov contract
65
+ * - chainId=1284 (Moonbeam) — historical archive, chain halted
66
+ * - chainId=1285 (Moonriver) — legacy Apollo governor, chain halted
78
67
  *
79
- * Uses `Promise.allSettled` so a transient outage on one chain doesn't take
80
- * down the other — partial results are preferred over a hard failure.
68
+ * The sunset chains keep appearing here on purpose: the indexer still has their
69
+ * proposals even though no RPC exists for them any more, so the governance
70
+ * record stays readable (MOO-551). What they lose is the on-chain half —
71
+ * `getProposalsOnChainData` finds no environment for 1284/1285 and derives their
72
+ * state from indexer events, leaving quorum and eta unset.
73
+ *
74
+ * `Promise.allSettled` so one chain's outage degrades to a missing slice rather
75
+ * than an empty list.
81
76
  */
82
- async function getMoonbeamProposals(
77
+ async function fetchGovernorProposals(
83
78
  governanceEnvironment: Environment,
84
79
  ): Promise<Proposal[]> {
85
- const results = await Promise.allSettled([
86
- fetchAllProposals(governanceEnvironment, { chainId: 1 }),
87
- fetchAllProposals(governanceEnvironment, { chainId: 1284 }),
88
- ]);
80
+ const results = await Promise.allSettled(
81
+ SUPPORTED_GOVERNOR_CHAIN_IDS.map((chainId) =>
82
+ fetchAllProposals(governanceEnvironment, { chainId }),
83
+ ),
84
+ );
89
85
 
90
- const chainsAttempted: ReadonlyArray<1 | 1284> = [1, 1284];
91
86
  const apiProposals: ApiProposal[] = [];
92
87
  results.forEach((result, index) => {
93
- const chainId = chainsAttempted[index];
88
+ const chainId = SUPPORTED_GOVERNOR_CHAIN_IDS[index];
94
89
  if (result.status === "fulfilled") {
95
90
  apiProposals.push(...result.value);
96
91
  } else if (chainId !== undefined) {
@@ -109,44 +104,13 @@ async function getMoonbeamProposals(
109
104
  }
110
105
 
111
106
  /**
112
- * Fetch proposals for Moonriver using the Governor API.
107
+ * Governor-API pipeline: resolve IPFS descriptions + cross-chain quorums in
108
+ * parallel, read on-chain data, then map each ApiProposal to a Proposal.
113
109
  *
114
- * Moonriver runs a single legacy standalone governor (no multichain governor),
115
- * so we fetch only its chainId from the same lunar indexer and reuse the shared
116
- * pipeline. `getProposalsOnChainData` classifies these as non-multichain and
117
- * reads state/quorum from the legacy governor (via `getQuorum`).
118
- */
119
- async function getMoonriverProposals(
120
- governanceEnvironment: Environment,
121
- ): Promise<Proposal[]> {
122
- // Moonriver is a single chain, so there's no other chain to "continue with"
123
- // like the Moonbeam fan-out — but a bare throw would reject the whole
124
- // `Promise.all` in getProposals and drop Moonbeam/Ethereum results too. Mirror
125
- // the per-chain handling getMoonbeamProposals uses: report the outage via
126
- // onError and degrade to an empty Moonriver list instead of rejecting.
127
- let apiProposals: ApiProposal[] = [];
128
- try {
129
- apiProposals = await fetchAllProposals(governanceEnvironment, {
130
- chainId: moonriver.id,
131
- });
132
- } catch (reason) {
133
- console.warn(
134
- `[getProposals] Failed to fetch proposals for chainId=${moonriver.id}; continuing with an empty Moonriver list.`,
135
- reason,
136
- );
137
- governanceEnvironment.onError?.(reason, {
138
- source: "governance-proposals",
139
- chainId: moonriver.id,
140
- });
141
- }
142
- return buildProposals(apiProposals, governanceEnvironment);
143
- }
144
-
145
- /**
146
- * Shared Governor-API pipeline: resolve IPFS descriptions + cross-chain quorums
147
- * in parallel, read on-chain data, then map each ApiProposal to a Proposal.
148
- * Used by both the Moonbeam/Ethereum and Moonriver paths so the mapping stays
149
- * in one place.
110
+ * Single caller (`fetchGovernorProposals`) since the sunset removed the separate
111
+ * `getMoonbeamProposals` / `getMoonriverProposals` entry points (MOO-551) —
112
+ * kept as its own function because it is the list-shaped counterpart to
113
+ * `getProposal`'s single-proposal pipeline, and the two are read together.
150
114
  */
151
115
  async function buildProposals(
152
116
  apiProposals: ApiProposal[],
@@ -164,85 +128,20 @@ async function buildProposals(
164
128
  { crossChainQuorums },
165
129
  );
166
130
 
167
- const proposals: Proposal[] = apiProposals.map((apiProposal, index) => {
168
- const onChainData = onChainDataList[index]!;
169
- const formattedData = formatApiProposalData(apiProposal);
170
- // Single source of truth — see getProposal.ts. getProposalsOnChainData
171
- // classified with the Artemis cutoff and routed the reads accordingly;
172
- // re-classifying here without the cutoff would drift and drop `multichain`.
173
- const isMultichain = onChainData.isMultichain;
174
-
175
- const now = Math.floor(Date.now() / 1000);
176
- let proposalState = onChainData.state;
177
-
178
- if (
179
- proposalState === ProposalState.Pending &&
180
- now >= apiProposal.votingStartTime &&
181
- now <= apiProposal.votingEndTime
182
- ) {
183
- proposalState = ProposalState.Active;
184
- }
185
-
186
- if (formattedData.executed) {
187
- proposalState = ProposalState.Executed;
188
- } else if (
189
- isMultichain &&
190
- onChainData.votesCollected &&
191
- now > apiProposal.votingEndTime &&
192
- proposalState === ProposalState.Succeeded
193
- ) {
194
- // Succeeded with collection done means "awaiting execution" — surface
195
- // as Queued so the frontend renders the "Ready to Execute" timeline
196
- // step. Defeated/Canceled/Executed must NOT be promoted: under the new
197
- // state-machine-based votesCollected, those terminal states also
198
- // satisfy `votesCollected: true`, so a `< Queued` gate would mislabel
199
- // them.
200
- proposalState = ProposalState.Queued;
201
- }
202
-
203
- const proposal: Proposal = {
204
- id: apiProposal.proposalId,
205
- chainId: apiProposal.chainId,
206
- proposalId: apiProposal.proposalId,
207
- proposer: apiProposal.proposer as `0x${string}`,
208
- eta: onChainData.eta,
209
- startTimestamp: apiProposal.votingStartTime,
210
- endTimestamp: apiProposal.votingEndTime,
211
- startBlock: Number(apiProposal.blockNumber),
212
- forVotes: formattedData.forVotes,
213
- againstVotes: formattedData.againstVotes,
214
- abstainVotes: formattedData.abstainVotes,
215
- totalVotes: formattedData.totalVotes,
216
- canceled: formattedData.canceled,
217
- executed: formattedData.executed,
218
- quorum: new Amount(onChainData.quorum, 18),
219
- state: proposalState,
220
- // Extended data
221
- title: formattedData.title,
222
- subtitle: formattedData.subtitle,
223
- description: apiProposal.description,
224
- targets: apiProposal.targets,
225
- calldatas: apiProposal.calldatas,
226
- // Legacy-governor proposals (Moonriver, early Moonbeam) carry the function
227
- // signature separately from the selector-less calldata; pass it through so
228
- // consumers can decode the call. Empty for multichain-governor proposals.
229
- signatures: apiProposal.signatures ?? [],
230
- stateChanges: formattedData.stateChanges,
231
- environment: governanceEnvironment,
232
- };
233
-
234
- if (apiProposal.snapshotBlocks) {
235
- proposal.snapshotBlocks = apiProposal.snapshotBlocks;
236
- }
237
-
238
- if (isMultichain) {
239
- proposal.multichain = {
240
- id: apiProposal.proposalId,
241
- votesCollected: onChainData.votesCollected,
242
- };
243
- }
244
-
245
- return proposal;
131
+ // `getProposalsOnChainData` maps 1:1 over its input, so every index resolves —
132
+ // guard rather than assert, so the invariant is enforced instead of asserted
133
+ // away if that ever stops holding.
134
+ const proposals: Proposal[] = apiProposals.flatMap((apiProposal, index) => {
135
+ const onChainData = onChainDataList[index];
136
+ return onChainData
137
+ ? [
138
+ mapApiProposalToProposal(
139
+ apiProposal,
140
+ onChainData,
141
+ governanceEnvironment,
142
+ ),
143
+ ]
144
+ : [];
246
145
  });
247
146
 
248
147
  return proposals;