|
1 | 1 | import hre from "hardhat";
|
2 | 2 | import { toBigInt, BigNumberish, getNumber, BytesLike } from "ethers";
|
3 |
| -import { SortitionModule, SortitionModuleNeo } from "../typechain-types"; |
| 3 | +import { DisputeKitClassic, DisputeKitShutter, SortitionModule, SortitionModuleNeo } from "../typechain-types"; |
4 | 4 | import env from "./utils/env";
|
5 | 5 | import loggerFactory from "./utils/logger";
|
6 | 6 | import { Cores, getContracts as getContractsForCoreType } from "./utils/contracts";
|
| 7 | +import { shutterAutoReveal } from "./keeperBotShutter"; |
7 | 8 |
|
8 |
| -let request: <T>(url: string, query: string) => Promise<T>; // Workaround graphql-request ESM import |
9 | 9 | const { ethers } = hre;
|
| 10 | +const SHUTTER_AUTO_REVEAL_ONLY = env.optional("SHUTTER_AUTO_REVEAL_ONLY", "false") === "true"; |
| 11 | +const MAX_DRAW_CALLS_WITHOUT_JURORS = 10; |
10 | 12 | const MAX_DRAW_ITERATIONS = 30;
|
11 | 13 | const MAX_EXECUTE_ITERATIONS = 20;
|
12 | 14 | const MAX_DELAYED_STAKES_ITERATIONS = 50;
|
@@ -76,73 +78,115 @@ enum Phase {
|
76 | 78 | }
|
77 | 79 | const PHASES = Object.values(Phase);
|
78 | 80 |
|
| 81 | +const getDisputeKit = async ( |
| 82 | + coreDisputeId: string, |
| 83 | + coreRoundId: string |
| 84 | +): Promise<{ |
| 85 | + disputeKit: DisputeKitClassic | DisputeKitShutter; |
| 86 | + localDisputeId: bigint; |
| 87 | + localRoundId: bigint; |
| 88 | +}> => { |
| 89 | + const { core, disputeKitClassic, disputeKitShutter } = await getContracts(); |
| 90 | + const round = await core.getRoundInfo(coreDisputeId, coreRoundId); |
| 91 | + const disputeKitAddress = await core.disputeKits(round.disputeKitID); |
| 92 | + let disputeKit: DisputeKitClassic | DisputeKitShutter; |
| 93 | + switch (disputeKitAddress) { |
| 94 | + case disputeKitClassic.target: |
| 95 | + disputeKit = disputeKitClassic; |
| 96 | + break; |
| 97 | + case disputeKitShutter?.target: |
| 98 | + if (!disputeKitShutter) throw new Error(`DisputeKitShutter not deployed`); |
| 99 | + disputeKit = disputeKitShutter; |
| 100 | + break; |
| 101 | + default: |
| 102 | + throw new Error(`Unknown dispute kit: ${disputeKitAddress}`); |
| 103 | + } |
| 104 | + const [localDisputeId, localRoundId] = await disputeKit.getLocalDisputeRoundID(coreDisputeId, coreRoundId); |
| 105 | + return { disputeKit, localDisputeId, localRoundId }; |
| 106 | +}; |
| 107 | + |
79 | 108 | const getNonFinalDisputes = async (): Promise<Dispute[]> => {
|
80 |
| - const nonFinalDisputesRequest = `{ |
81 |
| - disputes(where: {period_not: execution}) { |
82 |
| - period |
83 |
| - id |
84 |
| - currentRoundIndex |
| 109 | + const { gql, request } = await import("graphql-request"); // workaround for ESM import |
| 110 | + const query = gql` |
| 111 | + query NonFinalDisputes { |
| 112 | + disputes(where: { period_not: execution }) { |
| 113 | + period |
| 114 | + id |
| 115 | + currentRoundIndex |
| 116 | + } |
85 | 117 | }
|
86 |
| - }`; |
| 118 | + `; |
87 | 119 | // TODO: use a local graph node if chainId is HARDHAT
|
88 |
| - const result = await request(SUBGRAPH_URL, nonFinalDisputesRequest); |
89 |
| - const { disputes } = result as { disputes: Dispute[] }; |
| 120 | + type Disputes = { disputes: Dispute[] }; |
| 121 | + const { disputes } = await request<Disputes>(SUBGRAPH_URL, query); |
90 | 122 | return disputes;
|
91 | 123 | };
|
92 | 124 |
|
93 | 125 | const getAppealContributions = async (disputeId: string): Promise<Contribution[]> => {
|
94 |
| - const appealContributionsRequest = (disputeId: string) => `{ |
95 |
| - contributions(where: {coreDispute: "${disputeId}"}) { |
96 |
| - contributor { |
97 |
| - id |
98 |
| - } |
99 |
| - ... on ClassicContribution { |
100 |
| - choice |
101 |
| - rewardWithdrawn |
102 |
| - } |
103 |
| - coreDispute { |
104 |
| - currentRoundIndex |
| 126 | + const { gql, request } = await import("graphql-request"); // workaround for ESM import |
| 127 | + const query = gql` |
| 128 | + query AppealContributions($disputeId: String!) { |
| 129 | + contributions(where: { coreDispute: $disputeId }) { |
| 130 | + contributor { |
| 131 | + id |
| 132 | + } |
| 133 | + ... on ClassicContribution { |
| 134 | + choice |
| 135 | + rewardWithdrawn |
| 136 | + } |
| 137 | + coreDispute { |
| 138 | + currentRoundIndex |
| 139 | + } |
105 | 140 | }
|
106 | 141 | }
|
107 |
| - }`; |
| 142 | + `; |
| 143 | + const variables = { disputeId }; |
| 144 | + type AppealContributions = { contributions: Contribution[] }; |
108 | 145 | // TODO: use a local graph node if chainId is HARDHAT
|
109 |
| - const result = await request(SUBGRAPH_URL, appealContributionsRequest(disputeId)); |
110 |
| - const { contributions } = result as { contributions: Contribution[] }; |
| 146 | + const { contributions } = await request<AppealContributions>(SUBGRAPH_URL, query, variables); |
111 | 147 | return contributions;
|
112 | 148 | };
|
113 | 149 |
|
114 | 150 | const getDisputesWithUnexecutedRuling = async (): Promise<Dispute[]> => {
|
115 |
| - const disputesWithUnexecutedRuling = `{ |
116 |
| - disputes(where: {period: execution, ruled: false}) { |
117 |
| - id |
118 |
| - currentRoundIndex |
119 |
| - period |
| 151 | + const { gql, request } = await import("graphql-request"); // workaround for ESM import |
| 152 | + const query = gql` |
| 153 | + query DisputesWithUnexecutedRuling { |
| 154 | + disputes(where: { period: execution, ruled: false }) { |
| 155 | + id |
| 156 | + currentRoundIndex |
| 157 | + period |
| 158 | + } |
120 | 159 | }
|
121 |
| - }`; |
| 160 | + `; |
122 | 161 | // TODO: use a local graph node if chainId is HARDHAT
|
123 |
| - const result = (await request(SUBGRAPH_URL, disputesWithUnexecutedRuling)) as { disputes: Dispute[] }; |
124 |
| - return result.disputes; |
| 162 | + type Disputes = { disputes: Dispute[] }; |
| 163 | + const { disputes } = await request<Disputes>(SUBGRAPH_URL, query); |
| 164 | + return disputes; |
125 | 165 | };
|
126 | 166 |
|
127 | 167 | const getUniqueDisputes = (disputes: Dispute[]): Dispute[] => {
|
128 | 168 | return [...new Map(disputes.map((v) => [v.id, v])).values()];
|
129 | 169 | };
|
130 | 170 |
|
131 | 171 | const getDisputesWithContributionsNotYetWithdrawn = async (): Promise<Dispute[]> => {
|
132 |
| - const disputesWithContributionsNotYetWithdrawn = `{ |
133 |
| - classicContributions(where: {rewardWithdrawn: false}) { |
134 |
| - coreDispute { |
135 |
| - id |
136 |
| - period |
137 |
| - currentRoundIndex |
| 172 | + const { gql, request } = await import("graphql-request"); // workaround for ESM import |
| 173 | + const query = gql` |
| 174 | + query DisputesWithContributionsNotYetWithdrawn { |
| 175 | + classicContributions(where: { rewardWithdrawn: false }) { |
| 176 | + coreDispute { |
| 177 | + id |
| 178 | + period |
| 179 | + currentRoundIndex |
| 180 | + } |
138 | 181 | }
|
139 | 182 | }
|
140 |
| - }`; |
| 183 | + `; |
141 | 184 | // TODO: use a local graph node if chainId is HARDHAT
|
142 |
| - const result = (await request(SUBGRAPH_URL, disputesWithContributionsNotYetWithdrawn)) as { |
| 185 | + type Contributions = { |
143 | 186 | classicContributions: { coreDispute: Dispute }[];
|
144 | 187 | };
|
145 |
| - const disputes = result.classicContributions |
| 188 | + const { classicContributions } = await request<Contributions>(SUBGRAPH_URL, query); |
| 189 | + const disputes = classicContributions |
146 | 190 | .filter((contribution) => contribution.coreDispute.period === "execution")
|
147 | 191 | .map((dispute) => dispute.coreDispute);
|
148 | 192 | return getUniqueDisputes(disputes);
|
@@ -248,7 +292,19 @@ const drawJurors = async (dispute: { id: string; currentRoundIndex: string }, it
|
248 | 292 | const { core } = await getContracts();
|
249 | 293 | let success = false;
|
250 | 294 | try {
|
251 |
| - await core.draw.staticCall(dispute.id, iterations, HIGH_GAS_LIMIT); |
| 295 | + const simulatedIterations = iterations * MAX_DRAW_CALLS_WITHOUT_JURORS; // Drawing will be skipped as long as no juror is available in the next MAX_DRAW_CALLS_WITHOUT_JURORS calls to draw() given this nb of iterations. |
| 296 | + const { drawnJurors: drawnJurorsBefore } = await core.getRoundInfo(dispute.id, dispute.currentRoundIndex); |
| 297 | + const nbDrawnJurors = (await core.draw.staticCall(dispute.id, simulatedIterations, HIGH_GAS_LIMIT)) as bigint; |
| 298 | + const extraJurors = nbDrawnJurors - BigInt(drawnJurorsBefore.length); |
| 299 | + logger.debug( |
| 300 | + `Draw: ${extraJurors} jurors available in the next ${simulatedIterations} iterations for dispute ${dispute.id}` |
| 301 | + ); |
| 302 | + if (extraJurors <= 0n) { |
| 303 | + logger.warn( |
| 304 | + `Draw: skipping, no jurors available in the next ${simulatedIterations} iterations for dispute ${dispute.id}` |
| 305 | + ); |
| 306 | + return success; |
| 307 | + } |
252 | 308 | } catch (e) {
|
253 | 309 | logger.error(`Draw: will fail for ${dispute.id}, skipping`);
|
254 | 310 | return success;
|
@@ -306,49 +362,55 @@ const executeRuling = async (dispute: { id: string }) => {
|
306 | 362 | };
|
307 | 363 |
|
308 | 364 | const withdrawAppealContribution = async (
|
309 |
| - disputeId: string, |
310 |
| - roundId: string, |
| 365 | + coreDisputeId: string, |
| 366 | + coreRoundId: string, |
311 | 367 | contribution: Contribution
|
312 | 368 | ): Promise<boolean> => {
|
313 |
| - const { disputeKitClassic: kit } = await getContracts(); |
| 369 | + const { disputeKit, localDisputeId, localRoundId } = await getDisputeKit(coreDisputeId, coreRoundId); |
314 | 370 | let success = false;
|
315 | 371 | let amountWithdrawn = 0n;
|
316 | 372 | try {
|
317 |
| - amountWithdrawn = await kit.withdrawFeesAndRewards.staticCall( |
318 |
| - disputeId, |
| 373 | + amountWithdrawn = await disputeKit.withdrawFeesAndRewards.staticCall( |
| 374 | + localDisputeId, |
319 | 375 | contribution.contributor.id,
|
320 |
| - roundId, |
| 376 | + localRoundId, |
321 | 377 | contribution.choice
|
322 | 378 | );
|
323 | 379 | } catch (e) {
|
324 | 380 | logger.warn(
|
325 |
| - `WithdrawFeesAndRewards: will fail for dispute #${disputeId}, round #${roundId}, choice ${contribution.choice} and beneficiary ${contribution.contributor.id}, skipping` |
| 381 | + `WithdrawFeesAndRewards: will fail for core dispute #${coreDisputeId}, round #${coreRoundId}, choice ${contribution.choice} and beneficiary ${contribution.contributor.id}, skipping` |
326 | 382 | );
|
327 | 383 | return success;
|
328 | 384 | }
|
329 | 385 | if (amountWithdrawn === 0n) {
|
330 | 386 | logger.debug(
|
331 |
| - `WithdrawFeesAndRewards: no fees or rewards to withdraw for dispute #${disputeId}, round #${roundId}, choice ${contribution.choice} and beneficiary ${contribution.contributor.id}, skipping` |
| 387 | + `WithdrawFeesAndRewards: no fees or rewards to withdraw for core dispute #${coreDisputeId}, round #${coreRoundId}, choice ${contribution.choice} and beneficiary ${contribution.contributor.id}, skipping` |
332 | 388 | );
|
333 | 389 | return success;
|
334 | 390 | }
|
335 | 391 | try {
|
336 | 392 | logger.info(
|
337 |
| - `WithdrawFeesAndRewards: appeal contribution for dispute #${disputeId}, round #${roundId}, choice ${contribution.choice} and beneficiary ${contribution.contributor.id}` |
| 393 | + `WithdrawFeesAndRewards: appeal contribution for core dispute #${coreDisputeId}, round #${coreRoundId}, choice ${contribution.choice} and beneficiary ${contribution.contributor.id}` |
338 | 394 | );
|
339 | 395 | const gas =
|
340 |
| - ((await kit.withdrawFeesAndRewards.estimateGas( |
341 |
| - disputeId, |
| 396 | + ((await disputeKit.withdrawFeesAndRewards.estimateGas( |
| 397 | + localDisputeId, |
342 | 398 | contribution.contributor.id,
|
343 |
| - roundId, |
| 399 | + localRoundId, |
344 | 400 | contribution.choice
|
345 | 401 | )) *
|
346 | 402 | 150n) /
|
347 | 403 | 100n; // 50% extra gas
|
348 | 404 | const tx = await (
|
349 |
| - await kit.withdrawFeesAndRewards(disputeId, contribution.contributor.id, roundId, contribution.choice, { |
350 |
| - gasLimit: gas, |
351 |
| - }) |
| 405 | + await disputeKit.withdrawFeesAndRewards( |
| 406 | + localDisputeId, |
| 407 | + contribution.contributor.id, |
| 408 | + localRoundId, |
| 409 | + contribution.choice, |
| 410 | + { |
| 411 | + gasLimit: gas, |
| 412 | + } |
| 413 | + ) |
352 | 414 | ).wait();
|
353 | 415 | logger.info(`WithdrawFeesAndRewards txID: ${tx?.hash}`);
|
354 | 416 | success = true;
|
@@ -445,10 +507,13 @@ const sendHeartbeat = async () => {
|
445 | 507 | }
|
446 | 508 | };
|
447 | 509 |
|
| 510 | +const shutdown = async () => { |
| 511 | + logger.info("Shutting down"); |
| 512 | + await delay(2000); // Some log messages may be lost otherwise |
| 513 | +}; |
| 514 | + |
448 | 515 | async function main() {
|
449 |
| - const graphqlRequest = await import("graphql-request"); // Workaround graphql-request ESM import |
450 |
| - request = graphqlRequest.request; |
451 |
| - const { core, sortition, disputeKitClassic } = await getContracts(); |
| 516 | + const { core, sortition, disputeKitShutter } = await getContracts(); |
452 | 517 |
|
453 | 518 | const getBlockTime = async () => {
|
454 | 519 | return await ethers.provider.getBlock("latest").then((block) => {
|
@@ -489,6 +554,14 @@ async function main() {
|
489 | 554 |
|
490 | 555 | await sendHeartbeat();
|
491 | 556 |
|
| 557 | + logger.info("Auto-revealing disputes"); |
| 558 | + await shutterAutoReveal(disputeKitShutter, DISPUTES_TO_SKIP); |
| 559 | + if (SHUTTER_AUTO_REVEAL_ONLY) { |
| 560 | + logger.debug("Shutter auto-reveal only, skipping other actions"); |
| 561 | + await shutdown(); |
| 562 | + return; |
| 563 | + } |
| 564 | + |
492 | 565 | logger.info(`Current phase: ${PHASES[getNumber(await sortition.phase())]}`);
|
493 | 566 |
|
494 | 567 | // Retrieve the disputes which are in a non-final period
|
@@ -609,7 +682,8 @@ async function main() {
|
609 | 682 | // ----------------------------------------------- //
|
610 | 683 | // REPARTITIONS EXECUTION //
|
611 | 684 | // ----------------------------------------------- //
|
612 |
| - const coherentCount = await disputeKitClassic.getCoherentCount(dispute.id, dispute.currentRoundIndex); |
| 685 | + const { disputeKit } = await getDisputeKit(dispute.id, dispute.currentRoundIndex); |
| 686 | + const coherentCount = await disputeKit.getCoherentCount(dispute.id, dispute.currentRoundIndex); |
613 | 687 | let numberOfMissingRepartitions = await getNumberOfMissingRepartitions(dispute, coherentCount);
|
614 | 688 | do {
|
615 | 689 | const executeIterations = Math.min(MAX_EXECUTE_ITERATIONS, numberOfMissingRepartitions);
|
@@ -673,8 +747,7 @@ async function main() {
|
673 | 747 |
|
674 | 748 | await sendHeartbeat();
|
675 | 749 |
|
676 |
| - logger.info("Shutting down"); |
677 |
| - await delay(2000); // Some log messages may be lost otherwise |
| 750 | + await shutdown(); |
678 | 751 | }
|
679 | 752 |
|
680 | 753 | main()
|
|
0 commit comments