@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.
Files changed (55) hide show
  1. package/README.md +41 -2
  2. package/dist/abis.d.ts.map +1 -1
  3. package/dist/abis.js +22 -1
  4. package/dist/abis.js.map +1 -1
  5. package/dist/cli/cli.js +206 -12
  6. package/dist/cli/cli.js.map +1 -1
  7. package/dist/cli/lib/cli.d.ts +9 -1
  8. package/dist/cli/lib/cli.d.ts.map +1 -1
  9. package/dist/cli/lib/cli.js +80 -4
  10. package/dist/cli/lib/cli.js.map +1 -1
  11. package/dist/data/bundled-cache.json +1155 -962
  12. package/dist/deduplication.d.ts +132 -0
  13. package/dist/deduplication.d.ts.map +1 -0
  14. package/dist/deduplication.js +270 -0
  15. package/dist/deduplication.js.map +1 -0
  16. package/dist/election.d.ts +141 -1
  17. package/dist/election.d.ts.map +1 -1
  18. package/dist/election.js +505 -88
  19. package/dist/election.js.map +1 -1
  20. package/dist/index.d.ts +4 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +27 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/stages/builder.d.ts +4 -0
  25. package/dist/stages/builder.d.ts.map +1 -1
  26. package/dist/stages/builder.js +7 -0
  27. package/dist/stages/builder.js.map +1 -1
  28. package/dist/stages/voting.d.ts.map +1 -1
  29. package/dist/stages/voting.js +5 -4
  30. package/dist/stages/voting.js.map +1 -1
  31. package/dist/tracker/discovery.d.ts +39 -8
  32. package/dist/tracker/discovery.d.ts.map +1 -1
  33. package/dist/tracker/discovery.js +141 -12
  34. package/dist/tracker/discovery.js.map +1 -1
  35. package/dist/tracker.d.ts +24 -4
  36. package/dist/tracker.d.ts.map +1 -1
  37. package/dist/tracker.js +57 -8
  38. package/dist/tracker.js.map +1 -1
  39. package/dist/types/core.d.ts +1 -1
  40. package/dist/types/core.d.ts.map +1 -1
  41. package/dist/types/election.d.ts +88 -0
  42. package/dist/types/election.d.ts.map +1 -1
  43. package/dist/types/index.d.ts +2 -2
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/dist/types/index.js.map +1 -1
  46. package/dist/types/stages.d.ts +6 -0
  47. package/dist/types/stages.d.ts.map +1 -1
  48. package/dist/types/stages.js.map +1 -1
  49. package/dist/types/tracking.d.ts +27 -1
  50. package/dist/types/tracking.d.ts.map +1 -1
  51. package/dist/utils/timing.d.ts +13 -0
  52. package/dist/utils/timing.d.ts.map +1 -1
  53. package/dist/utils/timing.js +35 -0
  54. package/dist/utils/timing.js.map +1 -1
  55. 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
- * Convert numeric proposal state to string
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 states = [
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
- const memberPropId = await (0, rpc_utils_1.queryWithRetry)(() => memberGovernor.electionIndexToProposalId(electionIndex));
268
- if (!memberPropId.isZero()) {
269
- memberProposalId = memberPropId.toString();
270
- const memberState = await (0, rpc_utils_1.queryWithRetry)(() => memberGovernor.state(memberProposalId));
271
- memberProposalState = stateToString(memberState);
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
- else {
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 provider - L2 provider
330
- * @param nomineeGovernorAddress - Optional governor address override
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 getElectionProposalId(electionIndex, provider, nomineeGovernorAddress = constants_1.ADDRESSES.ELECTION_NOMINEE_GOVERNOR) {
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 parameters for an election proposal
367
+ * Get the proposal ID for a given election index
349
368
  *
350
- * Searches for the ProposalCreated event to extract targets, values, calldatas,
351
- * and description needed for execute() call.
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 Election proposal params or null if not found
375
+ * @returns Proposal ID or null if election not yet created
357
376
  */
358
- async function getElectionProposalParams(electionIndex, provider, nomineeGovernorAddress = constants_1.ADDRESSES.ELECTION_NOMINEE_GOVERNOR) {
359
- log("getElectionProposalParams for index %d", electionIndex);
360
- const proposalId = await getElectionProposalId(electionIndex, provider, nomineeGovernorAddress);
361
- if (!proposalId) {
362
- log("No proposal ID found for election %d", electionIndex);
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
- // Search for ProposalCreated event
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: nomineeGovernorAddress,
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
- // Build execute calldata using governor interface
452
- const calldata = abis_1.governorInterface.encodeFunctionData("execute", [
453
- params.targets,
454
- params.values,
455
- params.calldatas,
456
- params.descriptionHash,
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
- to: nomineeGovernorAddress,
460
- data: calldata,
461
- value: "0",
462
- chain: "arb1",
463
- chainId: 42161,
464
- description: `execute() on NomineeElectionGovernor to trigger member election #${electionStatus.electionIndex}`,
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