forked from MyBitFoundation/github-proxy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
289 lines (247 loc) · 9.09 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
require('dotenv').config();
const express = require('express');
const Promise = require('bluebird');
const cors = require('cors')
const axios = require('axios');
const unit = require('ethjs-unit');
const web3 = require('web3');
const ethereumRegex = require('ethereum-regex');
const {
queryAllIssuesAndComments,
queryNextPageOfCommentsForIssue,
configForGraphGlRequest,
queryNextPageOfIssuesForRepo,
queryNextPageOfTimelineForIssue,
addressesUsedToFund,
mybitTickerCoinmarketcap,
etherscanEndPoint,
refreshTimeInSeconds} = require('./constants');
const parityContractAbi = require('./parityContractAbi');
const web3js = new web3(new web3.providers.HttpProvider(`https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`));
const parityRegistryContract = new web3js.eth.Contract(parityContractAbi, '0x5F0281910Af44bFb5fC7e86A404d0304B0e042F1');
let issues = [];
let fetchingIssues = false;
let numberOfUniqueContributors = 0;
let totalValueOfFund = 0;
let totalPayoutOfFund = 0;
let mybitInUsd = 0;
const app = express();
app.use(cors());
app.use(express.json());
app.get('/api/issues', (req, res) => {
res.send({
issues,
numberOfUniqueContributors,
totalValueOfFund,
totalPayoutOfFund
});
})
async function getCurrentUsdPriceOf(ticker){
const {data} = await axios(`https://api.coinmarketcap.com/v2/ticker/${ticker}/`)
return data.data.quotes.USD.price;
}
async function getTotalValue(){
let amountsPerAddress = await Promise.all(addressesUsedToFund.map(async address => {
const {data} = await axios(etherscanEndPoint(address))
let sent = 0;
let received = 0;
data.result.forEach(tx => {
//received
if(tx.to == address){
received += Number(unit.fromWei(tx.value, 'ether'))
}
else{
sent += Number(unit.fromWei(tx.value, 'ether'))
}
})
return {
sent,
received
}
}))
let totalValueTmp = 0;
amountsPerAddress.forEach(address => {
totalValueTmp += (address.received - address.sent)
})
return Number(totalValueTmp * mybitInUsd);
}
async function getErc20Symbol(address){
let tokenInfo = await parityRegistryContract.methods
.fromAddress(address)
.call()
return tokenInfo['1'];
}
async function getValueOfContract(contractAddress){
const {data} = await axios(`http://api.etherscan.io/api?module=account&action=tokentx&address=${contractAddress}`)
let value = 0, tokenSymbol;
//case where the contract has no transactions
if(data.status === "0"){
return null;
}
//pull total value: sum of all transfers sent to the address
data.result.forEach(tx => {
if(tx.to === contractAddress){
value += Number(unit.fromWei(tx.value, 'ether'));
}
})
tokenSymbol = data.result[0].tokenSymbol;
if(tokenSymbol === ''){
tokenSymbol = await getErc20Symbol(data.result[0].contractAddress);
}
return {
tokenSymbol,
value
}
}
async function getNextPageOfCommentsOfIssue(reponame, issueNumber, cursor){
const { data } = await axios(configForGraphGlRequest(queryNextPageOfCommentsForIssue(reponame, issueNumber, cursor)))
return data;
}
async function getNextPageOfTimelineOfIssue(reponame, issueNumber, cursor){
const { data } = await axios(configForGraphGlRequest(queryNextPageOfTimelineForIssue(reponame, issueNumber, cursor)))
return data;
}
async function getNextPageOfIssuesForRepo(reponame, cursor){
const { data } = await axios(configForGraphGlRequest(queryNextPageOfIssuesForRepo(reponame, cursor)))
return data;
}
async function processIssues(totalFundValue){
//pull all the repositories with issues and comments
const response = await axios(configForGraphGlRequest(queryAllIssuesAndComments));
let repos = response.data.data.organization.repositories.edges;
let uniqueContributors = {};
let totalPayout = 0;
repos = await Promise.all(repos.map( async ({node}) => {
const repoName = node.name;
let topics = node.repositoryTopics.edges;
//check if the repo is ddf enabled
topics = topics.map(({node}) => node.topic.name);
if(!topics.includes("ddf-enabled")){
return null;
}
let issuesOfRepo = node.issues;
//handle pagination for issues of a given repo
while(issuesOfRepo.pageInfo.hasNextPage){
//pull the next page using the cursor (id of the last issue)
const nextPageOfIssues = await getNextPageOfIssuesForRepo(repoName, issuesOfRepo.edges[issuesOfRepo.edges.length - 1].cursor);
//merge current array of issues for a given repo with the result from the new page of issues
issuesOfRepo.edges = issuesOfRepo.edges.concat(nextPageOfIssues.data.repository.issues.edges);
//update the hasNextPage flag with the value of the newly requested page of issues
issuesOfRepo.pageInfo.hasNextPage = nextPageOfIssues.data.repository.issues.pageInfo.hasNextPage;
}
//map all issues to pull information about each issue
issuesOfRepo = await Promise.all(issuesOfRepo.edges.map( async ({node}) => {
const {createdAt, url, title, number, state} = node;
const labels = node.labels.edges.map(({node}) => node.name);
let comments = node.comments;
//handle comments pagination - same logic as above
while(comments.pageInfo.hasNextPage){
const nextPageComments = await getNextPageOfCommentsOfIssue(repoName, number, comments.edges[comments.edges.length - 1].cursor);
comments.edges = comments.edges.concat(nextPageComments.data.repository.issue.comments.edges);
comments.pageInfo.hasNextPage = nextPageComments.data.repository.issue.comments.pageInfo.hasNextPage;
}
comments = comments.edges;
let contractAddress;
for(let i = 0; i < comments.length; i++){
const author = comments[i].node.author.login;
//pull contract address
if(author === "status-open-bounty"){
const match = comments[i].node.body.match(ethereumRegex());
if(match && match.length > 0){
contractAddress = match[0];
}
}
}
const valueInfo = contractAddress && await getValueOfContract(contractAddress);
let merged = false;
let timeline = node.timeline;
//handle timeline (list of events) pagination - same logic as above
while(timeline.pageInfo.hasNextPage){
const nextPageTimeline = await getNextPageOfTimelineOfIssue(repoName, number, timeline.edges[timeline.edges.length - 1].cursor);
timeline.edges = timeline.edges.concat(nextPageTimeline.data.repository.issue.timeline.edges);
timeline.pageInfo.hasNextPage = nextPageTimeline.data.repository.issue.timeline.pageInfo.hasNextPage;
}
timeline = timeline.edges;
//determined whether a referenced PR was merged
timeline.forEach(({node}) => {
if(node.source && node.source.state === "MERGED"){
merged = true;
//the issue needs to have a valid contract with a value for us to consider this a contributor for the ddf
if(valueInfo){
uniqueContributors[node.source.author.login] = 0;
}
}
})
if(state === "CLOSED" && !merged){
return null;
}
if(merged && valueInfo){
totalPayout += Number(valueInfo.value * mybitInUsd);
}
if(!merged && valueInfo){
totalFundValue += Number(valueInfo.value * mybitInUsd);
}
return{
createdAt,
merged,
url,
title,
contractAddress,
repoName,
labels,
tokenSymbol: valueInfo && valueInfo.tokenSymbol,
value: valueInfo ? valueInfo.value : 0,
mybitInUsd: valueInfo ? Number(valueInfo.value * mybitInUsd).toFixed(2) : 0,
}
}))
issuesOfRepo = issuesOfRepo.filter(issue => issue && issue.contractAddress)
return issuesOfRepo
}))
repos = repos.filter(repo => repo !== null)
let issuesToReturn = [];
repos.forEach((issuesOfRepo, index) => {
issuesOfRepo.forEach(issue => issuesToReturn.push(issue));
});
numberOfUniqueContributors = Object.keys(uniqueContributors).length;
totalPayoutOfFund = totalPayout;
totalValueOfFund = totalFundValue;
return issuesToReturn;
}
function mainCycle(){
getCurrentUsdPriceOf(mybitTickerCoinmarketcap)
.then(val => {
mybitInUsd=val
getFundingInfo();
}).catch(err => {
console.log("Failed to fetch MYB price, error: " + err)
setTimeout(mainCycle, 2000);
return;
})
}
function getFundingInfo(){
getTotalValue()
.then(fetchAllIssues)
.catch(err => {
console.log("error fetching total fund value" + err);
setTimeout(getFundingInfo, 5000);
})
}
function fetchAllIssues(totalFundValue){
if(fetchingIssues) return;
fetchingIssues= true;
processIssues(totalFundValue).then(repos => {
issues = repos;
fetchingIssues = false;
console.log("Fetched all the issues.")
}).catch(err => {
fetchingIssues = false;
console.log(err);
console.log("Fetching issues again in 5 seconds.")
setTimeout(() => fetchAllIssues(totalFundValue), 5000);
})
}
mainCycle();
setInterval(mainCycle, refreshTimeInSeconds * 1000)
const port = process.env.PORT || 9001;
app.listen(port);
console.log(`Express app listening on port ${port}`);