@moonwell-fi/moonwell-sdk 0.13.1 → 0.15.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.
- package/CHANGELOG.md +16 -0
- package/_cjs/actions/core/getUserBalances.js +10 -9
- package/_cjs/actions/core/getUserBalances.js.map +1 -1
- package/_cjs/actions/governance/getDelegates.js.map +1 -1
- package/_cjs/actions/governance/getUserVoteReceipt.js +39 -25
- package/_cjs/actions/governance/getUserVoteReceipt.js.map +1 -1
- package/_cjs/actions/governance/getUserVotingPowers.js +14 -4
- package/_cjs/actions/governance/getUserVotingPowers.js.map +1 -1
- package/_cjs/actions/governance/governor-api-client.js +70 -33
- package/_cjs/actions/governance/governor-api-client.js.map +1 -1
- package/_cjs/actions/governance/proposals/common.js +29 -1
- package/_cjs/actions/governance/proposals/common.js.map +1 -1
- package/_cjs/actions/governance/proposals/getProposal.js +24 -17
- package/_cjs/actions/governance/proposals/getProposal.js.map +1 -1
- package/_cjs/actions/governance/proposals/getProposals.js +27 -9
- package/_cjs/actions/governance/proposals/getProposals.js.map +1 -1
- package/_cjs/environments/definitions/ethereum/contracts.js +2 -0
- package/_cjs/environments/definitions/ethereum/contracts.js.map +1 -1
- package/_cjs/errors/version.js +1 -1
- package/_esm/actions/core/getUserBalances.js +14 -9
- package/_esm/actions/core/getUserBalances.js.map +1 -1
- package/_esm/actions/governance/getDelegates.js +3 -2
- package/_esm/actions/governance/getDelegates.js.map +1 -1
- package/_esm/actions/governance/getUserVoteReceipt.js +48 -26
- package/_esm/actions/governance/getUserVoteReceipt.js.map +1 -1
- package/_esm/actions/governance/getUserVotingPowers.js +17 -4
- package/_esm/actions/governance/getUserVotingPowers.js.map +1 -1
- package/_esm/actions/governance/governor-api-client.js +87 -35
- package/_esm/actions/governance/governor-api-client.js.map +1 -1
- package/_esm/actions/governance/proposals/common.js +44 -1
- package/_esm/actions/governance/proposals/common.js.map +1 -1
- package/_esm/actions/governance/proposals/getProposal.js +36 -23
- package/_esm/actions/governance/proposals/getProposal.js.map +1 -1
- package/_esm/actions/governance/proposals/getProposals.js +44 -10
- package/_esm/actions/governance/proposals/getProposals.js.map +1 -1
- package/_esm/environments/definitions/ethereum/contracts.js +2 -1
- package/_esm/environments/definitions/ethereum/contracts.js.map +1 -1
- package/_esm/errors/version.js +1 -1
- package/_types/actions/core/getUserBalances.d.ts.map +1 -1
- package/_types/actions/governance/getDelegates.d.ts.map +1 -1
- package/_types/actions/governance/getUserVoteReceipt.d.ts +16 -0
- package/_types/actions/governance/getUserVoteReceipt.d.ts.map +1 -1
- package/_types/actions/governance/getUserVotingPowers.d.ts.map +1 -1
- package/_types/actions/governance/governor-api-client.d.ts +37 -12
- package/_types/actions/governance/governor-api-client.d.ts.map +1 -1
- package/_types/actions/governance/proposals/common.d.ts +14 -1
- package/_types/actions/governance/proposals/common.d.ts.map +1 -1
- package/_types/actions/governance/proposals/getProposal.d.ts +6 -1
- package/_types/actions/governance/proposals/getProposal.d.ts.map +1 -1
- package/_types/actions/governance/proposals/getProposals.d.ts +1 -1
- package/_types/actions/governance/proposals/getProposals.d.ts.map +1 -1
- package/_types/client/createMoonwellClient.d.ts +4 -0
- package/_types/client/createMoonwellClient.d.ts.map +1 -1
- package/_types/environments/definitions/ethereum/contracts.d.ts +2 -0
- package/_types/environments/definitions/ethereum/contracts.d.ts.map +1 -1
- package/_types/environments/index.d.ts +2 -0
- package/_types/environments/index.d.ts.map +1 -1
- package/_types/errors/version.d.ts +1 -1
- package/actions/core/getUserBalances.ts +20 -15
- package/actions/governance/getDelegates.ts +3 -2
- package/actions/governance/getUserVoteReceipt.ts +71 -31
- package/actions/governance/getUserVotingPowers.ts +20 -4
- package/actions/governance/governor-api-client.ts +136 -62
- package/actions/governance/proposals/common.ts +51 -1
- package/actions/governance/proposals/getProposal.ts +46 -26
- package/actions/governance/proposals/getProposals.ts +48 -14
- package/environments/definitions/ethereum/contracts.ts +2 -1
- package/errors/version.ts +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Address, zeroAddress } from "viem";
|
|
2
|
+
import { mainnet } from "viem/chains";
|
|
2
3
|
import type { MoonwellClient } from "../../client/createMoonwellClient.js";
|
|
3
4
|
import {
|
|
4
5
|
Amount,
|
|
@@ -9,7 +10,12 @@ import type { OptionalNetworkParameterType } from "../../common/types.js";
|
|
|
9
10
|
import type { Chain, GovernanceToken } from "../../environments/index.js";
|
|
10
11
|
import type { UserVotingPowers } from "../../types/userVotingPowers.js";
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
+
const warnedSkippedEnvs = new Set<string>();
|
|
14
|
+
|
|
15
|
+
// Ethereum's views contract is staking-only (exposes getUserStakingVotingPower
|
|
16
|
+
// but not the cross-source getUserVotingPower used here). Reading it on Eth
|
|
17
|
+
// would revert at runtime, so we skip those chains explicitly.
|
|
18
|
+
const STAKING_ONLY_VIEWS_CHAIN_IDS: ReadonlySet<number> = new Set([mainnet.id]);
|
|
13
19
|
|
|
14
20
|
export type GetUserVotingPowersParameters<
|
|
15
21
|
environments,
|
|
@@ -60,15 +66,25 @@ export async function getUserVotingPowers<
|
|
|
60
66
|
}
|
|
61
67
|
const views = env.contracts.views;
|
|
62
68
|
if (views === undefined) {
|
|
63
|
-
const key = `${env.chainId}:${governanceToken}`;
|
|
64
|
-
if (!
|
|
65
|
-
|
|
69
|
+
const key = `${env.chainId}:${governanceToken}:no-views`;
|
|
70
|
+
if (!warnedSkippedEnvs.has(key)) {
|
|
71
|
+
warnedSkippedEnvs.add(key);
|
|
66
72
|
console.warn(
|
|
67
73
|
`[moonwell-sdk] getUserVotingPowers: skipping chainId=${env.chainId} for governanceToken=${governanceToken} — environment holds the token but has no views contract.`,
|
|
68
74
|
);
|
|
69
75
|
}
|
|
70
76
|
return [];
|
|
71
77
|
}
|
|
78
|
+
if (STAKING_ONLY_VIEWS_CHAIN_IDS.has(env.chainId)) {
|
|
79
|
+
const key = `${env.chainId}:${governanceToken}:staking-only`;
|
|
80
|
+
if (!warnedSkippedEnvs.has(key)) {
|
|
81
|
+
warnedSkippedEnvs.add(key);
|
|
82
|
+
console.warn(
|
|
83
|
+
`[moonwell-sdk] getUserVotingPowers: skipping chainId=${env.chainId} for governanceToken=${governanceToken} — views contract is staking-only and does not expose getUserVotingPower.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
72
88
|
return [{ env, views }];
|
|
73
89
|
});
|
|
74
90
|
|
|
@@ -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
|
|
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
|
|
176
|
+
options: FetchProposalsOptions,
|
|
103
177
|
): Promise<PaginatedResponse<ApiProposal>> {
|
|
104
178
|
const baseUrl = getGovernorApiUrl(environment);
|
|
105
179
|
const params = new URLSearchParams();
|
|
106
180
|
|
|
107
|
-
|
|
108
|
-
if (options
|
|
109
|
-
if (options
|
|
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
|
|
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
|
-
|
|
225
|
+
chainId: number,
|
|
226
|
+
proposalId: number | string,
|
|
152
227
|
): Promise<ApiProposal> {
|
|
153
228
|
const baseUrl = getGovernorApiUrl(environment);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
`${baseUrl}/api/v1/governor/proposals/${
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
`${baseUrl}/api/v1/governor/proposals/${
|
|
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
|
-
|
|
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(
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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(
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
528
|
+
chainId: number,
|
|
529
|
+
proposalId: number | string,
|
|
451
530
|
voterAddress: string,
|
|
452
531
|
): Promise<ApiVoteReceipt[]> {
|
|
453
532
|
const baseUrl = getGovernorApiUrl(environment);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
`${baseUrl}/api/v1/governor/proposals/${
|
|
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
|
-
|
|
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
|
|
7
|
-
import {
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
109
|
+
proposalState === ProposalState.Pending &&
|
|
90
110
|
now >= apiProposal.votingStartTime &&
|
|
91
111
|
now <= apiProposal.votingEndTime
|
|
92
112
|
) {
|
|
93
|
-
proposalState =
|
|
113
|
+
proposalState = ProposalState.Active;
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
if (formattedData.executed) {
|
|
97
|
-
proposalState =
|
|
117
|
+
proposalState = ProposalState.Executed;
|
|
98
118
|
} else if (
|
|
99
119
|
isMultichain &&
|
|
100
120
|
onChainData.votesCollected &&
|
|
101
121
|
now > apiProposal.votingEndTime &&
|
|
102
|
-
proposalState <
|
|
122
|
+
proposalState < ProposalState.Queued
|
|
103
123
|
) {
|
|
104
|
-
proposalState =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 ===
|
|
125
|
+
proposalState === ProposalState.Pending &&
|
|
90
126
|
now >= apiProposal.votingStartTime &&
|
|
91
127
|
now <= apiProposal.votingEndTime
|
|
92
128
|
) {
|
|
93
|
-
proposalState =
|
|
129
|
+
proposalState = ProposalState.Active;
|
|
94
130
|
}
|
|
95
131
|
|
|
96
|
-
const isMultichain = isMultichainProposal(apiProposal.targets);
|
|
97
|
-
|
|
98
132
|
if (formattedData.executed) {
|
|
99
|
-
proposalState =
|
|
133
|
+
proposalState = ProposalState.Executed;
|
|
100
134
|
} else if (
|
|
101
135
|
isMultichain &&
|
|
102
136
|
onChainData.votesCollected &&
|
|
103
137
|
now > apiProposal.votingEndTime &&
|
|
104
|
-
proposalState <
|
|
138
|
+
proposalState < ProposalState.Queued
|
|
105
139
|
) {
|
|
106
|
-
proposalState =
|
|
140
|
+
proposalState = ProposalState.Queued;
|
|
107
141
|
}
|
|
108
142
|
|
|
109
143
|
const proposal: Proposal = {
|