diff --git a/src/bots/kleros-liquid.js b/src/bots/kleros-liquid.js index c48e9e9..374c16c 100644 --- a/src/bots/kleros-liquid.js +++ b/src/bots/kleros-liquid.js @@ -1,106 +1,107 @@ -const delay = require('delay') -const https = require('https') -const _klerosLiquid = require('../contracts/kleros-liquid.json') +const delay = require("delay"); +const https = require("https"); +const _klerosLiquid = require("../contracts/kleros-liquid.json"); -const DELAYED_STAKES_ITERATIONS = 15 +const DELAYED_STAKES_ITERATIONS = 15; module.exports = async (web3, batchedSend) => { // Instantiate the Kleros Liquid contract. const klerosLiquid = new web3.eth.Contract( _klerosLiquid.abi, process.env.KLEROS_LIQUID_CONTRACT_ADDRESS - ) - const PhaseEnum = Object.freeze({"staking": 0, "generating": 1, "drawing": 2}) + ); + const PhaseEnum = Object.freeze({ staking: 0, generating: 1, drawing: 2 }); // Keep track of executed disputes so we don't waste resources on them. - const executedDisputeIDs = {} + const executedDisputeIDs = {}; + let doHeartbeat = true; while (true) { - if (process.env.HEARTBEAT_URL) { - https - .get(process.env.HEARTBEAT_URL, () => {}) - .on("error", (e) => { - console.error("Failed to send heartbeat: %s", e); - }); - } - + console.log("Initializing klerosLiquid loop..."); // Try to execute delayed set stakes if there are any. We check because this transaction still succeeds when there are not any and we don't want to waste gas in those cases. if ( (await klerosLiquid.methods.lastDelayedSetStake().call()) >= (await klerosLiquid.methods.nextDelayedSetStake().call()) - ) - batchedSend({ + ) { + console.log("Executing delayed set stakes..."); + await batchedSend({ args: [DELAYED_STAKES_ITERATIONS], method: klerosLiquid.methods.executeDelayedSetStakes, - to: klerosLiquid.options.address - }) - + to: klerosLiquid.options.address, + }); + } // Loop over all disputes. try { - let disputeID = 0 + let disputeID = 0; while (true) { if (!executedDisputeIDs[disputeID]) { - const dispute = await klerosLiquid.methods.disputes(disputeID).call() + let dispute; + try { + dispute = await klerosLiquid.methods.disputes(disputeID).call(); + } catch (_) { + //console.log(e); + break; + } const dispute2 = await klerosLiquid.methods .getDispute(disputeID) - .call() + .call(); const voteCounters = await Promise.all( // eslint-disable-next-line no-loop-func dispute2.votesLengths.map(async (numberOfVotes, i) => { - let voteCounter + let voteCounter; try { voteCounter = await klerosLiquid.methods .getVoteCounter(disputeID, i) - .call() + .call(); } catch (_) { // Look it up manually if numberOfChoices is too high for loop - let tied = true - let winningChoice = '0' - const _voteCounters = {} + let tied = true; + let winningChoice = "0"; + const _voteCounters = {}; for (let j = 0; j < numberOfVotes; j++) { const vote = await klerosLiquid.methods.getVote( disputeID, i, j - ) + ); if (vote.voted) { // increment vote count _voteCounters[vote.choice] = _voteCounters[vote.choice] ? _voteCounters[vote.choice] + 1 - : 1 + : 1; if (vote.choice === winningChoice) { - if (tied) tied = false // broke tie + if (tied) tied = false; // broke tie } else { const _winningChoiceVotes = - _voteCounters[winningChoice] || 0 + _voteCounters[winningChoice] || 0; if (_voteCounters[vote.choice] > _winningChoiceVotes) { - winningChoice = vote.choice - tied = false + winningChoice = vote.choice; + tied = false; } else if ( _voteCounters[vote.choice] === _winningChoiceVotes ) - tied = true + tied = true; } } } voteCounter = { tied, - winningChoice - } + winningChoice, + }; } - return voteCounter + return voteCounter; }) - ) + ); const notTieAndNoOneCoherent = voteCounters.map( - v => + (v) => !voteCounters[voteCounters.length - 1].tied && v.counts[voteCounters[voteCounters.length - 1].winningChoice] === - '0' - ) + "0" + ); if ( !dispute.ruled || dispute2.votesLengths.some( @@ -108,9 +109,10 @@ module.exports = async (web3, batchedSend) => { Number(notTieAndNoOneCoherent[i] ? l : l * 2) !== Number(dispute2.repartitionsInEachRound[i]) ) - ) + ) { // The dispute is not finalized, try to call all of its callbacks. - batchedSend( + console.log("Calling callbacks for dispute %s", disputeID); + await batchedSend( [ // We check if there are still pending draws because if there aren't any and the dispute is still in the evidence period, // then the transaction would still succeed and we don't want to waste gas in those cases. @@ -118,7 +120,7 @@ module.exports = async (web3, batchedSend) => { dispute.drawsInRound && { args: [disputeID, 15], method: klerosLiquid.methods.drawJurors, - to: klerosLiquid.options.address + to: klerosLiquid.options.address, }, ...dispute2.votesLengths.map( // eslint-disable-next-line no-loop-func @@ -127,53 +129,76 @@ module.exports = async (web3, batchedSend) => { Number(dispute2.repartitionsInEachRound[i]) && { args: [disputeID, i, 15], method: klerosLiquid.methods.execute, - to: klerosLiquid.options.address + to: klerosLiquid.options.address, } ), { args: [disputeID], method: klerosLiquid.methods.executeRuling, - to: klerosLiquid.options.address + to: klerosLiquid.options.address, }, { args: [disputeID], method: klerosLiquid.methods.passPeriod, - to: klerosLiquid.options.address - } - ].filter(t => t) - ) - else executedDisputeIDs[disputeID] = true // The dispute is finalized, cache it. + to: klerosLiquid.options.address, + }, + ].filter((t) => t) + ); + } else { + executedDisputeIDs[disputeID] = true; // The dispute is finalized, cache it. + console.log("Dispute %s is finalized, caching it.", disputeID); + } } - disputeID++ + disputeID++; } - } catch (_) {} // Reached the end of the disputes list. + } catch (e) { + console.error("Failed to process disputes: ", e); + doHeartbeat = false; + } // Reached the end of the disputes list. // Try to pass the phase. - let readyForNextPhase = false - const phase = await klerosLiquid.methods.phase().call() - const lastPhaseChange = await klerosLiquid.methods.lastPhaseChange().call() - const disputesWithoutJurors = await klerosLiquid.methods.disputesWithoutJurors().call() + let readyForNextPhase = false; + const phase = await klerosLiquid.methods.phase().call(); + const lastPhaseChange = await klerosLiquid.methods.lastPhaseChange().call(); + const disputesWithoutJurors = await klerosLiquid.methods + .disputesWithoutJurors() + .call(); if (phase == PhaseEnum.staking) { - const minStakingTime = await klerosLiquid.methods.minStakingTime().call() - if ((Date.now() - lastPhaseChange * 1000 >= minStakingTime * 1000) && disputesWithoutJurors > 0) { - readyForNextPhase = true + const minStakingTime = await klerosLiquid.methods.minStakingTime().call(); + if ( + Date.now() - lastPhaseChange * 1000 >= minStakingTime * 1000 && + disputesWithoutJurors > 0 + ) { + readyForNextPhase = true; } } else if (phase == PhaseEnum.generating) { - readyForNextPhase = true + readyForNextPhase = true; } else if (phase == PhaseEnum.drawing) { - const maxDrawingTime = await klerosLiquid.methods.maxDrawingTime().call() - if ((Date.now() - lastPhaseChange * 1000 >= maxDrawingTime * 1000) || disputesWithoutJurors == 0) { - readyForNextPhase = true + const maxDrawingTime = await klerosLiquid.methods.maxDrawingTime().call(); + if ( + Date.now() - lastPhaseChange * 1000 >= maxDrawingTime * 1000 || + disputesWithoutJurors == 0 + ) { + readyForNextPhase = true; } } if (readyForNextPhase) { - batchedSend({ + console.log("Passing phase..."); + await batchedSend({ method: klerosLiquid.methods.passPhase, - to: klerosLiquid.options.address - }) + to: klerosLiquid.options.address, + }); } - await delay(1000 * 60 * 10) // Every 10 minutes + if (process.env.HEARTBEAT_URL && doHeartbeat) { + https + .get(process.env.HEARTBEAT_URL, () => {}) + .on("error", (e) => { + console.error("Failed to send heartbeat: %s", e); + }); + } + console.log("Waiting for 10 minutes for next loop..."); + await delay(1000 * 60 * 10); // Every 10 minutes } -} +}; diff --git a/src/index.js b/src/index.js index 1526132..fb05237 100644 --- a/src/index.js +++ b/src/index.js @@ -42,7 +42,7 @@ const run = async (bot, { providerUrl, batcherAddress, privateKey }) => { } catch (err) { console.error('Bot error: ', err) } - await delay(10000) // Wait 10 seconds before restarting failed bot. + await delay(60000); // Wait 60 seconds before restarting failed bot. } } diff --git a/src/utils/batched-send.js b/src/utils/batched-send.js index 12cd934..90f94e9 100644 --- a/src/utils/batched-send.js +++ b/src/utils/batched-send.js @@ -57,11 +57,14 @@ module.exports = ( ) ).then(_pendingBatches => (pendingBatches = _pendingBatches)) - const currentGasPrice = web3.utils.toBN(await web3.eth.getGasPrice()) - const maxGasPrice = !!process.env.GAS_PRICE_CEILING_WEI ? process.env.GAS_PRICE_CEILING_WEI : currentGasPrice - const gasPrice = currentGasPrice.gt(web3.utils.toBN(maxGasPrice)) - ? maxGasPrice - : currentGasPrice.toString() + const currentGasPrice = web3.utils.toBN(await web3.eth.getGasPrice()); + const maxGasPrice = !!process.env.GAS_PRICE_CEILING_WEI + ? web3.utils.toBN(process.env.GAS_PRICE_CEILING_WEI) + : currentGasPrice; + const maxPriorityFeePerGas = web3.utils.toBN(web3.utils.toWei("1", "gwei")); + const maxFeePerGas = currentGasPrice.gt(maxGasPrice) + ? maxGasPrice.add(maxPriorityFeePerGas).toString() + : currentGasPrice.add(maxPriorityFeePerGas).toString(); // Build data for the batch transaction using all the transactions in the new batch and all the transactions in previous pending batches. // We do this because if we have pending batches by the time a new batch arrives, it means that their gas prices were too low, so sending a new batch transaction with the same nonce @@ -88,7 +91,7 @@ module.exports = ( batch.targets, batch.values, batch.datas - ) + ); web3.eth .sendSignedTransaction( ( @@ -100,8 +103,8 @@ module.exports = ( batch.totalGas, to: transactionBatcher.options.address, value: batch.totalValue, - maxFeePerGas: gasPrice, - maxPriorityFeePerGas: web3.utils.toWei('1', 'gwei') + maxFeePerGas, + maxPriorityFeePerGas, }, privateKey ) diff --git a/src/xdai-bots/x-kleros-liquid.js b/src/xdai-bots/x-kleros-liquid.js index 303c695..ed8d81e 100644 --- a/src/xdai-bots/x-kleros-liquid.js +++ b/src/xdai-bots/x-kleros-liquid.js @@ -1,113 +1,112 @@ -const delay = require('delay') -const https = require('https') -const _xKlerosLiquid = require('../contracts/x-kleros-liquid.json') -const _randomAuRa = require('../contracts/random-au-ra.json') +const delay = require("delay"); +const https = require("https"); +const _xKlerosLiquid = require("../contracts/x-kleros-liquid.json"); +const _randomAuRa = require("../contracts/random-au-ra.json"); -const DELAYED_STAKES_ITERATIONS = 15 +const DELAYED_STAKES_ITERATIONS = 15; module.exports = async (web3, batchedSend) => { // Instantiate the Kleros Liquid contract. const xKlerosLiquid = new web3.eth.Contract( _xKlerosLiquid.abi, process.env.XDAI_X_KLEROS_LIQUID_CONTRACT_ADDRESS - ) + ); const randomAuRa = new web3.eth.Contract( _randomAuRa.abi, await xKlerosLiquid.methods.RNGenerator().call() - ) + ); - const PhaseEnum = Object.freeze({ staking: 0, generating: 1, drawing: 2 }) + const PhaseEnum = Object.freeze({ staking: 0, generating: 1, drawing: 2 }); // Keep track of executed disputes so we don't waste resources on them. - const executedDisputeIDs = {} - + const executedDisputeIDs = {}; + let doHeartbeat = true; while (true) { - if (process.env.HEARTBEAT_URL) { - https - .get(process.env.HEARTBEAT_URL, () => {}) - .on("error", (e) => { - console.error("Failed to send heartbeat: %s", e); - }); - } - // Try to execute delayed set stakes if there are any. We check because this transaction still succeeds when there are not any and we don't want to waste gas in those cases. + console.log("Initializing xKlerosLiquid loop..."); if ( (await xKlerosLiquid.methods.lastDelayedSetStake().call()) >= (await xKlerosLiquid.methods.nextDelayedSetStake().call()) - ) - batchedSend({ + ) { + console.log("Executing delayed set stakes..."); + await batchedSend({ args: [DELAYED_STAKES_ITERATIONS], method: xKlerosLiquid.methods.executeDelayedSetStakes, - to: xKlerosLiquid.options.address - }) + to: xKlerosLiquid.options.address, + }); + } - // Loop over all disputes. try { - const totalDisputes = Number(await xKlerosLiquid.methods.totalDisputes().call()) - - for(let disputeID = 0; disputeID < totalDisputes; disputeID++) { + const totalDisputes = Number( + await xKlerosLiquid.methods.totalDisputes().call() + ); + console.log("Looping over %s disputes...", totalDisputes); + for (let disputeID = 0; disputeID < totalDisputes; disputeID++) { if (!executedDisputeIDs[disputeID]) { - const dispute = await xKlerosLiquid.methods.disputes(disputeID).call() + const dispute = await xKlerosLiquid.methods + .disputes(disputeID) + .call(); const dispute2 = await xKlerosLiquid.methods .getDispute(disputeID) - .call() + .call(); + const voteCounters = await Promise.all( // eslint-disable-next-line no-loop-func dispute2.votesLengths.map(async (numberOfVotes, i) => { - let voteCounter + let voteCounter; try { voteCounter = await xKlerosLiquid.methods .getVoteCounter(disputeID, i) - .call() + .call(); } catch (_) { // Look it up manually if numberOfChoices is too high for loop - let tied = true - let winningChoice = '0' - const _voteCounters = {} + let tied = true; + let winningChoice = "0"; + const _voteCounters = {}; for (let j = 0; j < numberOfVotes; j++) { const vote = await xKlerosLiquid.methods.getVote( disputeID, i, j - ) + ); if (vote.voted) { // increment vote count _voteCounters[vote.choice] = _voteCounters[vote.choice] ? _voteCounters[vote.choice] + 1 - : 1 + : 1; if (vote.choice === winningChoice) { - if (tied) tied = false // broke tie + if (tied) tied = false; // broke tie } else { const _winningChoiceVotes = - _voteCounters[winningChoice] || 0 + _voteCounters[winningChoice] || 0; if (_voteCounters[vote.choice] > _winningChoiceVotes) { - winningChoice = vote.choice - tied = false + winningChoice = vote.choice; + tied = false; } else if ( _voteCounters[vote.choice] === _winningChoiceVotes ) - tied = true + tied = true; } } } voteCounter = { tied, - winningChoice - } + winningChoice, + }; } - return voteCounter + return voteCounter; }) - ) + ); const notTieAndNoOneCoherent = voteCounters.map( - v => + (v) => !voteCounters[voteCounters.length - 1].tied && v.counts[voteCounters[voteCounters.length - 1].winningChoice] === - '0' - ) + "0" + ); if ( !dispute.ruled || dispute2.votesLengths.some( @@ -117,7 +116,8 @@ module.exports = async (web3, batchedSend) => { ) ) { // The dispute is not finalized, try to call all of its callbacks. - batchedSend( + console.log("Calling callbacks for dispute %s", disputeID); + await batchedSend( [ // We check if there are still pending draws because if there aren't any and the dispute is still in the evidence period, // then the transaction would still succeed and we don't want to waste gas in those cases. @@ -125,7 +125,7 @@ module.exports = async (web3, batchedSend) => { dispute.drawsInRound && { args: [disputeID, 15], method: xKlerosLiquid.methods.drawJurors, - to: xKlerosLiquid.options.address + to: xKlerosLiquid.options.address, }, ...dispute2.votesLengths.map( // eslint-disable-next-line no-loop-func @@ -134,67 +134,86 @@ module.exports = async (web3, batchedSend) => { Number(dispute2.repartitionsInEachRound[i]) && { args: [disputeID, i, 15], method: xKlerosLiquid.methods.execute, - to: xKlerosLiquid.options.address + to: xKlerosLiquid.options.address, } ), { args: [disputeID], method: xKlerosLiquid.methods.executeRuling, - to: xKlerosLiquid.options.address + to: xKlerosLiquid.options.address, }, { args: [disputeID], method: xKlerosLiquid.methods.passPeriod, - to: xKlerosLiquid.options.address - } - ].filter(t => t) - ) + to: xKlerosLiquid.options.address, + }, + ].filter((t) => t) + ); + console.log("Callbacks for dispute %s processed.", disputeID); } else { - executedDisputeIDs[disputeID] = true + executedDisputeIDs[disputeID] = true; + console.log("The dispute %s was already executed.", disputeID); } // The dispute is finalized, cache it. } } - } catch { + } catch (e) { // do nothing... + console.error("Error while looping over disputes:", e); + doHeartbeat = false; } // Try to pass the phase. - let readyForNextPhase = false - const phase = await xKlerosLiquid.methods.phase().call() - const lastPhaseChange = await xKlerosLiquid.methods.lastPhaseChange().call() + let readyForNextPhase = false; + const phase = await xKlerosLiquid.methods.phase().call(); + const lastPhaseChange = await xKlerosLiquid.methods + .lastPhaseChange() + .call(); const disputesWithoutJurors = await xKlerosLiquid.methods .disputesWithoutJurors() - .call() + .call(); if (phase == PhaseEnum.staking) { - const minStakingTime = await xKlerosLiquid.methods.minStakingTime().call() + const minStakingTime = await xKlerosLiquid.methods + .minStakingTime() + .call(); if ( Date.now() - lastPhaseChange * 1000 >= minStakingTime * 1000 && disputesWithoutJurors > 0 ) { - readyForNextPhase = true + readyForNextPhase = true; } } else if (phase == PhaseEnum.generating) { const isCommitPhase = await randomAuRa.methods.isCommitPhase().call(); if (isCommitPhase) { - readyForNextPhase = true + readyForNextPhase = true; } } else if (phase == PhaseEnum.drawing) { - const maxDrawingTime = await xKlerosLiquid.methods.maxDrawingTime().call() + const maxDrawingTime = await xKlerosLiquid.methods + .maxDrawingTime() + .call(); if ( Date.now() - lastPhaseChange * 1000 >= maxDrawingTime * 1000 && disputesWithoutJurors == 0 ) { - readyForNextPhase = true + readyForNextPhase = true; } } if (readyForNextPhase) { - batchedSend({ + console.log("Passing phase..."); + await batchedSend({ method: xKlerosLiquid.methods.passPhase, - to: xKlerosLiquid.options.address - }) + to: xKlerosLiquid.options.address, + }); } - await delay(5 * 60 * 1000) // Every 5 minutes + if (process.env.HEARTBEAT_URL && doHeartbeat) { + https + .get(process.env.HEARTBEAT_URL, () => {}) + .on("error", (e) => { + console.error("Failed to send heartbeat: %s", e); + }); + } + console.log("xKlerosLiquid loop concluded. Awaiting 5 mins for next loop."); + await delay(5 * 60 * 1000); // Every 5 minutes } -} +};