-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
31 changed files
with
435 additions
and
715 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,4 +5,5 @@ yarn-debug.log* | |
yarn-error.log* | ||
node_modules/ | ||
*.tsbuildinfo | ||
dist | ||
dist | ||
*.tgz |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<script type="text/javascript"> | ||
RED.nodes.registerType('consensus', { | ||
category: 'Energy Web X', | ||
paletteLabel: 'Offline Consensus', | ||
color: '#60A0FF', | ||
inputs: 1, | ||
outputs: 1, | ||
icon: "icons/consensus.svg", | ||
label: function() { | ||
return this.name || "Offline Consensus"; | ||
}, | ||
inputLabels: [], | ||
outputLabels: [], | ||
align: 'left', | ||
defaults: { | ||
ewxConfig: { value:"", type: "energywebx-config", required: true }, | ||
} | ||
}); | ||
</script> | ||
|
||
<script type="text/html" data-template-name="consensus"> | ||
<p>Checks if consensus was reached for given voting round.</p> | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
module.exports = function (RED) { | ||
const axios = require('axios'); | ||
|
||
const ConsensusStatus = { | ||
NOT_ENOUGH_VOTES: 'NOT_ENOUGH_VOTES', | ||
REACHED: 'REACHED', | ||
UNABLE_TO_REACH_CONSENSUS: 'UNABLE_TO_REACH_CONSENSUS', | ||
FAILED: 'FAILED' | ||
}; | ||
|
||
const gqlQuery = ` | ||
query GetSubmittedResults($solutionNamespace: String!, $votingRoundId: String!, $solutionGroupId: String!, $limit: Int!, $offset: Int!) { | ||
solutionResultSubmitteds(where: {solution: {id_eq: $solutionNamespace}votingRoundId_eq: $votingRoundId, successful_eq: true}, orderBy: blockNumber_ASC, limit: $limit, offset: $offset) { | ||
result | ||
worker { | ||
id | ||
} | ||
} | ||
operatorSubscribedSolutionGroups(where: {solutionGroup: {id_eq: $solutionGroupId}}, limit: $limit, offset: $offset) { | ||
operator { | ||
id | ||
mappings { | ||
worker { | ||
id | ||
} | ||
} | ||
} | ||
} | ||
} | ||
`; | ||
const getKeyWithHighestNumber = (obj) => | ||
|
||
Object.keys(obj).reduce((a, b) => (obj[a] > obj[b] ? a : b)); | ||
|
||
function NodeConstructor(config) { | ||
this.ewxConfig = RED.nodes.getNode(config.ewxConfig); | ||
|
||
RED.nodes.createNode(this, config); | ||
|
||
var node = this; | ||
|
||
node.on('input', async function (msg, send, done) { | ||
if (!msg.payload.votingRoundId) { | ||
this.status({fill: "red", shape: "dot", text: "votingRoundId is missing"}); | ||
|
||
node.error("votingRoundId is missing"); | ||
|
||
return; | ||
} | ||
|
||
let electedLeader = null; | ||
|
||
let page = 0; | ||
const limit = 50; | ||
|
||
const resultCounts = {}; | ||
const operatorsMapping = {}; | ||
const submittedResults = []; | ||
|
||
while (true) { | ||
const response = await axios.post(node.ewxConfig.subsquidUrl, { | ||
query: gqlQuery, | ||
variables: { | ||
votingRoundId: msg.payload.votingRoundId, | ||
solutionGroupId: node.ewxConfig.solutionGroupId, | ||
solutionNamespace: node.ewxConfig.solutionNamespace, | ||
limit, | ||
offset: (page * limit) | ||
} | ||
}).catch((e) => { | ||
console.error(`failed during fetching data, solution: ${node.ewxConfig.solutionNamespace}`, e, e.response?.data); | ||
this.status({fill: "red", shape: "dot", text: "failed to query data"}); | ||
|
||
return null; | ||
}); | ||
|
||
if (response === null) { | ||
send({ | ||
payload: { | ||
leaderAddress: null, | ||
consensusStatus: ConsensusStatus.FAILED, | ||
attempt: msg.payload.attempt ? msg.payload.attempt + 1 : 1, | ||
shouldRetry: true, | ||
result: null | ||
} | ||
}); | ||
|
||
done(); | ||
|
||
return; | ||
} | ||
|
||
const {data} = response; | ||
|
||
const {solutionResultSubmitteds, operatorSubscribedSolutionGroups} = data.data; | ||
|
||
if (solutionResultSubmitteds.length === 0 && operatorSubscribedSolutionGroups.length === 0) { | ||
break; | ||
} | ||
|
||
submittedResults.push(...solutionResultSubmitteds); | ||
|
||
for (const {operator} of operatorSubscribedSolutionGroups) { | ||
if (operatorsMapping[operator.id]) { | ||
continue; | ||
} | ||
|
||
const mappings = operator.mappings; | ||
|
||
if (mappings.length === 0) { | ||
continue; | ||
} | ||
|
||
operatorsMapping[operator.id] = mappings[0].worker.id; | ||
} | ||
|
||
page++; | ||
} | ||
|
||
this.log(`votingRoundId = ${msg.payload.votingRoundId} - finished fetching consensus data`); | ||
|
||
const applicableOperatorsCount = Object.entries(operatorsMapping).length; | ||
|
||
if (applicableOperatorsCount < 3) { | ||
this.log(`votingRoundId = ${msg.payload.votingRoundId} - not enough operators for consensus`); | ||
this.status({fill: "red", shape: "dot", text: "not enough operators"}); | ||
|
||
send({ | ||
payload: { | ||
leaderAddress: null, | ||
consensusStatus: ConsensusStatus.FAILED, | ||
attempt: msg.payload.attempt ? msg.payload.attempt + 1 : 1, | ||
shouldRetry: false, | ||
result: null | ||
} | ||
}); | ||
|
||
return done(); | ||
} | ||
|
||
const hasAnyVotes = submittedResults.length > 0; | ||
|
||
if (!hasAnyVotes) { | ||
this.log(`votingRoundId = ${msg.payload.votingRoundId} - not enough votes for consensus`); | ||
|
||
this.status({fill: "yellow", shape: "dot", text: "not enough votes"}); | ||
|
||
send({ | ||
payload: { | ||
leaderAddress: null, | ||
consensusStatus: ConsensusStatus.NOT_ENOUGH_VOTES, | ||
attempt: msg.payload.attempt ? msg.payload.attempt + 1 : 1, | ||
shouldRetry: true, | ||
result: null | ||
} | ||
}); | ||
|
||
return done(); | ||
} | ||
|
||
|
||
const minVotesRequired = applicableOperatorsCount / 2 + 0.5; | ||
|
||
for (const {result, worker} of submittedResults) { | ||
resultCounts[result] = (resultCounts[result] || 0) + 1; | ||
|
||
if (resultCounts[result] >= minVotesRequired && electedLeader == null) { | ||
electedLeader = worker.id; | ||
} | ||
} | ||
|
||
if (electedLeader) { | ||
this.log(`votingRoundId = ${msg.payload.votingRoundId} - reached consensus`); | ||
|
||
this.status({fill: "green", shape: "dot", text: "reached"}); | ||
const resultHash = getKeyWithHighestNumber(resultCounts); | ||
|
||
send({ | ||
payload: { | ||
leaderAddress: electedLeader, | ||
consensusStatus: ConsensusStatus.REACHED, | ||
attempt: msg.payload.attempt ? msg.payload.attempt + 1 : 1, | ||
shouldRetry: false, | ||
resultHash | ||
} | ||
}); | ||
|
||
return done(); | ||
} | ||
|
||
const highestVote = resultCounts[getKeyWithHighestNumber(resultCounts)]; | ||
|
||
const remainingVotes = applicableOperatorsCount - submittedResults.length; | ||
|
||
const canStillReachConsensus = highestVote + remainingVotes >= minVotesRequired; | ||
|
||
if (!canStillReachConsensus) { | ||
this.status({fill: "red", shape: "dot", text: "unable to reach"}); | ||
this.log(`votingRoundId = ${msg.payload.votingRoundId} - unable to reach consensus`); | ||
|
||
send({ | ||
payload: { | ||
leaderAddress: null, | ||
consensusStatus: ConsensusStatus.UNABLE_TO_REACH_CONSENSUS, | ||
attempt: msg.payload.attempt ? msg.payload.attempt + 1 : 1, | ||
shouldRetry: false, | ||
result: null | ||
} | ||
}); | ||
|
||
return done(); | ||
} else { | ||
this.status({fill: "yellow", shape: "dot", text: "not enough votes"}); | ||
this.log(`votingRoundId = ${msg.payload.votingRoundId} - not enough votes`); | ||
|
||
|
||
send({ | ||
payload: { | ||
leaderAddress: null, | ||
consensusStatus: ConsensusStatus.NOT_ENOUGH_VOTES, | ||
attempt: msg.payload.attempt ? msg.payload.attempt + 1 : 1, | ||
shouldRetry: true, | ||
result: null | ||
} | ||
}); | ||
|
||
return done(); | ||
} | ||
}); | ||
} | ||
|
||
RED.nodes.registerType("consensus", NodeConstructor); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,21 @@ | ||
<script type="text/javascript"> | ||
RED.nodes.registerType('energywebx-config', { | ||
category: 'config', | ||
icon: "energywebx-logo.png", | ||
label: function() { | ||
return this.energywebxconfig; | ||
color: '#60A0FF', | ||
paletteLabel: 'Energy Web X Config', | ||
label: function () { | ||
return `${this.networkName.toUpperCase()} Config`; | ||
}, | ||
defaults: { | ||
networkName: { value: "ewx", required: true }, | ||
solutionNamespace: { value: "oep", required: true }, | ||
rpcUrl: { value: {} }, | ||
socketUrl: { value: {} }, | ||
explorerUrl: { value: {} } | ||
solutionNamespace: {value: "", default: "", required: true}, | ||
solutionGroupId: {value: "", default: "", required: true}, | ||
rpcUrl: {value: null, required: true}, | ||
subsquidUrl: {value: null, required: true}, | ||
workerUrl: {value: "", default: 'http://localhost:3002', required: true}, | ||
workerAddress: {value: "", default: ""} | ||
} | ||
}); | ||
</script> | ||
|
||
<script type="text/html" data-template-name="energywebx-config"> | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,58 @@ | ||
module.exports = function(RED) { | ||
|
||
const polkadot = require('@polkadot/api'); | ||
|
||
const URLS = { | ||
PEX: 'https://public-rpc.testnet.energywebx.com', | ||
MAINNET: 'https://public-rpc.mainnet.energywebx.com' | ||
}; | ||
|
||
const matchRpcToSubsquid = (rpcUrl) => { | ||
if (rpcUrl === URLS.PEX) { | ||
return 'https://ewx-subsquid-dev.energyweb.org/graphql' | ||
} else if (rpcUrl === URLS.MAINNET) { | ||
return 'https://ewx-indexer.mainnet.energywebx.com/graphql'; | ||
} | ||
|
||
return process.env.__EWX_SUBSQUID_URL; | ||
} | ||
|
||
|
||
module.exports = function (RED) { | ||
function EnergyWebXConfigNode(config) { | ||
|
||
RED.nodes.createNode(this, config); | ||
this.solutionNamespace = config.solutionNamespace; | ||
|
||
if (this.networkName === 'rex'){ | ||
this.rpcUrl = 'https://rex-rpc.energywebx.org/'; | ||
this.socketUrl = 'https://rex-rpc.energywebx.org/ws'; | ||
this.explorerUrl = 'https://rex-explorer.energywebx.org/api'; | ||
} else if (this.networkName === 'ewx') { | ||
this.rpcUrl = 'https://rpc.energywebx.org/'; | ||
this.socketUrl = 'https://rpc.energywebx.org/ws'; | ||
this.explorerUrl = 'https://explorer.energywebx.org/api'; | ||
} | ||
|
||
const ewxRemoteConfig = config.__envConfig; | ||
|
||
this.workerUrl = 'http://localhost:3002'; | ||
|
||
this.workerAddress = ewxRemoteConfig.EWX_WORKER_ADDRESS; | ||
this.solutionNamespace = ewxRemoteConfig.EWX_SOLUTION_ID; | ||
this.solutionGroupId = ewxRemoteConfig.EWX_SOLUTION_GROUP_ID; | ||
this.rpcUrl = ewxRemoteConfig.EWX_RPC_URL; | ||
this.subsquidUrl = matchRpcToSubsquid(this.rpcUrl); | ||
|
||
this.log(`worker address = ${this.workerAddress}, solution namespace = ${this.solutionNamespace}, solution group id = ${this.solutionGroupId}, rpc url = ${this.rpcUrl}, subsquid url = ${this.subsquidUrl}`) | ||
|
||
const provider = new polkadot.HttpProvider(this.rpcUrl); | ||
|
||
const api = new polkadot.ApiPromise({ | ||
provider, | ||
throwOnUnknown: true, | ||
throwOnConnect: true, | ||
}); | ||
|
||
api.connect() | ||
.then(() => { | ||
this.log(`connected to ${this.rpcUrl}`); | ||
|
||
this.status({fill: "green", shape: "dot", text: "connected"}); | ||
}) | ||
.catch((e) => { | ||
this.log(e); | ||
|
||
this.status({fill: "red", shape: "ring", text: "disconnected"}); | ||
}) | ||
} | ||
|
||
RED.nodes.registerType("energywebx-config", EnergyWebXConfigNode); | ||
} |
Oops, something went wrong.