@gzeoneth/gov-tracker 0.2.1-beta.b2eeb41 → 0.2.1-beta.c266765
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/README.md +41 -2
- package/dist/abis.d.ts.map +1 -1
- package/dist/abis.js +22 -1
- package/dist/abis.js.map +1 -1
- package/dist/cli/cli.js +206 -12
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/lib/cli.d.ts +9 -1
- package/dist/cli/lib/cli.d.ts.map +1 -1
- package/dist/cli/lib/cli.js +80 -4
- package/dist/cli/lib/cli.js.map +1 -1
- package/dist/data/bundled-cache.json +1155 -962
- package/dist/deduplication.d.ts +132 -0
- package/dist/deduplication.d.ts.map +1 -0
- package/dist/deduplication.js +270 -0
- package/dist/deduplication.js.map +1 -0
- package/dist/election.d.ts +141 -1
- package/dist/election.d.ts.map +1 -1
- package/dist/election.js +505 -88
- package/dist/election.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -3
- package/dist/index.js.map +1 -1
- package/dist/stages/builder.d.ts +4 -0
- package/dist/stages/builder.d.ts.map +1 -1
- package/dist/stages/builder.js +7 -0
- package/dist/stages/builder.js.map +1 -1
- package/dist/stages/voting.d.ts.map +1 -1
- package/dist/stages/voting.js +5 -4
- package/dist/stages/voting.js.map +1 -1
- package/dist/tracker/discovery.d.ts +39 -8
- package/dist/tracker/discovery.d.ts.map +1 -1
- package/dist/tracker/discovery.js +141 -12
- package/dist/tracker/discovery.js.map +1 -1
- package/dist/tracker.d.ts +24 -4
- package/dist/tracker.d.ts.map +1 -1
- package/dist/tracker.js +57 -8
- package/dist/tracker.js.map +1 -1
- package/dist/types/core.d.ts +1 -1
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/election.d.ts +88 -0
- package/dist/types/election.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/stages.d.ts +6 -0
- package/dist/types/stages.d.ts.map +1 -1
- package/dist/types/stages.js.map +1 -1
- package/dist/types/tracking.d.ts +27 -1
- package/dist/types/tracking.d.ts.map +1 -1
- package/dist/utils/timing.d.ts +13 -0
- package/dist/utils/timing.d.ts.map +1 -1
- package/dist/utils/timing.js +35 -0
- package/dist/utils/timing.js.map +1 -1
- package/package.json +1 -1
package/dist/election.js
CHANGED
|
@@ -13,8 +13,19 @@ exports.prepareElectionCreation = prepareElectionCreation;
|
|
|
13
13
|
exports.hasVettingPeriod = hasVettingPeriod;
|
|
14
14
|
exports.trackElectionProposal = trackElectionProposal;
|
|
15
15
|
exports.getElectionProposalId = getElectionProposalId;
|
|
16
|
+
exports.getMemberElectionProposalId = getMemberElectionProposalId;
|
|
16
17
|
exports.getElectionProposalParams = getElectionProposalParams;
|
|
17
18
|
exports.prepareMemberElectionTrigger = prepareMemberElectionTrigger;
|
|
19
|
+
exports.getMemberElectionProposalParams = getMemberElectionProposalParams;
|
|
20
|
+
exports.prepareMemberElectionExecution = prepareMemberElectionExecution;
|
|
21
|
+
exports.getElectionIndexForProposalId = getElectionIndexForProposalId;
|
|
22
|
+
exports.trackAllElections = trackAllElections;
|
|
23
|
+
exports.trackIncompleteElections = trackIncompleteElections;
|
|
24
|
+
exports.getContenders = getContenders;
|
|
25
|
+
exports.getNomineesWithVotes = getNomineesWithVotes;
|
|
26
|
+
exports.getExcludedNominees = getExcludedNominees;
|
|
27
|
+
exports.getNomineeElectionDetails = getNomineeElectionDetails;
|
|
28
|
+
exports.getMemberElectionDetails = getMemberElectionDetails;
|
|
18
29
|
const ethers_1 = require("ethers");
|
|
19
30
|
const constants_1 = require("./constants");
|
|
20
31
|
const rpc_utils_1 = require("./utils/rpc-utils");
|
|
@@ -32,6 +43,19 @@ function getNomineeGovernor(address = constants_1.ADDRESSES.ELECTION_NOMINEE_GOV
|
|
|
32
43
|
function getMemberGovernor(address = constants_1.ADDRESSES.ELECTION_MEMBER_GOVERNOR, provider) {
|
|
33
44
|
return new ethers_1.ethers.Contract(address, abis_1.MEMBER_ELECTION_GOVERNOR_ABI, provider);
|
|
34
45
|
}
|
|
46
|
+
/** Get block range for log queries based on proposal snapshot */
|
|
47
|
+
async function getLogQueryBlockRange(governor, proposalId, provider, offsetFromSnapshot = 1000, fallbackRange = 100000) {
|
|
48
|
+
const toBlock = await provider.getBlockNumber();
|
|
49
|
+
let fromBlock;
|
|
50
|
+
try {
|
|
51
|
+
const snapshot = await governor.proposalSnapshot(proposalId);
|
|
52
|
+
fromBlock = Math.max(0, snapshot.toNumber() - offsetFromSnapshot);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
fromBlock = Math.max(0, toBlock - fallbackRange);
|
|
56
|
+
}
|
|
57
|
+
return { fromBlock, toBlock };
|
|
58
|
+
}
|
|
35
59
|
// Core Functions
|
|
36
60
|
/**
|
|
37
61
|
* Check the status of the Security Council election
|
|
@@ -168,26 +192,47 @@ function formatDuration(seconds) {
|
|
|
168
192
|
parts.push(`${secs}s`);
|
|
169
193
|
return parts.join(" ") || "0s";
|
|
170
194
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
195
|
+
const PROPOSAL_STATES = [
|
|
196
|
+
"Pending",
|
|
197
|
+
"Active",
|
|
198
|
+
"Canceled",
|
|
199
|
+
"Defeated",
|
|
200
|
+
"Succeeded",
|
|
201
|
+
"Queued",
|
|
202
|
+
"Expired",
|
|
203
|
+
"Executed",
|
|
204
|
+
];
|
|
174
205
|
function stateToString(state) {
|
|
175
|
-
const
|
|
176
|
-
"Pending",
|
|
177
|
-
"Active",
|
|
178
|
-
"Canceled",
|
|
179
|
-
"Defeated",
|
|
180
|
-
"Succeeded",
|
|
181
|
-
"Queued",
|
|
182
|
-
"Expired",
|
|
183
|
-
"Executed",
|
|
184
|
-
];
|
|
185
|
-
const result = states[state];
|
|
206
|
+
const result = PROPOSAL_STATES[state];
|
|
186
207
|
if (!result) {
|
|
187
208
|
throw new Error(`Unknown proposal state number: ${state}`);
|
|
188
209
|
}
|
|
189
210
|
return result;
|
|
190
211
|
}
|
|
212
|
+
function determineElectionPhase(nomineeProposalState, memberProposalId, memberProposalState, isInVettingPeriod) {
|
|
213
|
+
if (memberProposalState === "Executed") {
|
|
214
|
+
return "COMPLETED";
|
|
215
|
+
}
|
|
216
|
+
if (memberProposalId) {
|
|
217
|
+
if (memberProposalState === "Succeeded" || memberProposalState === "Queued") {
|
|
218
|
+
return "PENDING_EXECUTION";
|
|
219
|
+
}
|
|
220
|
+
return "MEMBER_ELECTION";
|
|
221
|
+
}
|
|
222
|
+
if (nomineeProposalState === "Executed") {
|
|
223
|
+
return "PENDING_EXECUTION";
|
|
224
|
+
}
|
|
225
|
+
if (isInVettingPeriod) {
|
|
226
|
+
return "VETTING_PERIOD";
|
|
227
|
+
}
|
|
228
|
+
if (nomineeProposalState === "Active" || nomineeProposalState === "Pending") {
|
|
229
|
+
return "NOMINEE_SELECTION";
|
|
230
|
+
}
|
|
231
|
+
if (nomineeProposalState === "Succeeded") {
|
|
232
|
+
return "PENDING_EXECUTION";
|
|
233
|
+
}
|
|
234
|
+
return "NOT_STARTED";
|
|
235
|
+
}
|
|
191
236
|
// Election Proposal Tracking
|
|
192
237
|
/**
|
|
193
238
|
* Track the status of a Security Council election by its index
|
|
@@ -239,6 +284,7 @@ async function trackElectionProposal(electionIndex, l2Provider, _l1Provider, opt
|
|
|
239
284
|
vettingDeadline: null,
|
|
240
285
|
isInVettingPeriod: false,
|
|
241
286
|
canProceedToMemberPhase: false,
|
|
287
|
+
canExecuteMember: false,
|
|
242
288
|
};
|
|
243
289
|
}
|
|
244
290
|
// Get nominee proposal state
|
|
@@ -260,51 +306,27 @@ async function trackElectionProposal(electionIndex, l2Provider, _l1Provider, opt
|
|
|
260
306
|
catch {
|
|
261
307
|
// May fail if no nominees yet
|
|
262
308
|
}
|
|
263
|
-
// Check for member proposal
|
|
309
|
+
// Check for member proposal using computed proposal ID (same scheme as nominee governor)
|
|
264
310
|
let memberProposalId = null;
|
|
265
311
|
let memberProposalState = null;
|
|
312
|
+
// Compute member proposal ID using getProposeArgs + hashProposal (same as nominee)
|
|
313
|
+
const computedMemberProposalId = await computeElectionProposalId(electionIndex, memberGovernor);
|
|
266
314
|
try {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
315
|
+
// Don't use queryWithRetry - we expect this to revert for non-existent proposals
|
|
316
|
+
const memberState = await memberGovernor.state(computedMemberProposalId);
|
|
317
|
+
// If we get here without reverting, the proposal exists
|
|
318
|
+
memberProposalId = computedMemberProposalId;
|
|
319
|
+
memberProposalState = stateToString(memberState);
|
|
273
320
|
}
|
|
274
321
|
catch {
|
|
275
|
-
// Member election not yet created
|
|
276
|
-
}
|
|
277
|
-
// Determine phase
|
|
278
|
-
let phase;
|
|
279
|
-
if (memberProposalState === "Executed") {
|
|
280
|
-
phase = "COMPLETED";
|
|
281
|
-
}
|
|
282
|
-
else if (memberProposalId) {
|
|
283
|
-
if (memberProposalState === "Succeeded" || memberProposalState === "Queued") {
|
|
284
|
-
phase = "PENDING_EXECUTION";
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
phase = "MEMBER_ELECTION";
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
else if (isInVettingPeriod) {
|
|
291
|
-
phase = "VETTING_PERIOD";
|
|
292
|
-
}
|
|
293
|
-
else if (nomineeProposalState === "Active" || nomineeProposalState === "Pending") {
|
|
294
|
-
phase = "NOMINEE_SELECTION";
|
|
295
|
-
}
|
|
296
|
-
else if (nomineeProposalState === "Succeeded") {
|
|
297
|
-
// Past vetting, waiting for member election creation
|
|
298
|
-
phase = "PENDING_EXECUTION";
|
|
322
|
+
// Member election not yet created (state() reverts for non-existent proposals)
|
|
299
323
|
}
|
|
300
|
-
|
|
301
|
-
phase = "NOT_STARTED";
|
|
302
|
-
}
|
|
303
|
-
// Can proceed if vetting ended and has enough compliant nominees
|
|
324
|
+
const phase = determineElectionPhase(nomineeProposalState, memberProposalId, memberProposalState, isInVettingPeriod);
|
|
304
325
|
const canProceedToMemberPhase = nomineeProposalState === "Succeeded" &&
|
|
305
326
|
!isInVettingPeriod &&
|
|
306
327
|
compliantNomineeCount >= constants_1.TIMING.SECURITY_COUNCIL_TARGET_NOMINEES &&
|
|
307
328
|
!memberProposalId;
|
|
329
|
+
const canExecuteMember = memberProposalState === "Succeeded";
|
|
308
330
|
return {
|
|
309
331
|
electionIndex,
|
|
310
332
|
phase,
|
|
@@ -318,21 +340,20 @@ async function trackElectionProposal(electionIndex, l2Provider, _l1Provider, opt
|
|
|
318
340
|
vettingDeadline,
|
|
319
341
|
isInVettingPeriod,
|
|
320
342
|
canProceedToMemberPhase,
|
|
343
|
+
canExecuteMember,
|
|
321
344
|
};
|
|
322
345
|
}
|
|
323
346
|
/**
|
|
324
|
-
* Get the proposal ID for a given election index
|
|
347
|
+
* Get the proposal ID for a given election index from any election governor
|
|
325
348
|
*
|
|
326
349
|
* Uses getProposeArgs to get proposal parameters and hashProposal to calculate the proposal ID.
|
|
350
|
+
* Both nominee and member governors use the same proposal ID scheme via ElectionGovernor base.
|
|
327
351
|
*
|
|
328
352
|
* @param electionIndex - Election index
|
|
329
|
-
* @param
|
|
330
|
-
* @
|
|
331
|
-
* @returns Proposal ID or null if election not yet created
|
|
353
|
+
* @param governor - Election governor contract instance
|
|
354
|
+
* @returns Proposal ID as string
|
|
332
355
|
*/
|
|
333
|
-
async function
|
|
334
|
-
const governor = getNomineeGovernor(nomineeGovernorAddress, provider);
|
|
335
|
-
// Get proposal arguments for this election
|
|
356
|
+
async function computeElectionProposalId(electionIndex, governor) {
|
|
336
357
|
// getProposeArgs returns: [targets, values, calldatas, description]
|
|
337
358
|
const proposeArgs = (await (0, rpc_utils_1.queryWithRetry)(() => governor.getProposeArgs(electionIndex)));
|
|
338
359
|
const [targets, values, calldatas, description] = proposeArgs;
|
|
@@ -340,55 +361,71 @@ async function getElectionProposalId(electionIndex, provider, nomineeGovernorAdd
|
|
|
340
361
|
const descriptionHash = (0, salt_computation_1.saltFromDescription)(description);
|
|
341
362
|
// Calculate proposal ID using hashProposal
|
|
342
363
|
const proposalId = await (0, rpc_utils_1.queryWithRetry)(() => governor.hashProposal(targets, values, calldatas, descriptionHash));
|
|
343
|
-
// Convert bytes32 to decimal string
|
|
344
364
|
return ethers_1.BigNumber.from(proposalId).toString();
|
|
345
365
|
}
|
|
346
|
-
// Member Election Trigger Functions
|
|
347
366
|
/**
|
|
348
|
-
* Get the proposal
|
|
367
|
+
* Get the proposal ID for a given election index
|
|
349
368
|
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
369
|
+
* Uses getProposeArgs to get proposal parameters and hashProposal to calculate the proposal ID.
|
|
370
|
+
* Verifies the proposal exists by checking state() - returns null if proposal doesn't exist.
|
|
352
371
|
*
|
|
353
372
|
* @param electionIndex - Election index
|
|
354
373
|
* @param provider - L2 provider
|
|
355
374
|
* @param nomineeGovernorAddress - Optional governor address override
|
|
356
|
-
* @returns
|
|
375
|
+
* @returns Proposal ID or null if election not yet created
|
|
357
376
|
*/
|
|
358
|
-
async function
|
|
359
|
-
|
|
360
|
-
const proposalId = await
|
|
361
|
-
|
|
362
|
-
|
|
377
|
+
async function getElectionProposalId(electionIndex, provider, nomineeGovernorAddress = constants_1.ADDRESSES.ELECTION_NOMINEE_GOVERNOR) {
|
|
378
|
+
const governor = getNomineeGovernor(nomineeGovernorAddress, provider);
|
|
379
|
+
const proposalId = await computeElectionProposalId(electionIndex, governor);
|
|
380
|
+
// Verify the proposal exists by checking state() - reverts for non-existent proposals
|
|
381
|
+
try {
|
|
382
|
+
await governor.state(proposalId);
|
|
383
|
+
return proposalId;
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// Proposal doesn't exist (state() reverts for unknown proposal IDs)
|
|
363
387
|
return null;
|
|
364
388
|
}
|
|
365
|
-
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get the member election proposal ID for a given election index
|
|
392
|
+
*
|
|
393
|
+
* @param electionIndex - Election index
|
|
394
|
+
* @param provider - L2 provider
|
|
395
|
+
* @param memberGovernorAddress - Optional governor address override
|
|
396
|
+
* @returns Member proposal ID as string
|
|
397
|
+
*/
|
|
398
|
+
async function getMemberElectionProposalId(electionIndex, provider, memberGovernorAddress = constants_1.ADDRESSES.ELECTION_MEMBER_GOVERNOR) {
|
|
399
|
+
const governor = getMemberGovernor(memberGovernorAddress, provider);
|
|
400
|
+
return computeElectionProposalId(electionIndex, governor);
|
|
401
|
+
}
|
|
402
|
+
// Member Election Trigger Functions
|
|
403
|
+
/**
|
|
404
|
+
* Search for ProposalCreated event and extract proposal parameters
|
|
405
|
+
*
|
|
406
|
+
* Common helper used by both nominee and member election param lookups.
|
|
407
|
+
*/
|
|
408
|
+
async function findProposalCreatedParams(proposalId, governorAddress, governor, provider) {
|
|
366
409
|
const topic = abis_1.proposalCreatedInterface.getEventTopic("ProposalCreated");
|
|
367
|
-
// Get proposal snapshot to narrow search range
|
|
368
|
-
const governor = getNomineeGovernor(nomineeGovernorAddress, provider);
|
|
369
410
|
let startBlock;
|
|
370
411
|
try {
|
|
371
412
|
const snapshot = await governor.proposalSnapshot(proposalId);
|
|
372
|
-
// Search from ~1000 blocks before snapshot (proposal is created before snapshot)
|
|
373
413
|
startBlock = Math.max(0, snapshot.toNumber() - 1000);
|
|
374
414
|
}
|
|
375
415
|
catch {
|
|
376
|
-
// Fallback: search last 10000 blocks
|
|
377
416
|
const currentBlock = await provider.getBlockNumber();
|
|
378
417
|
startBlock = Math.max(0, currentBlock - 10000);
|
|
379
418
|
}
|
|
380
419
|
const currentBlock = await provider.getBlockNumber();
|
|
381
420
|
const logs = await (0, rpc_utils_1.queryWithRetry)(() => provider.getLogs({
|
|
382
|
-
address:
|
|
421
|
+
address: governorAddress,
|
|
383
422
|
topics: [topic],
|
|
384
423
|
fromBlock: startBlock,
|
|
385
424
|
toBlock: currentBlock,
|
|
386
425
|
}));
|
|
387
|
-
// Find the log matching our proposal ID
|
|
388
426
|
for (const eventLog of logs) {
|
|
389
427
|
try {
|
|
390
428
|
const parsed = abis_1.proposalCreatedInterface.parseLog(eventLog);
|
|
391
|
-
// Cast through unknown required due to ethers' Result type structure
|
|
392
429
|
const args = parsed.args;
|
|
393
430
|
if (args.proposalId.toString() === proposalId) {
|
|
394
431
|
log("Found ProposalCreated event for proposal %s", proposalId);
|
|
@@ -408,6 +445,46 @@ async function getElectionProposalParams(electionIndex, provider, nomineeGoverno
|
|
|
408
445
|
log("ProposalCreated event not found for proposal %s", proposalId);
|
|
409
446
|
return null;
|
|
410
447
|
}
|
|
448
|
+
/**
|
|
449
|
+
* Build a prepared execute() transaction from proposal params
|
|
450
|
+
*/
|
|
451
|
+
function buildExecuteTransaction(params, governorAddress, description) {
|
|
452
|
+
const calldata = abis_1.governorInterface.encodeFunctionData("execute", [
|
|
453
|
+
params.targets,
|
|
454
|
+
params.values,
|
|
455
|
+
params.calldatas,
|
|
456
|
+
params.descriptionHash,
|
|
457
|
+
]);
|
|
458
|
+
return {
|
|
459
|
+
to: governorAddress,
|
|
460
|
+
data: calldata,
|
|
461
|
+
value: "0",
|
|
462
|
+
chain: "arb1",
|
|
463
|
+
chainId: 42161,
|
|
464
|
+
description,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Get the proposal parameters for an election proposal
|
|
469
|
+
*
|
|
470
|
+
* Searches for the ProposalCreated event to extract targets, values, calldatas,
|
|
471
|
+
* and description needed for execute() call.
|
|
472
|
+
*
|
|
473
|
+
* @param electionIndex - Election index
|
|
474
|
+
* @param provider - L2 provider
|
|
475
|
+
* @param nomineeGovernorAddress - Optional governor address override
|
|
476
|
+
* @returns Election proposal params or null if not found
|
|
477
|
+
*/
|
|
478
|
+
async function getElectionProposalParams(electionIndex, provider, nomineeGovernorAddress = constants_1.ADDRESSES.ELECTION_NOMINEE_GOVERNOR) {
|
|
479
|
+
log("getElectionProposalParams for index %d", electionIndex);
|
|
480
|
+
const proposalId = await getElectionProposalId(electionIndex, provider, nomineeGovernorAddress);
|
|
481
|
+
if (!proposalId) {
|
|
482
|
+
log("No proposal ID found for election %d", electionIndex);
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const governor = getNomineeGovernor(nomineeGovernorAddress, provider);
|
|
486
|
+
return findProposalCreatedParams(proposalId, nomineeGovernorAddress, governor, provider);
|
|
487
|
+
}
|
|
411
488
|
/**
|
|
412
489
|
* Prepare a transaction to trigger member election creation
|
|
413
490
|
*
|
|
@@ -442,26 +519,366 @@ async function prepareMemberElectionTrigger(electionStatus, provider, nomineeGov
|
|
|
442
519
|
log("Cannot proceed to member phase - not ready");
|
|
443
520
|
return null;
|
|
444
521
|
}
|
|
445
|
-
// Get proposal params
|
|
446
522
|
const params = await getElectionProposalParams(electionStatus.electionIndex, provider, nomineeGovernorAddress);
|
|
447
523
|
if (!params) {
|
|
448
524
|
log("Could not find proposal params for election %d", electionStatus.electionIndex);
|
|
449
525
|
return null;
|
|
450
526
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
527
|
+
return buildExecuteTransaction(params, nomineeGovernorAddress, `execute() on NomineeElectionGovernor to trigger member election #${electionStatus.electionIndex}`);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Get proposal parameters for a member election
|
|
531
|
+
*
|
|
532
|
+
* Retrieves the targets, values, calldatas, and description hash needed to
|
|
533
|
+
* execute a member election proposal.
|
|
534
|
+
*
|
|
535
|
+
* @param electionIndex - Election index
|
|
536
|
+
* @param provider - L2 provider
|
|
537
|
+
* @param memberGovernorAddress - Optional governor address override
|
|
538
|
+
* @returns Proposal parameters or null if not found
|
|
539
|
+
*/
|
|
540
|
+
async function getMemberElectionProposalParams(electionIndex, provider, memberGovernorAddress = constants_1.ADDRESSES.ELECTION_MEMBER_GOVERNOR) {
|
|
541
|
+
log("getMemberElectionProposalParams for index %d", electionIndex);
|
|
542
|
+
const memberGovernor = getMemberGovernor(memberGovernorAddress, provider);
|
|
543
|
+
// Compute proposal ID using getProposeArgs + hashProposal
|
|
544
|
+
const memberProposalId = await computeElectionProposalId(electionIndex, memberGovernor);
|
|
545
|
+
// Verify proposal exists by checking state
|
|
546
|
+
try {
|
|
547
|
+
await (0, rpc_utils_1.queryWithRetry)(() => memberGovernor.state(memberProposalId));
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
log("No member proposal found for election %d", electionIndex);
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
return findProposalCreatedParams(memberProposalId, memberGovernorAddress, memberGovernor, provider);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Prepare a transaction to execute member election result
|
|
557
|
+
*
|
|
558
|
+
* After member voting succeeds, calling execute() on the MemberElectionGovernor
|
|
559
|
+
* installs the new Security Council members.
|
|
560
|
+
*
|
|
561
|
+
* @param electionStatus - Status from trackElectionProposal (must have canExecuteMember=true)
|
|
562
|
+
* @param provider - L2 provider
|
|
563
|
+
* @param memberGovernorAddress - Optional governor address override
|
|
564
|
+
* @returns Prepared transaction or null if not ready
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* ```typescript
|
|
568
|
+
* const status = await trackElectionProposal(5, l2Provider, l1Provider);
|
|
569
|
+
*
|
|
570
|
+
* if (status.canExecuteMember) {
|
|
571
|
+
* const prepared = await prepareMemberElectionExecution(status, l2Provider);
|
|
572
|
+
* if (prepared) {
|
|
573
|
+
* const tx = await signer.sendTransaction({
|
|
574
|
+
* to: prepared.to,
|
|
575
|
+
* data: prepared.data,
|
|
576
|
+
* });
|
|
577
|
+
* await tx.wait();
|
|
578
|
+
* console.log("New Security Council members installed!");
|
|
579
|
+
* }
|
|
580
|
+
* }
|
|
581
|
+
* ```
|
|
582
|
+
*/
|
|
583
|
+
async function prepareMemberElectionExecution(electionStatus, provider, memberGovernorAddress = constants_1.ADDRESSES.ELECTION_MEMBER_GOVERNOR) {
|
|
584
|
+
log("prepareMemberElectionExecution for election %d", electionStatus.electionIndex);
|
|
585
|
+
if (!electionStatus.canExecuteMember) {
|
|
586
|
+
log("Cannot execute member election - not ready");
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
const params = await getMemberElectionProposalParams(electionStatus.electionIndex, provider, memberGovernorAddress);
|
|
590
|
+
if (!params) {
|
|
591
|
+
log("Could not find proposal params for member election %d", electionStatus.electionIndex);
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
return buildExecuteTransaction(params, memberGovernorAddress, `execute() on MemberElectionGovernor to install new Security Council members for election #${electionStatus.electionIndex}`);
|
|
595
|
+
}
|
|
596
|
+
// Election Discovery & Batch Tracking
|
|
597
|
+
/**
|
|
598
|
+
* Find the election index for a given proposal ID
|
|
599
|
+
*
|
|
600
|
+
* Searches through elections to find which one contains the given proposal ID
|
|
601
|
+
* (either as nominee or member proposal).
|
|
602
|
+
*
|
|
603
|
+
* @param proposalId - The proposal ID to find
|
|
604
|
+
* @param l2Provider - L2 provider
|
|
605
|
+
* @param l1Provider - L1 provider (needed for trackElectionProposal)
|
|
606
|
+
* @returns Election index or null if not found
|
|
607
|
+
*/
|
|
608
|
+
async function getElectionIndexForProposalId(proposalId, l2Provider, l1Provider) {
|
|
609
|
+
log("getElectionIndexForProposalId: searching for proposal %s", proposalId);
|
|
610
|
+
// Get current election count
|
|
611
|
+
const status = await checkElectionStatus(l2Provider, l1Provider);
|
|
612
|
+
const electionCount = status.electionCount;
|
|
613
|
+
// Search through elections (most recent first for efficiency)
|
|
614
|
+
for (let i = electionCount - 1; i >= 0; i--) {
|
|
615
|
+
try {
|
|
616
|
+
const electionStatus = await trackElectionProposal(i, l2Provider, l1Provider);
|
|
617
|
+
if (electionStatus.nomineeProposalId === proposalId) {
|
|
618
|
+
log("Found proposal %s as nominee proposal for election %d", proposalId, i);
|
|
619
|
+
return i;
|
|
620
|
+
}
|
|
621
|
+
if (electionStatus.memberProposalId === proposalId) {
|
|
622
|
+
log("Found proposal %s as member proposal for election %d", proposalId, i);
|
|
623
|
+
return i;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
// Skip elections that fail to track
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
log("Proposal %s not found in any election", proposalId);
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Track all active elections (not yet completed)
|
|
636
|
+
*
|
|
637
|
+
* Returns election statuses for all elections that are still in progress.
|
|
638
|
+
*
|
|
639
|
+
* @param l2Provider - L2 provider
|
|
640
|
+
* @param l1Provider - L1 provider
|
|
641
|
+
* @returns Array of election statuses for active elections
|
|
642
|
+
*/
|
|
643
|
+
async function trackAllElections(l2Provider, l1Provider) {
|
|
644
|
+
log("trackAllElections: fetching all active elections");
|
|
645
|
+
const status = await checkElectionStatus(l2Provider, l1Provider);
|
|
646
|
+
const electionCount = status.electionCount;
|
|
647
|
+
const results = [];
|
|
648
|
+
// Track all elections (including completed for history)
|
|
649
|
+
for (let i = 0; i < electionCount; i++) {
|
|
650
|
+
try {
|
|
651
|
+
const electionStatus = await trackElectionProposal(i, l2Provider, l1Provider);
|
|
652
|
+
results.push(electionStatus);
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
log("Failed to track election %d: %s", i, err);
|
|
656
|
+
// Skip failed elections
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
log("Tracked %d elections", results.length);
|
|
660
|
+
return results;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Track only incomplete elections (not yet completed)
|
|
664
|
+
*
|
|
665
|
+
* Returns election statuses for elections that are still in progress.
|
|
666
|
+
* Completed elections are excluded.
|
|
667
|
+
*
|
|
668
|
+
* @param l2Provider - L2 provider
|
|
669
|
+
* @param l1Provider - L1 provider
|
|
670
|
+
* @returns Array of election statuses for incomplete elections
|
|
671
|
+
*/
|
|
672
|
+
async function trackIncompleteElections(l2Provider, l1Provider) {
|
|
673
|
+
const all = await trackAllElections(l2Provider, l1Provider);
|
|
674
|
+
return all.filter((e) => e.phase !== "COMPLETED");
|
|
675
|
+
}
|
|
676
|
+
// ============================================================================
|
|
677
|
+
// Detailed Election Tracking
|
|
678
|
+
// ============================================================================
|
|
679
|
+
/**
|
|
680
|
+
* Get all contenders who registered for a nominee election
|
|
681
|
+
*
|
|
682
|
+
* Fetches ContenderAdded events to build the list of registered contenders.
|
|
683
|
+
*
|
|
684
|
+
* @param proposalId - Nominee election proposal ID
|
|
685
|
+
* @param provider - L2 provider
|
|
686
|
+
* @param nomineeGovernorAddress - Optional governor address override
|
|
687
|
+
* @returns Array of contenders with registration info
|
|
688
|
+
*/
|
|
689
|
+
async function getContenders(proposalId, provider, nomineeGovernorAddress = constants_1.ADDRESSES.ELECTION_NOMINEE_GOVERNOR) {
|
|
690
|
+
log("getContenders for proposal %s", proposalId);
|
|
691
|
+
const governor = getNomineeGovernor(nomineeGovernorAddress, provider);
|
|
692
|
+
const iface = new ethers_1.ethers.utils.Interface(abis_1.NOMINEE_ELECTION_GOVERNOR_ABI);
|
|
693
|
+
const { fromBlock, toBlock } = await getLogQueryBlockRange(governor, proposalId, provider);
|
|
694
|
+
const logs = await (0, rpc_utils_1.queryWithRetry)(() => provider.getLogs({
|
|
695
|
+
address: nomineeGovernorAddress,
|
|
696
|
+
topics: [
|
|
697
|
+
iface.getEventTopic("ContenderAdded"),
|
|
698
|
+
ethers_1.ethers.utils.hexZeroPad(ethers_1.BigNumber.from(proposalId).toHexString(), 32),
|
|
699
|
+
],
|
|
700
|
+
fromBlock,
|
|
701
|
+
toBlock,
|
|
702
|
+
}));
|
|
703
|
+
const contenders = logs.flatMap((eventLog) => {
|
|
704
|
+
try {
|
|
705
|
+
const parsed = iface.parseLog(eventLog);
|
|
706
|
+
return [
|
|
707
|
+
{
|
|
708
|
+
address: parsed.args.contender,
|
|
709
|
+
registeredAtBlock: eventLog.blockNumber,
|
|
710
|
+
registrationTxHash: eventLog.transactionHash,
|
|
711
|
+
},
|
|
712
|
+
];
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
log("Found %d contenders for proposal %s", contenders.length, proposalId);
|
|
719
|
+
return contenders;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Get all nominees for a nominee election with their vote counts
|
|
723
|
+
*
|
|
724
|
+
* Fetches nominee list from contract and enriches with vote data.
|
|
725
|
+
*
|
|
726
|
+
* @param proposalId - Nominee election proposal ID
|
|
727
|
+
* @param provider - L2 provider
|
|
728
|
+
* @param nomineeGovernorAddress - Optional governor address override
|
|
729
|
+
* @returns Array of nominees with vote and exclusion data
|
|
730
|
+
*/
|
|
731
|
+
async function getNomineesWithVotes(proposalId, provider, nomineeGovernorAddress = constants_1.ADDRESSES.ELECTION_NOMINEE_GOVERNOR) {
|
|
732
|
+
log("getNomineesWithVotes for proposal %s", proposalId);
|
|
733
|
+
const governor = getNomineeGovernor(nomineeGovernorAddress, provider);
|
|
734
|
+
const nomineeAddresses = await (0, rpc_utils_1.queryWithRetry)(() => governor.nominees(proposalId));
|
|
735
|
+
const nominees = await Promise.all(nomineeAddresses.map(async (addr) => {
|
|
736
|
+
const [votesReceived, isExcluded] = await Promise.all([
|
|
737
|
+
(0, rpc_utils_1.queryWithRetry)(() => governor.votesReceived(proposalId, addr)),
|
|
738
|
+
(0, rpc_utils_1.queryWithRetry)(() => governor.isExcluded(proposalId, addr)),
|
|
739
|
+
]);
|
|
740
|
+
return { address: addr, votesReceived, isExcluded };
|
|
741
|
+
}));
|
|
742
|
+
log("Found %d nominees for proposal %s", nominees.length, proposalId);
|
|
743
|
+
return nominees;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Get excluded nominees with exclusion details
|
|
747
|
+
*
|
|
748
|
+
* Fetches NomineeExcluded events to get exclusion information.
|
|
749
|
+
*
|
|
750
|
+
* @param proposalId - Nominee election proposal ID
|
|
751
|
+
* @param provider - L2 provider
|
|
752
|
+
* @param nomineeGovernorAddress - Optional governor address override
|
|
753
|
+
* @returns Array of excluded nominees with exclusion tx info
|
|
754
|
+
*/
|
|
755
|
+
async function getExcludedNominees(proposalId, provider, nomineeGovernorAddress = constants_1.ADDRESSES.ELECTION_NOMINEE_GOVERNOR) {
|
|
756
|
+
log("getExcludedNominees for proposal %s", proposalId);
|
|
757
|
+
const governor = getNomineeGovernor(nomineeGovernorAddress, provider);
|
|
758
|
+
const iface = new ethers_1.ethers.utils.Interface(abis_1.NOMINEE_ELECTION_GOVERNOR_ABI);
|
|
759
|
+
const { fromBlock, toBlock } = await getLogQueryBlockRange(governor, proposalId, provider, 0);
|
|
760
|
+
const logs = await (0, rpc_utils_1.queryWithRetry)(() => provider.getLogs({
|
|
761
|
+
address: nomineeGovernorAddress,
|
|
762
|
+
topics: [
|
|
763
|
+
iface.getEventTopic("NomineeExcluded"),
|
|
764
|
+
ethers_1.ethers.utils.hexZeroPad(ethers_1.BigNumber.from(proposalId).toHexString(), 32),
|
|
765
|
+
],
|
|
766
|
+
fromBlock,
|
|
767
|
+
toBlock,
|
|
768
|
+
}));
|
|
769
|
+
const parsedLogs = logs.flatMap((eventLog) => {
|
|
770
|
+
try {
|
|
771
|
+
const parsed = iface.parseLog(eventLog);
|
|
772
|
+
return [{ eventLog, nominee: parsed.args.nominee }];
|
|
773
|
+
}
|
|
774
|
+
catch {
|
|
775
|
+
return [];
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
const excluded = await Promise.all(parsedLogs.map(async ({ eventLog, nominee }) => {
|
|
779
|
+
const votesReceived = await (0, rpc_utils_1.queryWithRetry)(() => governor.votesReceived(proposalId, nominee));
|
|
780
|
+
return {
|
|
781
|
+
address: nominee,
|
|
782
|
+
votesReceived,
|
|
783
|
+
isExcluded: true,
|
|
784
|
+
excludedAtBlock: eventLog.blockNumber,
|
|
785
|
+
exclusionTxHash: eventLog.transactionHash,
|
|
786
|
+
};
|
|
787
|
+
}));
|
|
788
|
+
log("Found %d excluded nominees for proposal %s", excluded.length, proposalId);
|
|
789
|
+
return excluded;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Get detailed nominee election information
|
|
793
|
+
*
|
|
794
|
+
* Aggregates contenders, nominees, excluded nominees, and voting data.
|
|
795
|
+
*
|
|
796
|
+
* @param electionIndex - Election index
|
|
797
|
+
* @param provider - L2 provider
|
|
798
|
+
* @param nomineeGovernorAddress - Optional governor address override
|
|
799
|
+
* @returns Detailed nominee election data or null if not found
|
|
800
|
+
*/
|
|
801
|
+
async function getNomineeElectionDetails(electionIndex, provider, nomineeGovernorAddress = constants_1.ADDRESSES.ELECTION_NOMINEE_GOVERNOR) {
|
|
802
|
+
log("getNomineeElectionDetails for election %d", electionIndex);
|
|
803
|
+
const proposalId = await getElectionProposalId(electionIndex, provider, nomineeGovernorAddress);
|
|
804
|
+
if (!proposalId) {
|
|
805
|
+
log("No proposal found for election %d", electionIndex);
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
const governor = getNomineeGovernor(nomineeGovernorAddress, provider);
|
|
809
|
+
const [contenders, nominees, snapshotBlock] = await Promise.all([
|
|
810
|
+
getContenders(proposalId, provider, nomineeGovernorAddress),
|
|
811
|
+
getNomineesWithVotes(proposalId, provider, nomineeGovernorAddress),
|
|
812
|
+
(0, rpc_utils_1.queryWithRetry)(() => governor.proposalSnapshot(proposalId)),
|
|
457
813
|
]);
|
|
814
|
+
const quorumThreshold = await (0, rpc_utils_1.queryWithRetry)(() => governor.quorum(snapshotBlock.toNumber()));
|
|
815
|
+
const compliantNominees = nominees.filter((n) => !n.isExcluded);
|
|
816
|
+
const excludedNominees = nominees.filter((n) => n.isExcluded);
|
|
458
817
|
return {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
818
|
+
proposalId,
|
|
819
|
+
electionIndex,
|
|
820
|
+
contenders,
|
|
821
|
+
nominees,
|
|
822
|
+
compliantNominees,
|
|
823
|
+
excludedNominees,
|
|
824
|
+
quorumThreshold,
|
|
825
|
+
targetNomineeCount: constants_1.TIMING.SECURITY_COUNCIL_TARGET_NOMINEES,
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Get member election results with weighted votes
|
|
830
|
+
*
|
|
831
|
+
* Fetches top nominees (winners) and their weighted vote totals.
|
|
832
|
+
*
|
|
833
|
+
* @param electionIndex - Election index
|
|
834
|
+
* @param provider - L2 provider
|
|
835
|
+
* @param memberGovernorAddress - Optional governor address override
|
|
836
|
+
* @param nomineeGovernorAddress - Optional nominee governor address override
|
|
837
|
+
* @returns Detailed member election data or null if not found
|
|
838
|
+
*/
|
|
839
|
+
async function getMemberElectionDetails(electionIndex, provider, memberGovernorAddress = constants_1.ADDRESSES.ELECTION_MEMBER_GOVERNOR, nomineeGovernorAddress = constants_1.ADDRESSES.ELECTION_NOMINEE_GOVERNOR) {
|
|
840
|
+
log("getMemberElectionDetails for election %d", electionIndex);
|
|
841
|
+
const memberGovernor = getMemberGovernor(memberGovernorAddress, provider);
|
|
842
|
+
const nomineeGovernor = getNomineeGovernor(nomineeGovernorAddress, provider);
|
|
843
|
+
// Compute proposal ID using getProposeArgs + hashProposal (same scheme as nominee governor)
|
|
844
|
+
const memberProposalId = await computeElectionProposalId(electionIndex, memberGovernor);
|
|
845
|
+
// Verify proposal exists by checking state (reverts for non-existent proposals)
|
|
846
|
+
try {
|
|
847
|
+
await (0, rpc_utils_1.queryWithRetry)(() => memberGovernor.state(memberProposalId));
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
log("No member proposal found for election %d", electionIndex);
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
const [winners, deadline, fullWeightDeadline, nomineeProposalId] = await Promise.all([
|
|
854
|
+
(0, rpc_utils_1.queryWithRetry)(() => memberGovernor.topNominees(memberProposalId)).catch(() => []),
|
|
855
|
+
(0, rpc_utils_1.queryWithRetry)(() => memberGovernor.proposalDeadline(memberProposalId)),
|
|
856
|
+
(0, rpc_utils_1.queryWithRetry)(() => memberGovernor.fullWeightVotingDeadline(memberProposalId)),
|
|
857
|
+
getElectionProposalId(electionIndex, provider, nomineeGovernorAddress),
|
|
858
|
+
]);
|
|
859
|
+
const allNominees = nomineeProposalId
|
|
860
|
+
? await (0, rpc_utils_1.queryWithRetry)(() => nomineeGovernor.compliantNominees(nomineeProposalId))
|
|
861
|
+
: [];
|
|
862
|
+
const winnersSet = new Set(winners.map((w) => w.toLowerCase()));
|
|
863
|
+
const nomineeWeights = await Promise.all(allNominees.map(async (addr) => ({
|
|
864
|
+
addr,
|
|
865
|
+
weight: await (0, rpc_utils_1.queryWithRetry)(() => memberGovernor.weightReceived(memberProposalId, addr)),
|
|
866
|
+
})));
|
|
867
|
+
const nomineeDetails = nomineeWeights
|
|
868
|
+
.sort((a, b) => (b.weight.gt(a.weight) ? 1 : -1))
|
|
869
|
+
.map((n, i) => ({
|
|
870
|
+
address: n.addr,
|
|
871
|
+
weightReceived: n.weight,
|
|
872
|
+
isWinner: winnersSet.has(n.addr.toLowerCase()),
|
|
873
|
+
rank: i + 1,
|
|
874
|
+
}));
|
|
875
|
+
return {
|
|
876
|
+
proposalId: memberProposalId,
|
|
877
|
+
electionIndex,
|
|
878
|
+
nominees: nomineeDetails,
|
|
879
|
+
winners,
|
|
880
|
+
fullWeightDeadline: fullWeightDeadline.toNumber(),
|
|
881
|
+
proposalDeadline: deadline.toNumber(),
|
|
465
882
|
};
|
|
466
883
|
}
|
|
467
884
|
//# sourceMappingURL=election.js.map
|