diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index d921f2084..1fdafc237 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -822,9 +822,11 @@ "not-connected-message": "The application is not connected to a server.", "dao-confirm-tally": "You are voting for {{voteValue}} on:
{{proposalName}}

Confirm vote in your blockchain wallet.
", "dao-default-tally": "You are voting on
{{proposalName}}

Confirm vote in your blockchain wallet.
", - "moloch-guild": "Guild.", + "moloch-guild": "Budget.", "guild-total-shares": "Total Shares", "guild-total-assets": "Total Assets", "guild-share-value": "Value per Share", - "guild-total-value": "Total Value" + "guild-total-value": "Total Value", + "guild-voting-address": "Voter", + "guild-voting-addresses": "Voters" } diff --git a/imports/api/collectives/Collectives.js b/imports/api/collectives/Collectives.js index c9a128a92..28c752b6e 100644 --- a/imports/api/collectives/Collectives.js +++ b/imports/api/collectives/Collectives.js @@ -97,6 +97,51 @@ Schema.Menu = new SimpleSchema({ }, }); +Schema.Dataset = new SimpleSchema({ + data: { + type: Array, + optional: true, + defaultValue: [], + }, + 'data.$': { + type: Object, + optional: true, + }, + 'data.$.t': { + type: Date, + optional: true, + }, + 'data.$.x': { + type: Number, + optional: true, + decimal: true, + }, + 'data.$.y': { + type: Number, + optional: true, + decimal: true, + }, +}); + +Schema.Chart = new SimpleSchema({ + guildLabel: { + type: String, + optional: true, + }, + type: { + type: String, + optional: true, + }, + dataset: { + type: [Schema.Dataset], + optional: true, + }, + lastSyncedBlock: { + type: Number, + optional: true, + }, +}); + Schema.CollectiveProfile = new SimpleSchema({ website: { type: String, @@ -111,6 +156,10 @@ Schema.CollectiveProfile = new SimpleSchema({ type: [Parameter], optional: true, }, + chart: { + type: [Schema.Chart], + optional: true, + }, blockchain: { type: Blockchain, optional: true, @@ -161,6 +210,10 @@ Schema.CollectiveProfile = new SimpleSchema({ type: Date, optional: true, }, + lastSyncedBlock: { + type: Number, + optional: true, + }, }); diff --git a/imports/api/server/methods.js b/imports/api/server/methods.js index 6f668c735..caa08e8b7 100644 --- a/imports/api/server/methods.js +++ b/imports/api/server/methods.js @@ -11,7 +11,7 @@ import { getTime } from '/imports/api/time'; import { logUser, log, defaults, gui } from '/lib/const'; import { stripHTML, urlDoctor, fixDBUrl } from '/lib/utils'; import { notifierHTML } from '/imports/api/notifier/notifierTemplate.js'; -import { computeDAOStats } from '/lib/dao'; +import { refreshDAOs } from '/lib/dao'; import { getLastTimestamp, getBlockHeight } from '/lib/web3'; import { Collectives } from '/imports/api/collectives/Collectives'; @@ -388,7 +388,7 @@ Meteor.methods({ Contracts.update({ _id: feed[i]._id }, { $set: { period: newPeriod } }); } } - computeDAOStats(); + refreshDAOs(); }, async getBlock(collectives) { diff --git a/imports/startup/both/modules/metamask.js b/imports/startup/both/modules/metamask.js index 0e8ab8c7a..0f5d98702 100644 --- a/imports/startup/both/modules/metamask.js +++ b/imports/startup/both/modules/metamask.js @@ -382,47 +382,11 @@ const _hasRightToVote = async (memberAddress, proposalIndex, collectiveId) => { */ const _submitVote = async (proposalIndex, uintVote, contract, choice) => { const res = await _callDAOMethod('submitVote', [proposalIndex, uintVote], choice.collectiveId, 'send', { from: Meteor.user().username }); + console.log(res); if (res) { _pendingTransaction(Meteor.user().username, res, contract, choice); displayModal(false, modal); } - - /** - const collective = Collectives.findOne({ _id: choice.collectiveId }); - const smartContracts = collective.profile.blockchain.smartContracts; - - const map = _getMethodMap(smartContracts, 'submitVote'); - const contractABI = JSON.parse(map.abi); - - const dao = await new web3.eth.Contract(contractABI, map.publicAddress); - - await dao.methods[`${'submitVote'}`](proposalIndex, uintVote).send({ from: Meteor.user().username }, (err, res) => { - if (err) { - let message; - switch (err.code) { - case -32603: - message = TAPi18n.__('metamask-invalid-address'); - break; - case 4001: - default: - message = TAPi18n.__('metamask-denied-signature'); - } - displayModal( - true, - { - icon: Meteor.settings.public.app.logo, - title: TAPi18n.__('wallet'), - message, - cancel: TAPi18n.__('close'), - alertMode: true, - }, - ); - return err; - } - _pendingTransaction(Meteor.user().username, res, contract, choice); - displayModal(false, modal); - return res; - });*/ }; /** diff --git a/imports/ui/templates/components/collective/guild/guild.html b/imports/ui/templates/components/collective/guild/guild.html index 968e6dbf4..32b2b7a3b 100644 --- a/imports/ui/templates/components/collective/guild/guild.html +++ b/imports/ui/templates/components/collective/guild/guild.html @@ -10,11 +10,20 @@
{{name}}
-
+
+
+
+ +
{{{members}}}
+
+
- {{{publicAddress}}} + {{{publicAddress}}}
+ {{#with guildChart}} + {{> chart}} + {{/with}}
@@ -47,7 +56,7 @@ {{/with}}
-
+
{{_ 'guild-total-value'}}
diff --git a/imports/ui/templates/components/collective/guild/guild.js b/imports/ui/templates/components/collective/guild/guild.js index fdaa3509e..ca1e33cb4 100644 --- a/imports/ui/templates/components/collective/guild/guild.js +++ b/imports/ui/templates/components/collective/guild/guild.js @@ -8,6 +8,7 @@ import { getCoin } from '/imports/api/blockchain/modules/web3Util'; import '/imports/ui/templates/components/collective/guild/guild.html'; import '/imports/ui/templates/components/decision/balance/balance.js'; +import '/imports/ui/templates/widgets/chart/chart.js'; const standardBalance = { @@ -24,7 +25,6 @@ const standardBalance = { value: 100, }; - Template.guild.onCreated(function () { Template.instance().collective = new ReactiveVar(); Template.instance().ready = new ReactiveVar(false); @@ -85,7 +85,7 @@ Template.guild.helpers({ }, totalValue() { const row = _getRow('guild-total-value', Template.instance()); - row.tokenTotal = true; + row.tokenTotal = false; return row; }, getImage(pic) { @@ -93,10 +93,13 @@ Template.guild.helpers({ }, members() { const count = Template.instance().memberCount.get(); - if (count === 1) { - return `${count} ${TAPi18n.__('member')}`; + if (count) { + if (count === 1) { + return `${count} ${TAPi18n.__('guild-voting-address')}`; + } + return `${count} ${TAPi18n.__('guild-voting-addresses')}`; } - return `${count} ${TAPi18n.__('members')}`; + return ''; }, totalStyle() { const coin = getCoin(Template.instance().collective.get().profile.blockchain.coin.code); @@ -113,4 +116,7 @@ Template.guild.helpers({ blockchainLink() { return `${Meteor.settings.public.web.sites.blockExplorer}/address/${Template.instance().collective.get().profile.blockchain.publicAddress}`; }, + guildChart() { + return { collectiveId: this.collectiveId, guildLabel: 'guild-total-assets' }; + }, }); diff --git a/imports/ui/templates/components/decision/ballot/ballot.js b/imports/ui/templates/components/decision/ballot/ballot.js index ed8d78fce..1aa7a3d3e 100644 --- a/imports/ui/templates/components/decision/ballot/ballot.js +++ b/imports/ui/templates/components/decision/ballot/ballot.js @@ -180,11 +180,11 @@ const _cryptoVote = async () => { switch (voteValue) { case defaults.YES: message = TAPi18n.__('dao-confirm-tally').replace('{{voteValue}}', TAPi18n.__('yes')).replace('{{proposalName}}', getProposalDescription(poll.title, true)); - submitVote(poll.importId.toNumber(), 1, poll, contract); + await submitVote(poll.importId.toNumber(), 1, poll, contract); break; case defaults.NO: message = TAPi18n.__('dao-confirm-tally').replace('{{voteValue}}', TAPi18n.__('no')).replace('{{proposalName}}', getProposalDescription(poll.title, true)); - submitVote(poll.importId.toNumber(), 2, poll, contract); + await submitVote(poll.importId.toNumber(), 2, poll, contract); break; default: message = TAPi18n.__('dao-default-tally').replace('{{proposalName}}', getProposalDescription(poll.title, true)); diff --git a/imports/ui/templates/layout/sidebar/sidebar.html b/imports/ui/templates/layout/sidebar/sidebar.html index 9c7932331..3dbb7e2e3 100644 --- a/imports/ui/templates/layout/sidebar/sidebar.html +++ b/imports/ui/templates/layout/sidebar/sidebar.html @@ -10,7 +10,6 @@
diff --git a/imports/ui/templates/layout/sidebar/sidebar.js b/imports/ui/templates/layout/sidebar/sidebar.js index aa6dadec9..aaf47ba54 100644 --- a/imports/ui/templates/layout/sidebar/sidebar.js +++ b/imports/ui/templates/layout/sidebar/sidebar.js @@ -314,7 +314,7 @@ Template.sidebar.helpers({ return 0; }, replicator() { - return `· ${TAPi18n.__('start-a-democracy')}`; + return `${TAPi18n.__('start-a-democracy')}`; }, totalDelegates() { if (Template.instance().delegates.get()) { diff --git a/imports/ui/templates/layout/url/home/home.js b/imports/ui/templates/layout/url/home/home.js index 58b1d6e34..9c9a93b4c 100644 --- a/imports/ui/templates/layout/url/home/home.js +++ b/imports/ui/templates/layout/url/home/home.js @@ -143,7 +143,6 @@ Template.homeFeed.onCreated(function () { instance.autorun(function (computation) { if (subscription.ready()) { - console.log(Contracts.findOne()); const collectiveId = Contracts.findOne().collectiveId; Session.set('search', { input: '', diff --git a/imports/ui/templates/widgets/chart/chart.html b/imports/ui/templates/widgets/chart/chart.html new file mode 100644 index 000000000..6af750f3c --- /dev/null +++ b/imports/ui/templates/widgets/chart/chart.html @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/imports/ui/templates/widgets/chart/chart.js b/imports/ui/templates/widgets/chart/chart.js new file mode 100644 index 000000000..805e0e71b --- /dev/null +++ b/imports/ui/templates/widgets/chart/chart.js @@ -0,0 +1,140 @@ +import { Template } from 'meteor/templating'; +import { Collectives } from '/imports/api/collectives/Collectives'; + +import { getCoin } from '/imports/api/blockchain/modules/web3Util'; + +import '/imports/ui/templates/widgets/chart/chart.html'; + +const Chart = require('chart.js'); + +/** + * Shorten number to thousands, millions, billions, etc. + * http://en.wikipedia.org/wiki/Metric_prefix + * + * @param {number} num Number to shorten. + * @param {number} [digits=0] The number of digits to appear after the decimal point. + * @returns {string|number} + * + * @example + * // returns '12.5k' + * shortenLargeNumber(12543, 1) + * + * @example + * // returns '-13k' + * shortenLargeNumber(-12567) + * + * @example + * // returns '51M' + * shortenLargeNumber(51000000) + * + * @example + * // returns 651 + * shortenLargeNumber(651) + * + * @example + * // returns 0.12345 + * shortenLargeNumber(0.12345) + */ +const shortenLargeNumber = (num, digits) => { + const units = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + let decimal; + + for (let i = units.length - 1; i >= 0; i -= 1) { + decimal = Math.pow(1000, i + 1); + + if (num <= -decimal || num >= decimal) { + return +(num / decimal).toFixed(digits) + units[i]; + } + } + + return num; +}; + +const defaultChart = { + type: 'line', + data: { + datasets: [{ + data: [], + }], + }, + options: { + animation: { + duration: 0, + }, + tooltips: { enabled: false }, + hover: { mode: null }, + elements: { + point: { + radius: 0, + }, + line: { + tension: 0, + }, + }, + responsive: true, + maintainAspectRatio: false, + legend: { + display: false, + }, + scales: { + yAxes: [{ + display: true, + gridLines: { + drawBorder: false, + zeroLineWidth: 0, + borderDash: [2, 2], + display: true, + color: '#f1f1f1', + }, + stacked: true, + ticks: { + fontColor: '#7d849f', + fontSize: 9, + beginAtZero: false, + maxTicksLimit: 6, + }, + }], + xAxes: [{ + display: true, + type: 'time', + distribution: 'linear', + gridLines: { + borderDash: [2, 2], + zeroLineWidth: 0, + display: true, + color: '#f1f1f1', + }, + stacked: true, + ticks: { + fontSize: 9, + fontColor: '#7d849f', + autoSkip: false, + maxRotation: 0, + minRotation: 0, + beginAtZero: true, + maxTicksLimit: 6, + }, + }], + }, + }, +}; + +const _setupChart = (collectiveId, guildLabel) => { + const ctx = document.getElementById(`collectiveChart-${collectiveId}`); + const collective = Collectives.findOne({ _id: collectiveId }); + const token = getCoin(_.findWhere(collective.profile.guild, { name: guildLabel }).type.replace('token.', '')); + const finalChart = defaultChart; + finalChart.data.datasets = _.findWhere(collective.profile.chart, { guildLabel }).dataset; + finalChart.data.datasets[0].backgroundColor = [`${token.color}33`]; + finalChart.data.datasets[0].borderColor = [token.color]; + finalChart.data.datasets[0].borderWidth = 1.5; + finalChart.data.datasets[0].lineTension = 0.5; + finalChart.options.scales.yAxes[0].ticks.callback = (value) => { + return token ? shortenLargeNumber(value, 0) : value; + }; + return new Chart(ctx, finalChart); +}; + +Template.chart.onRendered(function () { + _setupChart(Template.instance().data.collectiveId, Template.instance().data.guildLabel); +}); diff --git a/imports/ui/templates/widgets/feed/feed.js b/imports/ui/templates/widgets/feed/feed.js index 4d5fa59d9..9069b247c 100644 --- a/imports/ui/templates/widgets/feed/feed.js +++ b/imports/ui/templates/widgets/feed/feed.js @@ -294,7 +294,6 @@ Template.feed.helpers({ } Template.instance().lastItemDate.set(feed[feed.length - 1].timestamp); } - console.log(feed); return feed; }, lastItem() { diff --git a/imports/ui/templates/widgets/feed/feedItem.html b/imports/ui/templates/widgets/feed/feedItem.html index 9cc51658b..e5839b216 100644 --- a/imports/ui/templates/widgets/feed/feedItem.html +++ b/imports/ui/templates/widgets/feed/feedItem.html @@ -114,12 +114,6 @@ {{else}} {{{description}}} -
-
- {{{_ 'moloch-contract-parameters'}}} - {{> help tooltip='moloch-contract-parameters-tooltip'}} -
-
@@ -162,27 +156,6 @@
{{#unless ragequit}} -
- {{#unless onChainVote}} - {{#if quadraticEnabled}} -
- {{{_ 'election-rule-quadratic'}}} - {{> help tooltip='voting-editor-quadratic-tooltip'}} -
- {{/if}} - {{#if balanceEnabled}} -
- {{{_ 'election-rule-balance'}}} - {{> help tooltip='voting-editor-balance-tooltip'}} -
- {{/if}} - {{else}} -
- {{{_ 'election-rule-onchain'}}} - {{> help tooltip='voting-editor-onchain-tooltip'}} -
- {{/unless}} -
{{#if requiresClosing}} {{#with closingData}} {{> countdown}} diff --git a/lib/const.js b/lib/const.js index baed9cfaf..473181614 100644 --- a/lib/const.js +++ b/lib/const.js @@ -111,6 +111,7 @@ const _defaults = { ROOT: 'ETH', YES: 'YES', NO: 'NO', + START_BLOCK: 5000000, }; /** diff --git a/lib/dao.js b/lib/dao.js index 6ced969a3..0b9e4d02d 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; import { Collectives } from '/imports/api/collectives/Collectives'; -import { getEvents, syncDAOGuilds } from '/lib/web3'; -import { log } from '/lib/const'; +import { getEvents, syncDAOGuilds, getBlockHeight } from '/lib/web3'; +import { log, defaults } from '/lib/const'; import { Contracts } from '/imports/api/contracts/Contracts'; import { Tokens } from '/imports/api/tokens/tokens'; const daoCollectives = []; +let lastServerSyncedBlock = 0; const _daoToCollective = (dao) => { Collectives.insert(dao, (error, result) => { @@ -99,45 +100,6 @@ const _setupTokens = async () => { return Tokens.find().fetch(); }; -/** -* @summary from collection of collectives, get all the events related to them on chain -*/ -const _insertDAOEvents = async () => { - let collectives; - let noCollectives = true; - while (noCollectives) { - collectives = Collectives.find().fetch(); - if (collectives.length === 0) { - noCollectives = true; - } else { - noCollectives = false; - } - } - log(`[dao] Found a total of ${collectives.length} collectives to parse.`); - const daoLogs = []; - for (let i = 0; i < collectives.length; i += 1) { - if (collectives[i].profile.blockchain) { - log(`[dao] Processing ${collectives[i].name}...`); - if (collectives[i].profile.blockchain.publicAddress && collectives[i].profile.blockchain.coin.code) { - log(`[dao] Updating wallet of ${collectives[i].name}...`); - // await updateWallet(collectives[i].profile.blockchain.publicAddress, collectives[i].profile.blockchain.coin.code); - } - if (collectives[i].profile.blockchain.smartContracts && collectives[i].profile.blockchain.smartContracts.length > 0) { - log(`[dao] Reading smart contracts of ${collectives[i].name}...`); - for (let k = 0; k < collectives[i].profile.blockchain.smartContracts.length; k += 1) { - if (collectives[i].profile.blockchain.smartContracts[k].abi && !collectives[i].profile.blockchain.smartContracts[k].EIP) { - await getEvents(collectives[i].profile.blockchain.smartContracts[k], collectives[i]._id).then((res) => { - daoLogs.push(res); - }); - } - } - } - } - } - - return daoLogs; -}; - /** * @summary persists the latest stats of a DAO */ @@ -167,14 +129,46 @@ const _computeDAOStats = () => { } }; +/** +* @summary dynamic refresh while app runs on latest activity per smart contract +*/ +const _refreshDAOs = async () => { + let collectives; + let noCollectives = true; + while (noCollectives) { + collectives = Collectives.find().fetch(); + if (collectives.length === 0) { + noCollectives = true; + } else { + noCollectives = false; + } + } + const currentBlock = await getBlockHeight(); + if (lastServerSyncedBlock < currentBlock) { + log(`[dao] Refreshing DAO activity on block height ${currentBlock}...`); + + let syncFrom; + for (let i = 0; i < collectives.length; i += 1) { + if (collectives[i].profile.blockchain && collectives[i].profile.blockchain.smartContracts && collectives[i].profile.blockchain.smartContracts.length > 0) { + for (let k = 0; k < collectives[i].profile.blockchain.smartContracts.length; k += 1) { + syncFrom = (collectives[i].profile.lastSyncedBlock) ? (collectives[i].profile.lastSyncedBlock + 1) : defaults.START_BLOCK; + if (syncFrom < currentBlock) { + await getEvents(collectives[i].profile.blockchain.smartContracts[k], collectives[i]._id, syncFrom, 'latest'); + } + } + } + } + lastServerSyncedBlock = currentBlock; + } +}; + /** * @summary setup DAOs on this server instance */ const _setupDAOs = async () => { await _insertDAOs().then(async (res) => { if (res) { - await _insertDAOEvents(); - _computeDAOStats(); + await _refreshDAOs(); await syncDAOGuilds(); } }); @@ -190,3 +184,4 @@ if (Meteor.isServer) { } export const computeDAOStats = _computeDAOStats; +export const refreshDAOs = _refreshDAOs; diff --git a/lib/web3.js b/lib/web3.js index 760026ece..ec94d0786 100644 --- a/lib/web3.js +++ b/lib/web3.js @@ -9,13 +9,14 @@ import { BigNumber } from 'bignumber.js'; import { migrateAddress, getContractObject, getTransactionObject, parseContent, getFinality } from '/lib/interpreter'; import { log, defaults } from '/lib/const'; +import { computeDAOStats } from '/lib/dao'; import erc20 from 'human-standard-token-abi'; import { Math } from 'core-js'; + const Web3 = require('web3'); -const START_BLOCK = 5000000; const precedentCache = []; let web3; @@ -163,7 +164,7 @@ const _getUserAddress = (res, collectiveId, role) => { * @param {string} collectiveId this is being subscribed to * @param {object} block with data from chain of this event */ -const _mirrorContractEvent = (event, map, state, collectiveId, block) => { +const _mirrorContractEvent = (event, state, collectiveId, block) => { log(`[web3] Mirroring blockchain event as contract action with collectiveId: ${collectiveId}...`); // create users required for this contract @@ -280,7 +281,7 @@ const _quickTally = (voter) => { * @param {string} collectiveId this is being subscribed to * @param {object} block with data from chain of this event */ -const _mirrorTransaction = (event, map, collectiveId, block) => { +const _mirrorTransaction = (event, collectiveId, block) => { // create users required for this transaction const authorUsername = _getUserAddress(event, collectiveId, 'MEMBER'); const user = Meteor.users.findOne({ username: authorUsername }); @@ -400,7 +401,7 @@ const _checkPrecedent = (username, list) => { * @param {string} collectiveId this is being subscribed to */ -const _mirrorTransactionState = (event, map, state, collectiveId, block) => { +const _mirrorTransactionState = (event, collectiveId, block) => { log(`[web3] Transaction state with collectiveId: ${collectiveId}...`); if (event.returnValues.didPass) { @@ -565,7 +566,7 @@ const _mirrorContractState = (state, index, collectiveId) => { * @param {string} collectiveId this is being subscribed to * @param {object} block with data from chain of this event */ -const _mirrorCollectiveEvent = (event, map, collectiveId, block) => { +const _mirrorCollectiveEvent = (collectiveId, block) => { log(`[web3] Mirroring collective data for ${collectiveId}`); const collective = Collectives.findOne({ _id: collectiveId }); @@ -589,7 +590,7 @@ const _mirrorCollectiveEvent = (event, map, collectiveId, block) => { * @param {string} collectiveId this is being subscribed to * @param {object} block with data from chain of this event */ -const _mirrorUserEvent = (event, map, collectiveId, block) => { +const _mirrorUserEvent = (event, collectiveId, block) => { const authorUsername = event.returnValues.memberAddress.toLowerCase(); const member = Meteor.users.findOne({ username: authorUsername }); @@ -725,6 +726,10 @@ const _mirrorUserEvent = (event, map, collectiveId, block) => { log(`[web3] Created a ragequit transaction with txId ${txId} ...`); }; +/** +* @summary gets the information of a given block. +* @param {number} blockNumber to fetch info from. +*/ const _getEventBlock = async (blockNumber) => { let block; log(`[web3] Getting data for block: ${blockNumber}`); @@ -807,31 +812,25 @@ const _writeEvents = async (event, smartContract, state, collectiveId) => { log(`[web3] Event timestamp found: ${eventTimestamp}`); for (let k = 0; k < map.length; k += 1) { - if (map[k].eventName === event[i].event) { - if (event[i].event === 'SubmitProposal') { - if (eventTimestamp >= lastEventBlockTimestamp) { - _mirrorContractEvent(event[i], map[k], state, collectiveId, block); - } - } - if (event[i].event === 'ProcessProposal') { - if (eventTimestamp >= lastEventBlockTimestamp) { - _mirrorTransactionState(event[i], map[k], state, collectiveId, block); - } - } - if (event[i].event === 'SubmitVote') { - if (eventTimestamp >= lastEventBlockTimestamp) { - _mirrorTransaction(event[i], map[k], collectiveId, block); - } - } - if (event[i].event === 'SummonComplete') { - if (eventTimestamp >= lastEventBlockTimestamp) { - _mirrorCollectiveEvent(event[i], map[k], collectiveId, block); - } - } - if (event[i].event === 'Ragequit') { - if (eventTimestamp >= lastEventBlockTimestamp) { - _mirrorUserEvent(event[i], map[k], collectiveId, block); - } + if ((map[k].eventName === event[i].event) && (eventTimestamp >= lastEventBlockTimestamp)) { + switch (event[i].event) { + case 'SubmitProposal': + _mirrorContractEvent(event[i], state, collectiveId, block); + break; + case 'ProcessProposal': + _mirrorTransactionState(event[i], collectiveId, block); + break; + case 'SubmitVote': + _mirrorTransaction(event[i], collectiveId, block); + break; + case 'SummonComplete': + _mirrorCollectiveEvent(collectiveId, block); + break; + case 'Ragequit': + _mirrorUserEvent(event[i], collectiveId, block); + break; + default: + log(`[web3] No interpreter function was found for event '${event[i].event}'`); } } } @@ -842,11 +841,11 @@ const _writeEvents = async (event, smartContract, state, collectiveId) => { // save the index of last event from reading contract state if (lastEventBlockTimestamp < currentEventBlockTimestamp) { - Collectives.update({ _id: collectiveId }, { $set: { 'profile.lastEventBlockTimestamp': currentEventBlockTimestamp } }, (err, res) => { + Collectives.update({ _id: collectiveId }, { $set: { 'profile.lastEventBlockTimestamp': currentEventBlockTimestamp, 'profile.lastSyncedBlock': event[parseInt(event.length - 1, 10)].blockNumber } }, (err, res) => { if (err) { console.log(err); } - log(`[web3] Updated collective with lastEventBlockTimestamp ${lastEventBlockTimestamp}`); + log(`[web3] Updated collective with lastEventBlockTimestamp ${lastEventBlockTimestamp} and lastSyncedBlock: ${event[parseInt(event.length - 1, 10)].blockNumber}`); return res; }); } @@ -927,34 +926,34 @@ const _getState = async (smartContract) => { * @param {object} smartContract object from a collective * @param {string} collectiveId this is being subscribed to */ -const _getEvents = async (smartContract, collectiveId) => { +const _getEvents = async (smartContract, collectiveId, fromBlock, toBlock) => { let eventLog; if (_web3()) { - log(`[web3] Getting past events for ${smartContract.publicAddress}..`); + log(`[web3] Getting events for ${smartContract.publicAddress} from block ${fromBlock} to ${toBlock}..`); const abi = JSON.parse(smartContract.abi); if (abi) { - const state = await _getState(smartContract); - let events = []; await new web3.eth.Contract(abi, smartContract.publicAddress).getPastEvents('allEvents', { - fromBlock: START_BLOCK, - toBlock: 'latest', + fromBlock, + toBlock, }, (error, res) => { if (error) { log('[web3] Error fetching log data.'); log(error); - } else { - log(`[web3] Events consist of: ${JSON.stringify(_.uniq(_.pluck(res, 'event')))}`); } events = res; return res; }); - log(`[web3] Log for ${smartContract.publicAddress} has a length of ${events.length} events.`); if (events.length > 0 && smartContract.map && smartContract.map.length > 0) { + log(`[web3] Log for ${smartContract.publicAddress} has a length of ${events.length} events.`); + const state = await _getState(smartContract); await _writeEvents(events, smartContract, state, collectiveId); + computeDAOStats(); + } else { + log('[web3] No new events found.'); } } } @@ -1130,6 +1129,113 @@ const _askOracle = async (coin) => { return finalNumber.toString(); }; +/** +* @summary gets all the transfers (from and to) that happened betweent a token and an address +* @param {object} coin with token data +* @param {string} publicAddress interacting with token +*/ +const _getTransfers = async (coin, publicAddress, eventName, fromBlock, toBlock, filter) => { + log(`[web3] Building chart for ${publicAddress} history with ${coin.code}...`); + let transfers; + + await new web3.eth.Contract(erc20, coin.contractAddress).getPastEvents(eventName, { + filter, + fromBlock, + toBlock, + }, (error, res) => { + if (error) { + log('[web3] Error fetching log data.'); + log(error); + } + transfers = res; + }); + + return transfers; +}; + + +/** +* @summary makes the information readable by the dapp +* @param {object} feed with blockchain events +* @param {object} coin with data to parse feed +*/ +const _generateDataset = async (feed, coin) => { + const dataset = []; + let block; + console.log(feed); + for (let i = 0; i < feed.length; i += 1) { + block = await _getEventBlock(feed[i].blockNumber); + dataset.push({ + t: new Date(block.timestamp * 1000), + y: parseFloat(_BNToNumber(feed[i].returnValues._value, coin), 10), + }); + } + return dataset; +}; + + +/** +* @summary turns a dataset with historic activity into a cumulative dataset +* @param {object} dataset to parse +*/ +const _cummulative = (dataset) => { + const cummlativeSet = dataset; + for (let i = 1; i < dataset.length; i += 1) { + cummlativeSet[i].y = parseFloat(cummlativeSet[i].y + cummlativeSet[i - 1].y, 10); + } + return cummlativeSet; +}; + + +/** +* @summary creates a chart based on the activity of a public address with a token +* @param {object} coin with token data +* @param {string} publicAddress interacting with token +*/ +const _buildChart = async (coin, publicAddress, fromBlock, toBlock) => { + const income = await _getTransfers(coin, publicAddress, 'Transfer', fromBlock, toBlock, { _to: publicAddress }); + const outcome = await _getTransfers(coin, publicAddress, 'Transfer', fromBlock, toBlock, { _from: publicAddress }); + const firstPass = await _generateDataset(income, coin); + const negativePass = _.map(await _generateDataset(outcome, coin), (item) => { const newItem = item; newItem.y *= -1; return newItem; }); + + const allBlocks = income.concat(outcome); + if (allBlocks.length > 0) { + const lastSyncedBlock = _.sortBy(allBlocks, 'blockNumber')[allBlocks.length - 1].blockNumber; + const dataset = _.reject(_.sortBy(firstPass.concat(negativePass), 't'), (item) => { return (item.y === 0); }); + const final = { + data: _cummulative(dataset), + lastSyncedBlock, + }; + return final; + } + + return undefined; +}; + + +/** +* @summary when an existing chart needs updating and new information is added to the cummulative +* @param {array} legacy values +* @param {array} update values +*/ +const _updateChart = (legacy, update) => { + const finalUpdate = legacy; + let newY; + for (let i = 0; i < update.length; i += 1) { + if (i === 0) { + newY = parseFloat(legacy[legacy.length - 1].y + update[i].y, 10); + } else { + newY = parseFloat(update[i].y + finalUpdate[finalUpdate.length - 1].y, 10); + } + finalUpdate.push({ + t: update[i].t, + y: newY, + }); + } + + return finalUpdate; +}; + /** * @summary gets the current value of the guild for each DAO */ @@ -1162,8 +1268,29 @@ const _syncDAOGuilds = async () => { if (coin) { daoGuild = _.findWhere(collectives[i].profile.blockchain.smartContracts, { label: 'GuildBank' }); finalGuild[k].value = await _balanceOf(coin, daoGuild.publicAddress); + const chart = _.findWhere(collectives[i].profile.chart, { guildLabel: collectives[i].profile.guild[k].name }); + let finalChart; + let dataset; + if (!chart || !chart.dataset || chart.dataset.length === 0) { + dataset = await _buildChart(coin, daoGuild.publicAddress, defaults.START_BLOCK, 'latest'); + finalChart = [{ + guildLabel: collectives[i].profile.guild[k].name, + type: 'lineal', + dataset: [dataset], + lastSyncedBlock: dataset.lastSyncedBlock, + }]; + } else if (chart.dataset && chart.dataset.length > 0) { + const initialBlock = parseInt(_.findWhere(collectives[i].profile.chart, { guildLabel: collectives[i].profile.guild[k].name }).lastSyncedBlock + 1, 10); + dataset = await _buildChart(coin, daoGuild.publicAddress, (!initialBlock) ? defaults.START_BLOCK : initialBlock, 'latest'); + finalChart = [chart]; + if (dataset) { + finalChart[0].dataset[0].data = _updateChart(finalChart[0].dataset[0].data, dataset.data); + finalChart[0].lastSyncedBlock = dataset.lastSyncedBlock; + } + } + log(`[dao] Total assets for this DAO... ${ticker} ${finalGuild[k].value}`); - Collectives.update({ _id: collectives[i]._id }, { $set: { 'profile.guild': finalGuild } }); + Collectives.update({ _id: collectives[i]._id }, { $set: { 'profile.guild': finalGuild, 'profile.chart': finalChart } }); } break; case 'guild-share-value': diff --git a/package-lock.json b/package-lock.json index a290dd641..fea47da10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2334,6 +2334,42 @@ "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", "dev": true }, + "chart.js": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.3.tgz", + "integrity": "sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==", + "requires": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "chartjs-color": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", + "integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==", + "requires": { + "chartjs-color-string": "^0.6.0", + "color-convert": "^1.9.3" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + } + } + }, + "chartjs-color-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", + "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", + "requires": { + "color-name": "^1.0.0" + } + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -8078,6 +8114,11 @@ } } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "more-entropy": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/more-entropy/-/more-entropy-0.0.7.tgz", diff --git a/package.json b/package.json index 3b26c8d27..b9c43f38e 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "blockstack": "^18.2.1", "brace-expansion": "^1.1.11", "braces": "^3.0.2", + "chart.js": "^2.9.3", "core-js": "^2.5.7", "cryptiles": "^4.1.2", "deep-extend": "^0.5.1", diff --git a/private/lib/dao.json b/private/lib/dao.json index 1577cd01b..b0d56ed68 100644 --- a/private/lib/dao.json +++ b/private/lib/dao.json @@ -5,7 +5,7 @@ "domain": "molochdao.com", "emails": [], "profile": { - "logo": "images/moloch.png", + "logo": "/images/moloch.png", "website": "https://molochdao.com", "bio": "A community DAO to fund Ethereum development in the name of Moloch the God of Coordination Failure.", "guild": [ diff --git a/private/lib/token.json b/private/lib/token.json index 6f38212eb..db8a008ca 100644 --- a/private/lib/token.json +++ b/private/lib/token.json @@ -2,7 +2,7 @@ "coin": [ { "code": "DAI", - "format": "0,0.0a", + "format": "0,0.00", "emoji": "", "unicode": "", "name": "Dai Stablecoin", @@ -13,7 +13,7 @@ "title": "DAI is a stable coin based on Collateral Debt Positions created with Maker DAO.", "type": "ERC20", "blockchain": "ETHEREUM", - "contractAddress": "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359", + "contractAddress": "0x6b175474e89094c44da98b954eedeac495271d0f", "defaultVote": "0.1", "editor": { "allowBalanceToggle:": true, @@ -140,6 +140,7 @@ "color": "#7885cb", "type": "ERC20", "blockchain": "ETHEREUM", + "abi": "[{\"constant\":true,\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"guy\",\"type\":\"address\"},{\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"src\",\"type\":\"address\"},{\"name\":\"dst\",\"type\":\"address\"},{\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"name\":\"\",\"type\":\"uint8\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"dst\",\"type\":\"address\"},{\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"deposit\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"},{\"name\":\"\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"src\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"guy\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"src\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"dst\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"dst\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"src\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"Withdrawal\",\"type\":\"event\"}]", "contractAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "defaultVote": "0.01", "editor": { diff --git a/public/templates/moloch/css/extra.css b/public/templates/moloch/css/extra.css index 389ab69ef..3e3052b2b 100644 --- a/public/templates/moloch/css/extra.css +++ b/public/templates/moloch/css/extra.css @@ -608,7 +608,7 @@ width: 1px; .election-rule { background: transparent; - color: #5a0074; + color: #48587d; font-size: 0.7em; display: inline-block; padding: 0px; @@ -789,11 +789,11 @@ width: 1px; } .parameter-name { -background: #f2f2f8; - color: #ae8abd; - margin: 0px -25px 10px; - border-top-left-radius: 4px; - border-top-right-radius: 4px; + background: #eceef2; + color: #8d95a9; + margin: 0px -25px 10px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; } .parameter-value { @@ -801,8 +801,8 @@ background: #f2f2f8; } .smart-contract { - margin: 0 auto; - text-align: left; + margin: 15px auto 0; + text-align: center; } .margin-top-minus-three { @@ -860,7 +860,7 @@ h4 { margin-bottom: 20px; padding-right: 15px; padding-left: 15px; - color: #9a63aa; + color: #48587d; font-size: 15px; line-height: 28px; font-weight: 100; @@ -1048,10 +1048,6 @@ h4 { box-shadow: none; } - body { - background-color: #f4f4f4; - } - .vote.vote-search.vote-feed.vote-delegation { background-color: #fff; } @@ -1135,7 +1131,7 @@ h4 { .guild-row-total { color: white; - background-color: #48587d; + background-color: #00c091; border-bottom: none; margin-bottom: -6px; border-bottom-left-radius: 4px; @@ -1147,10 +1143,10 @@ h4 { } .guild-icon { - width: 42px; - height: 42px; + width: 24px; + height: 24px; float: left; - margin: 10px 15px 10px 5px; + margin: 7px 10px 0px 0px; } .guild-title { @@ -1160,12 +1156,13 @@ h4 { margin-top: 0px; } -.guild-verifier { - margin-top: -10px; -} - .guild-detail { clear: left; + width: 100%; + position: relative; + display: inline-block; + padding: 5px 0px 0px; + margin-bottom: 0px; } .guild-name { @@ -1176,6 +1173,22 @@ h4 { font-size: 1.2em; } +.guild-chart { + width: 100%; +} + +.guild-meta { + float: left; + margin-right: 15px; + line-height: 28px; +} + +.guild-row-last { + border-bottom: 0px; + margin-bottom: -7px; + font-weight: 500; +} + .token-total { border: 1px solid white; color: white; @@ -1191,6 +1204,20 @@ h4 { } .vote.vote-search.vote-feed.vote-delegation.guild-block { - border: 3px solid #f9fafb; + border: 5px solid #f9fafb; +} + +.chart { + width: 100% !important; + height: 150px !important; + /* background: #ffffff; */ + /* border: #eef0f3 1px solid; */ + padding: 0px 0px 0px; + border-radius: 6px; + margin: 0px 0px 10px; } +.verifier.verifier-live.verifier-feed.verifier-mini.verifier-key.verifier-guild { + float: none; + display: inline-block; +} diff --git a/public/templates/moloch/css/moloch.css b/public/templates/moloch/css/moloch.css index 8ff4e04a4..b9ebb32b6 100755 --- a/public/templates/moloch/css/moloch.css +++ b/public/templates/moloch/css/moloch.css @@ -2323,7 +2323,7 @@ blockquote { .identity-label { display: inline-block; - color: #5a0075; + color: #48587d; font-size: 16px; line-height: 22px; text-decoration: none; @@ -5845,7 +5845,7 @@ blockquote { float: right; border-radius: 3px; /* background-color: #f9f9ff; */ - color: #5a0074; + color: #48587d; font-size: 10px; line-height: 18px; font-weight: 400; @@ -6668,9 +6668,9 @@ blockquote { padding-right: 5px; padding-left: 5px; float: right; - border: 1px solid #ff617e; + border: 1px solid #48587d; border-radius: 100px; - background-color: #ff617e; + background-color: #48587d; color: #fff; font-size: 10px; line-height: 15px;