@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
@@ -1,14 +1,81 @@
1
+ import axios from "axios";
1
2
  import type { Environment } from "../../environments/index.js";
2
3
  import { getWithRetry } from "../axiosWithRetry.js";
3
4
 
4
5
  /**
5
6
  * Base configuration for Governor API requests
6
- * The Governor API is the new governance indexer, accessed via governanceIndexerUrl
7
+ * The Governor API is the multigov governance indexer, accessed via governanceIndexerUrl
7
8
  */
8
9
  const getGovernorApiUrl = (environment: Environment): string => {
9
10
  return environment.governanceIndexerUrl;
10
11
  };
11
12
 
13
+ /**
14
+ * Chains the governor indexer serves. Ethereum first because that's where the
15
+ * active multigov contract lives; Moonbeam follows for the historical archive.
16
+ */
17
+ export const SUPPORTED_GOVERNOR_CHAIN_IDS = [1, 1284] as const;
18
+
19
+ /**
20
+ * Build the chain-prefixed proposal key the indexer requires
21
+ * (e.g. chainId=1, proposalId=7 → "1-0000000007").
22
+ */
23
+ export const buildProposalKey = (
24
+ chainId: number,
25
+ proposalId: number | string,
26
+ ): string => `${chainId}-${String(proposalId).padStart(10, "0")}`;
27
+
28
+ /**
29
+ * Thrown by proposal-scoped fetchers when the indexer reports the proposal
30
+ * doesn't exist (HTTP 404). Callers use `isNotFoundError` to drive chainId
31
+ * fallback without misclassifying a real 5xx outage as "not found".
32
+ */
33
+ export class GovernorNotFoundError extends Error {
34
+ public readonly chainId: number;
35
+ public readonly proposalId: number | string;
36
+ constructor(chainId: number, proposalId: number | string) {
37
+ super(
38
+ `Governor resource not found for chainId=${chainId}, proposalId=${proposalId}`,
39
+ );
40
+ this.name = "GovernorNotFoundError";
41
+ this.chainId = chainId;
42
+ this.proposalId = proposalId;
43
+ }
44
+ }
45
+
46
+ export const isNotFoundError = (error: unknown): boolean => {
47
+ if (error instanceof GovernorNotFoundError) return true;
48
+ return axios.isAxiosError(error) && error.response?.status === 404;
49
+ };
50
+
51
+ /**
52
+ * Wraps a proposal-scoped fetch: if the underlying axios call surfaces a 404,
53
+ * throws a typed `GovernorNotFoundError` so callers can distinguish missing
54
+ * resources from real outages. Other failures (5xx, non-200, network) propagate
55
+ * as-is.
56
+ */
57
+ async function fetchProposalScoped<T>(
58
+ url: string,
59
+ chainId: number,
60
+ proposalId: number | string,
61
+ resourceLabel: string,
62
+ ): Promise<T> {
63
+ try {
64
+ const response = await getWithRetry<T>(url);
65
+ if (response.status !== 200 || !response.data) {
66
+ throw new Error(
67
+ `Failed to fetch ${resourceLabel}: ${response.statusText}`,
68
+ );
69
+ }
70
+ return response.data;
71
+ } catch (error) {
72
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
73
+ throw new GovernorNotFoundError(chainId, proposalId);
74
+ }
75
+ throw error;
76
+ }
77
+ }
78
+
12
79
  /**
13
80
  * Paginated response type
14
81
  */
@@ -94,19 +161,26 @@ export type ApiVoteReceipt = {
94
161
  timestamp: number;
95
162
  };
96
163
 
164
+ export type FetchProposalsOptions = PaginationOptions & {
165
+ chainId: number;
166
+ };
167
+
97
168
  /**
98
169
  * Fetch proposals from Governor API
170
+ *
171
+ * `chainId` is required by the indexer — 1 (Ethereum multigov) or
172
+ * 1284 (Moonbeam historical). Missing/unsupported chainId returns 400.
99
173
  */
100
174
  export async function fetchProposals(
101
175
  environment: Environment,
102
- options?: PaginationOptions & { chainId?: number },
176
+ options: FetchProposalsOptions,
103
177
  ): Promise<PaginatedResponse<ApiProposal>> {
104
178
  const baseUrl = getGovernorApiUrl(environment);
105
179
  const params = new URLSearchParams();
106
180
 
107
- if (options?.limit) params.append("limit", options.limit.toString());
108
- if (options?.cursor) params.append("cursor", options.cursor);
109
- if (options?.chainId) params.append("chainId", options.chainId.toString());
181
+ params.append("chainId", options.chainId.toString());
182
+ if (options.limit) params.append("limit", options.limit.toString());
183
+ if (options.cursor) params.append("cursor", options.cursor);
110
184
 
111
185
  const response = await getWithRetry<PaginatedResponse<ApiProposal>>(
112
186
  `${baseUrl}/api/v1/governor/proposals?${params.toString()}`,
@@ -120,20 +194,20 @@ export async function fetchProposals(
120
194
  }
121
195
 
122
196
  /**
123
- * Fetch all proposals (handles pagination internally)
197
+ * Fetch all proposals for a given chainId (handles pagination internally)
124
198
  */
125
199
  export async function fetchAllProposals(
126
200
  environment: Environment,
127
- options?: { chainId?: number },
201
+ options: { chainId: number },
128
202
  ): Promise<ApiProposal[]> {
129
203
  const allProposals: ApiProposal[] = [];
130
204
  let cursor: string | undefined = undefined;
131
205
 
132
206
  do {
133
207
  const response = await fetchProposals(environment, {
208
+ chainId: options.chainId,
134
209
  limit: 1000,
135
210
  ...(cursor && { cursor }),
136
- ...(options?.chainId && { chainId: options.chainId }),
137
211
  });
138
212
 
139
213
  allProposals.push(...response.results);
@@ -148,19 +222,17 @@ export async function fetchAllProposals(
148
222
  */
149
223
  export async function fetchProposal(
150
224
  environment: Environment,
151
- proposalId: string,
225
+ chainId: number,
226
+ proposalId: number | string,
152
227
  ): Promise<ApiProposal> {
153
228
  const baseUrl = getGovernorApiUrl(environment);
154
-
155
- const response = await getWithRetry<ApiProposal>(
156
- `${baseUrl}/api/v1/governor/proposals/${proposalId}`,
229
+ const key = buildProposalKey(chainId, proposalId);
230
+ return fetchProposalScoped<ApiProposal>(
231
+ `${baseUrl}/api/v1/governor/proposals/${key}`,
232
+ chainId,
233
+ proposalId,
234
+ "proposal",
157
235
  );
158
-
159
- if (response.status !== 200 || !response.data) {
160
- throw new Error(`Failed to fetch proposal: ${response.statusText}`);
161
- }
162
-
163
- return response.data;
164
236
  }
165
237
 
166
238
  /**
@@ -168,24 +240,23 @@ export async function fetchProposal(
168
240
  */
169
241
  export async function fetchProposalVotes(
170
242
  environment: Environment,
171
- proposalId: string,
243
+ chainId: number,
244
+ proposalId: number | string,
172
245
  options?: PaginationOptions,
173
246
  ): Promise<PaginatedResponse<ApiVote>> {
174
247
  const baseUrl = getGovernorApiUrl(environment);
248
+ const key = buildProposalKey(chainId, proposalId);
175
249
  const params = new URLSearchParams();
176
250
 
177
251
  if (options?.limit) params.append("limit", options.limit.toString());
178
252
  if (options?.cursor) params.append("cursor", options.cursor);
179
253
 
180
- const response = await getWithRetry<PaginatedResponse<ApiVote>>(
181
- `${baseUrl}/api/v1/governor/proposals/${proposalId}/votes?${params.toString()}`,
254
+ return fetchProposalScoped<PaginatedResponse<ApiVote>>(
255
+ `${baseUrl}/api/v1/governor/proposals/${key}/votes?${params.toString()}`,
256
+ chainId,
257
+ proposalId,
258
+ "proposal votes",
182
259
  );
183
-
184
- if (response.status !== 200 || !response.data) {
185
- throw new Error(`Failed to fetch proposal votes: ${response.statusText}`);
186
- }
187
-
188
- return response.data;
189
260
  }
190
261
 
191
262
  /**
@@ -193,16 +264,22 @@ export async function fetchProposalVotes(
193
264
  */
194
265
  export async function fetchAllProposalVotes(
195
266
  environment: Environment,
196
- proposalId: string,
267
+ chainId: number,
268
+ proposalId: number | string,
197
269
  ): Promise<ApiVote[]> {
198
270
  const allVotes: ApiVote[] = [];
199
271
  let cursor: string | undefined = undefined;
200
272
 
201
273
  do {
202
- const response = await fetchProposalVotes(environment, proposalId, {
203
- limit: 1000,
204
- ...(cursor && { cursor }),
205
- });
274
+ const response = await fetchProposalVotes(
275
+ environment,
276
+ chainId,
277
+ proposalId,
278
+ {
279
+ limit: 1000,
280
+ ...(cursor && { cursor }),
281
+ },
282
+ );
206
283
 
207
284
  allVotes.push(...response.results);
208
285
  cursor = response.nextCursor;
@@ -216,28 +293,23 @@ export async function fetchAllProposalVotes(
216
293
  */
217
294
  export async function fetchProposalStateChanges(
218
295
  environment: Environment,
219
- proposalId: string,
296
+ chainId: number,
297
+ proposalId: number | string,
220
298
  options?: PaginationOptions,
221
299
  ): Promise<PaginatedResponse<ApiProposalStateChange>> {
222
300
  const baseUrl = getGovernorApiUrl(environment);
301
+ const key = buildProposalKey(chainId, proposalId);
223
302
  const params = new URLSearchParams();
224
303
 
225
304
  if (options?.limit) params.append("limit", options.limit.toString());
226
305
  if (options?.cursor) params.append("cursor", options.cursor);
227
306
 
228
- const response = await getWithRetry<
229
- PaginatedResponse<ApiProposalStateChange>
230
- >(
231
- `${baseUrl}/api/v1/governor/proposals/${proposalId}/state-changes?${params.toString()}`,
307
+ return fetchProposalScoped<PaginatedResponse<ApiProposalStateChange>>(
308
+ `${baseUrl}/api/v1/governor/proposals/${key}/state-changes?${params.toString()}`,
309
+ chainId,
310
+ proposalId,
311
+ "proposal state changes",
232
312
  );
233
-
234
- if (response.status !== 200 || !response.data) {
235
- throw new Error(
236
- `Failed to fetch proposal state changes: ${response.statusText}`,
237
- );
238
- }
239
-
240
- return response.data;
241
313
  }
242
314
 
243
315
  /**
@@ -245,16 +317,22 @@ export async function fetchProposalStateChanges(
245
317
  */
246
318
  export async function fetchAllProposalStateChanges(
247
319
  environment: Environment,
248
- proposalId: string,
320
+ chainId: number,
321
+ proposalId: number | string,
249
322
  ): Promise<ApiProposalStateChange[]> {
250
323
  const allStateChanges: ApiProposalStateChange[] = [];
251
324
  let cursor: string | undefined = undefined;
252
325
 
253
326
  do {
254
- const response = await fetchProposalStateChanges(environment, proposalId, {
255
- limit: 1000,
256
- ...(cursor && { cursor }),
257
- });
327
+ const response = await fetchProposalStateChanges(
328
+ environment,
329
+ chainId,
330
+ proposalId,
331
+ {
332
+ limit: 1000,
333
+ ...(cursor && { cursor }),
334
+ },
335
+ );
258
336
 
259
337
  allStateChanges.push(...response.results);
260
338
  cursor = response.nextCursor;
@@ -447,20 +525,16 @@ export async function fetchAllVoterVotes(
447
525
  */
448
526
  export async function fetchUserVoteReceipt(
449
527
  environment: Environment,
450
- proposalId: string,
528
+ chainId: number,
529
+ proposalId: number | string,
451
530
  voterAddress: string,
452
531
  ): Promise<ApiVoteReceipt[]> {
453
532
  const baseUrl = getGovernorApiUrl(environment);
454
-
455
- const response = await getWithRetry<ApiVoteReceipt[]>(
456
- `${baseUrl}/api/v1/governor/proposals/${proposalId}/vote/${voterAddress}`,
533
+ const key = buildProposalKey(chainId, proposalId);
534
+ return fetchProposalScoped<ApiVoteReceipt[]>(
535
+ `${baseUrl}/api/v1/governor/proposals/${key}/vote/${voterAddress}`,
536
+ chainId,
537
+ proposalId,
538
+ "user vote receipt",
457
539
  );
458
-
459
- if (response.status !== 200 || !response.data) {
460
- throw new Error(
461
- `Failed to fetch user vote receipt: ${response.statusText}`,
462
- );
463
- }
464
-
465
- return response.data;
466
540
  }
@@ -7,7 +7,7 @@ import { publicEnvironments } from "../../../environments/index.js";
7
7
  import {
8
8
  MultichainProposalStateMapping,
9
9
  type Proposal,
10
- type ProposalState,
10
+ ProposalState,
11
11
  } from "../../../types/proposal.js";
12
12
  import { postWithRetry } from "../../axiosWithRetry.js";
13
13
  import type { ApiProposal } from "../governor-api-client.js";
@@ -134,6 +134,35 @@ export type ApiProposalFormatted = {
134
134
  subtitle: string;
135
135
  };
136
136
 
137
+ /**
138
+ * Derive a ProposalState value from API data alone (no on-chain read).
139
+ *
140
+ * Used for proposals whose chainId doesn't match the governance environment's
141
+ * chainId — e.g. chainId=1 (Ethereum multigov) proposals reached through the
142
+ * Moonbeam governance environment, where on-chain reads against Moonbeam's
143
+ * governor would be meaningless.
144
+ *
145
+ * Precedence (highest wins): Executed → Canceled → Queued → Active → Pending.
146
+ * Executed wins over Canceled because the SDK treats EXECUTED state changes as
147
+ * the terminal truth even when an earlier CANCELED event is present.
148
+ */
149
+ export const deriveProposalStateFromApi = (
150
+ formatted: ApiProposalFormatted,
151
+ apiProposal: ApiProposal,
152
+ now: number,
153
+ ): ProposalState => {
154
+ if (formatted.executed) return ProposalState.Executed;
155
+ if (formatted.canceled) return ProposalState.Canceled;
156
+ const hasQueued = apiProposal.stateChanges?.some(
157
+ (sc) => sc.state === "QUEUED",
158
+ );
159
+ if (hasQueued) return ProposalState.Queued;
160
+ if (now >= apiProposal.votingStartTime && now <= apiProposal.votingEndTime) {
161
+ return ProposalState.Active;
162
+ }
163
+ return ProposalState.Pending;
164
+ };
165
+
137
166
  /**
138
167
  * Parses and formats API proposal data
139
168
  */
@@ -255,8 +284,25 @@ export const getProposalsOnChainData = async (
255
284
 
256
285
  const legacyArtemisMaxId = await getLegacyArtemisMaxId(governanceEnvironment);
257
286
 
287
+ const nowSeconds = Math.floor(Date.now() / 1000);
288
+
258
289
  const onChainDataList = await Promise.all(
259
290
  apiProposals.map(async (p) => {
291
+ // Proposals from a different chain than the governance env (e.g. chainId=1
292
+ // Ethereum proposals fetched through the Moonbeam env) can't be read from
293
+ // this env's contracts — derive state from API events here so callers
294
+ // never see a misleading `state: 0` default for finalized proposals.
295
+ if (p.chainId !== governanceEnvironment.chainId) {
296
+ const formatted = formatApiProposalData(p);
297
+ return {
298
+ state: deriveProposalStateFromApi(formatted, p, nowSeconds),
299
+ proposalData: null,
300
+ eta: 0,
301
+ votesCollected: false,
302
+ quorum: 0n,
303
+ };
304
+ }
305
+
260
306
  const isMultichain = isMultichainAware(p, legacyArtemisMaxId);
261
307
 
262
308
  const governorContract = isMultichain
@@ -296,6 +342,10 @@ export const getProposalsOnChainData = async (
296
342
 
297
343
  const votesCollectedList = await Promise.all(
298
344
  apiProposals.map(async (apiProposal) => {
345
+ if (apiProposal.chainId !== governanceEnvironment.chainId) {
346
+ return false;
347
+ }
348
+
299
349
  const isMultichain = isMultichainAware(apiProposal, legacyArtemisMaxId);
300
350
 
301
351
  if (
@@ -3,8 +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 { Chain, Environment } from "../../../environments/index.js";
6
- import type { Proposal } from "../../../types/proposal.js";
7
- import { fetchProposal } from "../governor-api-client.js";
6
+ import { type Proposal, ProposalState } from "../../../types/proposal.js";
7
+ import {
8
+ type ApiProposal,
9
+ SUPPORTED_GOVERNOR_CHAIN_IDS,
10
+ fetchProposal,
11
+ isNotFoundError,
12
+ } from "../governor-api-client.js";
8
13
  import {
9
14
  appendProposalExtendedData,
10
15
  formatApiProposalData,
@@ -20,6 +25,11 @@ export type GetProposalParameters<
20
25
  network extends Chain | undefined,
21
26
  > = NetworkParameterType<environments, network> & {
22
27
  proposalId: number;
28
+ /**
29
+ * The chain the proposal lives on (1 = Ethereum multigov,
30
+ * 1284 = Moonbeam historical). When omitted, both are tried in turn.
31
+ */
32
+ chainId?: number;
23
33
  };
24
34
 
25
35
  export type GetProposalReturnType = Promise<Proposal | undefined>;
@@ -45,33 +55,41 @@ export async function getProposal<
45
55
  return undefined;
46
56
  }
47
57
 
48
- try {
49
- if (environment.chainId === moonbeam.id) {
50
- // Moonbeam: Use new Governor API
51
- return await getMoonbeamProposal(environment, proposalId);
52
- } else {
53
- // Moonriver: Use old Ponder approach
54
- return await getMoonriverProposal(environment, proposalId);
55
- }
56
- } catch (error) {
57
- console.error(
58
- `[getProposal] Error fetching proposal ${proposalId}:`,
59
- error,
60
- );
61
- return undefined;
58
+ if (environment.chainId === moonbeam.id) {
59
+ return getMoonbeamProposal(environment, proposalId, args.chainId);
62
60
  }
61
+ return getMoonriverProposal(environment, proposalId);
63
62
  }
64
63
 
65
64
  /**
66
- * Fetch a single proposal for Moonbeam using the new Governor API
65
+ * Fetch a single proposal for Moonbeam using the Governor API.
66
+ *
67
+ * When `chainId` is provided we hit only that chain. When omitted we try the
68
+ * supported chains in order (Ethereum first since that's where active multigov
69
+ * proposals live) and fall back on `NotFoundError`. Real outages (5xx, network
70
+ * errors) propagate so callers can distinguish "missing" from "broken".
67
71
  */
68
72
  async function getMoonbeamProposal(
69
73
  governanceEnvironment: Environment,
70
74
  proposalId: number,
75
+ chainId?: number,
71
76
  ): Promise<Proposal | undefined> {
72
- const apiProposalId = `${proposalId}`;
77
+ const tryChains = chainId ? [chainId] : SUPPORTED_GOVERNOR_CHAIN_IDS;
78
+
79
+ let apiProposal: ApiProposal | undefined;
80
+ for (const cid of tryChains) {
81
+ try {
82
+ apiProposal = await fetchProposal(governanceEnvironment, cid, proposalId);
83
+ break;
84
+ } catch (error) {
85
+ if (isNotFoundError(error)) continue;
86
+ throw error;
87
+ }
88
+ }
73
89
 
74
- const apiProposal = await fetchProposal(governanceEnvironment, apiProposalId);
90
+ if (!apiProposal) {
91
+ return undefined;
92
+ }
75
93
 
76
94
  const formattedData = formatApiProposalData(apiProposal);
77
95
  const onChainDataList = await getProposalsOnChainData(
@@ -79,29 +97,31 @@ async function getMoonbeamProposal(
79
97
  governanceEnvironment,
80
98
  );
81
99
  const onChainData = onChainDataList[0]!;
82
-
83
100
  const isMultichain = isMultichainProposal(apiProposal.targets);
84
101
 
85
- let proposalState = onChainData.state;
102
+ // For cross-chain proposals, onChainData.state is already API-derived inside
103
+ // getProposalsOnChainData, so the post-processing below acts as a no-op
104
+ // (votesCollected is false and the derived state is already terminal).
86
105
  const now = Math.floor(Date.now() / 1000);
106
+ let proposalState = onChainData.state;
87
107
 
88
108
  if (
89
- proposalState === 0 &&
109
+ proposalState === ProposalState.Pending &&
90
110
  now >= apiProposal.votingStartTime &&
91
111
  now <= apiProposal.votingEndTime
92
112
  ) {
93
- proposalState = 1; // Active
113
+ proposalState = ProposalState.Active;
94
114
  }
95
115
 
96
116
  if (formattedData.executed) {
97
- proposalState = 7; // ProposalState.Executed
117
+ proposalState = ProposalState.Executed;
98
118
  } else if (
99
119
  isMultichain &&
100
120
  onChainData.votesCollected &&
101
121
  now > apiProposal.votingEndTime &&
102
- proposalState < 5
122
+ proposalState < ProposalState.Queued
103
123
  ) {
104
- proposalState = 5; // ProposalState.Queued
124
+ proposalState = ProposalState.Queued;
105
125
  }
106
126
 
107
127
  const proposal: Proposal = {
@@ -4,8 +4,8 @@ import { Amount, getEnvironmentsFromArgs } from "../../../common/index.js";
4
4
  import type { OptionalNetworkParameterType } from "../../../common/types.js";
5
5
  import type { Chain, Environment } from "../../../environments/index.js";
6
6
  import * as logger from "../../../logger/console.js";
7
- import type { Proposal } from "../../../types/proposal.js";
8
- import { fetchAllProposals } from "../governor-api-client.js";
7
+ import { type Proposal, ProposalState } from "../../../types/proposal.js";
8
+ import { type ApiProposal, fetchAllProposals } from "../governor-api-client.js";
9
9
  import {
10
10
  appendProposalExtendedData,
11
11
  formatApiProposalData,
@@ -58,7 +58,14 @@ export async function getProposals<
58
58
  );
59
59
 
60
60
  const proposals = allProposals.flat();
61
- const sortedProposals = proposals.sort((a, b) => b.proposalId - a.proposalId);
61
+ // Newer multigov-ethereum proposals (chainId=1) and historical Moonbeam
62
+ // proposals (chainId=1284) restart their proposalId counters from 1, so IDs
63
+ // may collide across chains. Sort by proposalId desc with chainId as a
64
+ // stable tiebreaker (smaller chainId — Ethereum — wins).
65
+ const sortedProposals = proposals.sort((a, b) => {
66
+ if (b.proposalId !== a.proposalId) return b.proposalId - a.proposalId;
67
+ return a.chainId - b.chainId;
68
+ });
62
69
 
63
70
  logger.end(logId);
64
71
 
@@ -66,12 +73,36 @@ export async function getProposals<
66
73
  }
67
74
 
68
75
  /**
69
- * Fetch proposals for Moonbeam using the new Governor API
76
+ * Fetch proposals for Moonbeam using the Governor API.
77
+ *
78
+ * The same indexer DO serves two chains:
79
+ * - chainId=1 (Ethereum) — the active multigov contract
80
+ * - chainId=1284 (Moonbeam) — historical proposals
81
+ *
82
+ * Uses `Promise.allSettled` so a transient outage on one chain doesn't take
83
+ * down the other — partial results are preferred over a hard failure.
70
84
  */
71
85
  async function getMoonbeamProposals(
72
86
  governanceEnvironment: Environment,
73
87
  ): Promise<Proposal[]> {
74
- const apiProposals = await fetchAllProposals(governanceEnvironment);
88
+ const results = await Promise.allSettled([
89
+ fetchAllProposals(governanceEnvironment, { chainId: 1 }),
90
+ fetchAllProposals(governanceEnvironment, { chainId: 1284 }),
91
+ ]);
92
+
93
+ const chainsAttempted: ReadonlyArray<1 | 1284> = [1, 1284];
94
+ const apiProposals: ApiProposal[] = [];
95
+ results.forEach((result, index) => {
96
+ if (result.status === "fulfilled") {
97
+ apiProposals.push(...result.value);
98
+ } else {
99
+ console.warn(
100
+ `[getProposals] Failed to fetch proposals for chainId=${chainsAttempted[index]}; continuing with remaining chains.`,
101
+ result.reason,
102
+ );
103
+ }
104
+ });
105
+
75
106
  const onChainDataList = await getProposalsOnChainData(
76
107
  apiProposals,
77
108
  governanceEnvironment,
@@ -79,31 +110,34 @@ async function getMoonbeamProposals(
79
110
 
80
111
  const proposals: Proposal[] = apiProposals.map((apiProposal, index) => {
81
112
  const onChainData = onChainDataList[index]!;
82
-
83
113
  const formattedData = formatApiProposalData(apiProposal);
114
+ const isMultichain = isMultichainProposal(apiProposal.targets);
84
115
 
85
- let proposalState = onChainData.state;
116
+ // `onChainData.state` is already API-derived for cross-chain proposals
117
+ // (handled inside getProposalsOnChainData), so the post-processing below
118
+ // is a no-op for them — derived states already reflect executed/canceled/
119
+ // queued/active/pending, and votesCollected is false so the Queued branch
120
+ // never fires.
86
121
  const now = Math.floor(Date.now() / 1000);
122
+ let proposalState = onChainData.state;
87
123
 
88
124
  if (
89
- proposalState === 0 &&
125
+ proposalState === ProposalState.Pending &&
90
126
  now >= apiProposal.votingStartTime &&
91
127
  now <= apiProposal.votingEndTime
92
128
  ) {
93
- proposalState = 1; // Active
129
+ proposalState = ProposalState.Active;
94
130
  }
95
131
 
96
- const isMultichain = isMultichainProposal(apiProposal.targets);
97
-
98
132
  if (formattedData.executed) {
99
- proposalState = 7; // ProposalState.Executed
133
+ proposalState = ProposalState.Executed;
100
134
  } else if (
101
135
  isMultichain &&
102
136
  onChainData.votesCollected &&
103
137
  now > apiProposal.votingEndTime &&
104
- proposalState < 5
138
+ proposalState < ProposalState.Queued
105
139
  ) {
106
- proposalState = 5; // ProposalState.Queued
140
+ proposalState = ProposalState.Queued;
107
141
  }
108
142
 
109
143
  const proposal: Proposal = {
@@ -12,6 +12,7 @@ import {
12
12
  } from "../../../environments/utils/index.js";
13
13
  import type { MorphoUserReward } from "../../../types/morphoUserReward.js";
14
14
  import type { MorphoUserStakingReward } from "../../../types/morphoUserStakingReward.js";
15
+ import { getGovernanceTokenPriceFor } from "../../governance/getWellPrice.js";
15
16
 
16
17
  /**
17
18
  * Error thrown for any failure communicating with the Merkl API: non-ok HTTP
@@ -164,11 +165,17 @@ export async function getUserMorphoStakingRewardsData(params: {
164
165
  await Promise.all([
165
166
  viewsContract?.read.getAllMarketsInfo(),
166
167
  homeViewsContract?.read.getNativeTokenPrice(),
167
- homeViewsContract?.read.getGovernanceTokenPrice(),
168
+ getGovernanceTokenPriceFor(params.environment).catch((err) => {
169
+ params.environment.onError?.(err, {
170
+ source: "governance-token-price",
171
+ chainId: params.environment.chainId,
172
+ });
173
+ return 0n;
174
+ }),
168
175
  ]);
169
176
 
170
- const governanceTokenPrice = new Amount(governanceTokenPriceRaw || 0n, 18);
171
- const nativeTokenPrice = new Amount(nativeTokenPriceRaw || 0n, 18);
177
+ const governanceTokenPrice = new Amount(governanceTokenPriceRaw, 18);
178
+ const nativeTokenPrice = new Amount(nativeTokenPriceRaw ?? 0n, 18);
172
179
 
173
180
  let tokenPrices =
174
181
  allMarkets