From 6e01a677aebd4b202d96f57162efc5147fb1c7db Mon Sep 17 00:00:00 2001 From: Andrey Pastukhov Date: Fri, 29 Jul 2022 14:57:40 +1000 Subject: [PATCH] TC-98 | Add ProposalComments component with tab filters --- app/router.scrollBehavior.js | 9 + components/ProposalCommentFormModal.vue | 27 +- components/governance/Proposal.vue | 319 +++++++++--------- components/governance/ProposalComment.vue | 171 +++++++--- components/governance/ProposalComments.vue | 91 +++++ .../governance/ProposalCommentsSkeleton.vue | 46 ++- constants/variables.js | 2 + langs/en.json | 4 +- networkConfig.js | 4 +- services/index.js | 1 + services/lookupAddress.js | 114 +++++++ store/governance/gov.js | 131 +------ store/governance/proposal.js | 269 +++++++++++++++ utils/stringUtils.js | 24 ++ 14 files changed, 876 insertions(+), 336 deletions(-) create mode 100644 app/router.scrollBehavior.js create mode 100644 components/governance/ProposalComments.vue create mode 100644 services/lookupAddress.js create mode 100644 store/governance/proposal.js diff --git a/app/router.scrollBehavior.js b/app/router.scrollBehavior.js new file mode 100644 index 0000000..58c8c89 --- /dev/null +++ b/app/router.scrollBehavior.js @@ -0,0 +1,9 @@ +const routerScrollBehavior = (to, from, savedPosition) => { + if (to.name === 'governance-id') { + return { x: 0, y: 0 } + } + + return savedPosition || { x: 0, y: 0 } +} + +export default routerScrollBehavior diff --git a/components/ProposalCommentFormModal.vue b/components/ProposalCommentFormModal.vue index 9a95cfd..d4255c4 100644 --- a/components/ProposalCommentFormModal.vue +++ b/components/ProposalCommentFormModal.vue @@ -52,11 +52,25 @@ - + {{ $t('for') }} - + {{ $t('against') }} @@ -88,6 +102,11 @@ export default { message: '' } }), + computed: { + isValid() { + return this.validate() + } + }, methods: { validate() { const { form, fields, support } = this @@ -100,9 +119,7 @@ export default { return fields.contact && fields.message }, onCastVote() { - const isValid = this.validate() - - if (isValid) { + if (this.isValid) { this.$emit('castVote', this.form) this.$emit('close') } diff --git a/components/governance/Proposal.vue b/components/governance/Proposal.vue index f1b739e..c7690e3 100644 --- a/components/governance/Proposal.vue +++ b/components/governance/Proposal.vue @@ -4,158 +4,159 @@

{{ data.title }}

-

{{ data.description }}

-
- -
- - +

{{ data.description }}

+
-
-
{{ $t('castYourVote') }}
- -
- {{ $t('for') }} - {{ $t('against') }} -
-
- - - -
-
-
{{ $t('executeProposal') }}
- - {{ $t('execute') }} - -
-
-
{{ $t('currentResults') }}
-
- {{ $t('for') }} - TORN / {{ calculatePercent('for') }}% -
- -
- {{ $t('against') }} - TORN / - {{ calculatePercent('against') }}% -
- -
- {{ $t('quorum') }} +
+
+
{{ $t('castYourVote') }}
- +
+ {{ $t('for') }} + {{ $t('against') }} +
- TORN / - {{ quorumPercent }}% + +
- -
-
-
{{ $t('information') }}
-
-
- {{ $t('proposalAddress') }} -
- - {{ data.target }} - +
+
{{ $t('executeProposal') }}
+ + {{ $t('execute') }} + +
+ +
+
{{ $t('currentResults') }}
+
+ {{ $t('for') }} + TORN / {{ calculatePercent('for') }}% +
+ +
+ {{ $t('against') }} + TORN / + {{ calculatePercent('against') }}% +
+ +
+ {{ $t('quorum') }} + + + + TORN + / {{ quorumPercent }}% +
+ +
+
+
{{ $t('information') }}
+
+
+ {{ $t('proposalAddress') }} +
-
-
- {{ $t('id') }} -
{{ data.id }}
-
-
- {{ $t('status') }} -
- {{ $t(data.status) }} +
+ {{ $t('id') }} +
{{ data.id }}
-
-
- {{ $t('startDate') }} -
{{ $moment.unix(data.startTime).format('llll') }}
-
-
- {{ $t('endDate') }} -
{{ $moment.unix(data.endTime).format('llll') }}
-
-
- {{ $t(timerLabel) }} -
- {{ countdown }} +
+ {{ $t('status') }} +
+ {{ $t(data.status) }} +
+
+
+ {{ $t('startDate') }} +
{{ $moment.unix(data.startTime).format('llll') }}
+
+
+ {{ $t('endDate') }} +
{{ $moment.unix(data.endTime).format('llll') }}
+
+
+ {{ $t(timerLabel) }} +
+ {{ countdown }} +
@@ -168,8 +169,7 @@ + + diff --git a/components/governance/ProposalComment.vue b/components/governance/ProposalComment.vue index c5dcf6e..a57eab2 100644 --- a/components/governance/ProposalComment.vue +++ b/components/governance/ProposalComment.vue @@ -2,39 +2,71 @@
-
- TORN +
+
+ TORN +
- -
{{ $t('delegated') }}
-
+ - -
{{ shortVoter }}
-
-
- - {{ contact }} -
+
+ + + + + +
diff --git a/components/governance/ProposalComments.vue b/components/governance/ProposalComments.vue new file mode 100644 index 0000000..8a81ea8 --- /dev/null +++ b/components/governance/ProposalComments.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/components/governance/ProposalCommentsSkeleton.vue b/components/governance/ProposalCommentsSkeleton.vue index 5b221b4..0605df8 100644 --- a/components/governance/ProposalCommentsSkeleton.vue +++ b/components/governance/ProposalCommentsSkeleton.vue @@ -2,12 +2,26 @@
-
+
- +
+ +
+
+ +
+ +
+ +
-
- +
+
+ +
+
+ +
@@ -25,3 +39,27 @@ export default { } } + + diff --git a/constants/variables.js b/constants/variables.js index 8789410..35ab9b1 100644 --- a/constants/variables.js +++ b/constants/variables.js @@ -95,3 +95,5 @@ export const DUMMY_NONCE = '0x11111111111111111111111111111111111111111111111111 export const DUMMY_WITHDRAW_DATA = '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' + +export const CHUNK_COUNT_PER_BATCH_REQUEST = 200 diff --git a/langs/en.json b/langs/en.json index 1e05885..947c01f 100644 --- a/langs/en.json +++ b/langs/en.json @@ -285,10 +285,10 @@ }, "proposalComment": { "modal-title": "Title: Proposal #{id}", - "modal-subtitle": "Please provide feedback about your decision. Why are you against of this proposal?", + "modal-subtitle": "Please explain: Why are you for or against this proposal?", "form-contact": "Contact", "form-contact-placeholder": "Enter contact (optional)", - "form-contact-tooltip": "Contact (optional) may be nickname in forum, email, telegram, twitter or others", + "form-contact-tooltip": "Contact (optional) may be a nickname in forum, email, telegram, twitter or other", "form-message": "Message", "form-message-placeholder": "Enter message", "form-message-opt-placeholder": "Enter message (optional)", diff --git a/networkConfig.js b/networkConfig.js index a361120..787d931 100644 --- a/networkConfig.js +++ b/networkConfig.js @@ -116,7 +116,7 @@ export default { ensSubdomainKey: 'mainnet-tornado', pollInterval: 15, constants: { - GOVERNANCE_TORNADOCASH_BLOCK: 11474695, + GOVERNANCE_BLOCK: 11474695, NOTE_ACCOUNT_BLOCK: 11842486, ENCRYPTED_NOTES_BLOCK: 14248730, MINING_BLOCK_TIME: 15 @@ -535,7 +535,7 @@ export default { ensSubdomainKey: 'goerli-tornado', pollInterval: 15, constants: { - GOVERNANCE_TORNADOCASH_BLOCK: 3945171, + GOVERNANCE_BLOCK: 3945171, NOTE_ACCOUNT_BLOCK: 4131375, ENCRYPTED_NOTES_BLOCK: 4131375, MINING_BLOCK_TIME: 15 diff --git a/services/index.js b/services/index.js index 3e136c2..81d23fc 100644 --- a/services/index.js +++ b/services/index.js @@ -9,6 +9,7 @@ export * from './events' export { default as graph } from './graph' export { default as schema } from './schema' export { default as walletConnectConnector } from './walletConnect' +export * from './lookupAddress' // eslint-disable-next-line no-undef window.graph = graph diff --git a/services/lookupAddress.js b/services/lookupAddress.js new file mode 100644 index 0000000..66ac81c --- /dev/null +++ b/services/lookupAddress.js @@ -0,0 +1,114 @@ +// from https://github.com/ChainSafe/web3.js/issues/2683#issuecomment-547348416 + +import namehash from 'eth-ens-namehash' +import { BigNumber, utils } from 'ethers' +import ABI from 'web3-eth-ens/lib/resources/ABI/Resolver' +import uniq from 'lodash/uniq' +import chunk from 'lodash/chunk' +import { CHUNK_COUNT_PER_BATCH_REQUEST } from '@/constants' + +export const createBatchRequestCallback = (resolve, reject) => (error, data) => { + if (error) { + reject(error) + } else { + resolve(data) + } +} + +const CACHE = {} + +const createFetchNodeAddresses = (registryContract, batch) => async (address) => { + const addressLower = address.toLowerCase() + + const node = addressLower.substr(2) + '.addr.reverse' + const nodeHash = namehash.hash(node) + let nodeAddress = null + + if (!CACHE[addressLower]) { + try { + nodeAddress = await new Promise((resolve, reject) => { + const callback = createBatchRequestCallback(resolve, reject) + const requestData = registryContract.methods.resolver(nodeHash).call.request(callback) + batch.add(requestData) + }) + + if (+nodeAddress === 0) nodeAddress = null + } catch (error) { + console.error(`Error resolve ens for "${address}"`, error.message) + // do nothing + } + } + + return { + addressLower, + address, + nodeHash, + nodeAddress + } +} + +const createFetchEnsNames = (web3, batch, results) => async (data) => { + const { address, addressLower, nodeHash, nodeAddress } = data + if (!nodeAddress) return results + + if (CACHE[addressLower]) { + results[address] = CACHE[addressLower] + return results + } + + const nodeContract = new web3.eth.Contract(ABI, nodeAddress) + + try { + const ensName = await new Promise((resolve, reject) => { + const callback = createBatchRequestCallback(resolve, reject) + const requestData = nodeContract.methods.name(nodeHash).call.request(callback) + batch.add(requestData) + }) + + const isZeroAddress = + ensName.trim().length && utils.isAddress(ensName) && BigNumber.from(ensName).isZero() + + if (isZeroAddress) return results + + CACHE[addressLower] = ensName + results[address] = ensName + + return results + } catch (error) { + console.error(`Error lookupAddress ens for "${address}"`, error.message) + return results + } +} + +export const lookupAddressesRequest = async (addressList, web3, registryContract) => { + const fetchNodeAddressesBatch = new web3.BatchRequest() + const fetchNodeAddresses = createFetchNodeAddresses(registryContract, fetchNodeAddressesBatch) + const fetchNodeAddressesPromises = uniq(addressList).map(fetchNodeAddresses) + fetchNodeAddressesBatch.execute() + + const nodeAddresses = await Promise.all(fetchNodeAddressesPromises) + + const results = {} + const fetchEnsNamesBatch = new web3.BatchRequest() + const fetchEnsNames = createFetchEnsNames(web3, fetchEnsNamesBatch, results) + const fetchEnsNamesPromises = nodeAddresses.map(fetchEnsNames) + fetchEnsNamesBatch.execute() + + await Promise.all(fetchEnsNamesPromises) + return results +} + +export const lookupAddresses = async (addressList, web3) => { + const registryContract = await web3.eth.ens.registry.contract + // web3.eth.ens._lastSyncCheck = Date.now() // - need for test in fork + + const addressListChunks = chunk(addressList, CHUNK_COUNT_PER_BATCH_REQUEST) + let results = {} + + for await (const list of addressListChunks) { + const result = await lookupAddressesRequest(list, web3, registryContract) + results = { ...results, ...result } + } + + return results +} diff --git a/store/governance/gov.js b/store/governance/gov.js index 8d94b0b..5e7f69d 100644 --- a/store/governance/gov.js +++ b/store/governance/gov.js @@ -1,5 +1,6 @@ /* eslint-disable no-console */ /* eslint-disable import/order */ + import Web3 from 'web3' import { utils } from 'ethers' import { ToastProgrammatic as Toast } from 'buefy' @@ -16,19 +17,17 @@ const state = () => { approvalAmount: 'unlimited', lockedBalance: '0', isFetchingLockedBalance: false, - isFetchingProposalComments: false, currentDelegate: '0x0000000000000000000000000000000000000000', timestamp: 0, delegatedBalance: '0', isFetchingDelegatedBalance: false, delegators: [], - proposalComments: [], latestProposalId: { value: null, status: null }, isFetchingProposals: true, - isSaveProposal: false, + isCastingVote: false, proposals: [], voterReceipts: [], hasActiveProposals: false, @@ -74,9 +73,6 @@ const getters = { return isFetchingProposals }, - isFetchingProposalComments: (state) => { - return state.isFetchingProposalComments - }, votingPower: (state) => { return toBN(state.lockedBalance) .add(toBN(state.delegatedBalance)) @@ -108,11 +104,8 @@ const mutations = { SAVE_FETCHING_PROPOSALS(state, status) { this._vm.$set(state, 'isFetchingProposals', status) }, - SAVE_SAVE_PROPOSAL(state, status) { - this._vm.$set(state, 'isSaveProposal', status) - }, - SAVE_FETCHING_PROPOSAL_COMMENTS(state, status) { - this._vm.$set(state, 'isFetchingProposalComments', status) + SAVE_CASTING_VOTE(state, status) { + this._vm.$set(state, 'isCastingVote', status) }, SAVE_LOCKED_BALANCE(state, { balance }) { this._vm.$set(state, 'lockedBalance', balance) @@ -129,9 +122,6 @@ const mutations = { SAVE_DELEGATEE(state, { currentDelegate }) { this._vm.$set(state, 'currentDelegate', currentDelegate) }, - SAVE_PROPOSAL_COMMENTS(state, proposalComments) { - state.proposalComments = proposalComments - }, SAVE_PROPOSALS(state, proposals) { this._vm.$set(state, 'proposals', proposals) }, @@ -359,7 +349,7 @@ const actions = { const { getters, rootGetters, commit, rootState, dispatch, state } = context const { id, support, contact = '', message = '' } = payload - commit('SAVE_SAVE_PROPOSAL', true) + commit('SAVE_CASTING_VOTE', true) try { const { ethAccount } = rootState.metamask @@ -433,7 +423,7 @@ const actions = { ) } finally { dispatch('loading/disable', {}, { root: true }) - commit('SAVE_SAVE_PROPOSAL', false) + commit('SAVE_CASTING_VOTE', false) } }, async executeProposal({ getters, rootGetters, commit, rootState, dispatch }, { id }) { @@ -669,7 +659,7 @@ const actions = { const [events, statuses] = await Promise.all([ govInstance.getPastEvents('ProposalCreated', { - fromBlock: config.constants.GOVERNANCE_TORNADOCASH_BLOCK, + fromBlock: config.constants.GOVERNANCE_BLOCK, toBlock: 'latest' }), aggregatorContract.methods.getAllProposals(govInstance._address).call() @@ -819,14 +809,14 @@ const actions = { filter: { to: ethAccount }, - fromBlock: config.constants.GOVERNANCE_TORNADOCASH_BLOCK, + fromBlock: config.constants.GOVERNANCE_BLOCK, toBlock: 'latest' }) let undelegatedAccs = await govInstance.getPastEvents('Undelegated', { filter: { from: ethAccount }, - fromBlock: config.constants.GOVERNANCE_TORNADOCASH_BLOCK, + fromBlock: config.constants.GOVERNANCE_BLOCK, toBlock: 'latest' }) delegatedAccs = delegatedAccs.map((acc) => acc.returnValues.account) @@ -883,109 +873,6 @@ const actions = { console.error('fetchReceipt', e.message) } }, - async fetchProposalComments(context, payload) { - const { getters, rootGetters, commit, state } = context - const { id: proposalId } = payload - let { blockNumber: fromBlock } = payload - - commit('SAVE_FETCHING_PROPOSAL_COMMENTS', true) - - let { proposalComments } = state - if (proposalComments[0]?.id === proposalId) { - fromBlock = proposalComments[0].blockNumber + 1 - } else { - commit('SAVE_PROPOSAL_COMMENTS', []) - proposalComments = [] - } - - try { - const netId = rootGetters['metamask/netId'] - console.log('fetchProposalComments', proposalId) - const govInstance = getters.govContract({ netId }) - const web3 = getters.getWeb3({ netId }) - const CACHE_TX = {} - const CACHE_BLOCK = {} - - const getComment = (calldata) => { - const empty = { contact: '', message: '' } - if (!calldata) return empty - - const methodLength = 4 // length of castDelegatedVote method - const result = utils.defaultAbiCoder.decode( - ['address[]', 'uint256', 'bool'], - utils.hexDataSlice(calldata, methodLength) - ) - const data = govInstance.methods.castDelegatedVote(...result).encodeABI() - const dataLength = utils.hexDataLength(data) - - try { - const str = utils.defaultAbiCoder.decode(['string'], utils.hexDataSlice(calldata, dataLength)) - const [contact, message] = JSON.parse(str) - return { contact, message } - } catch { - return empty - } - } - - let votedEvents = await govInstance.getPastEvents('Voted', { - filter: { - // support: [false], - proposalId - }, - fromBlock, - toBlock: 'latest' - }) - - votedEvents = votedEvents.filter((event) => event.blockNumber >= fromBlock) - - const promises = votedEvents.map(async (votedEvent) => { - const { transactionHash, returnValues, blockNumber } = votedEvent - const { voter, support } = returnValues - - CACHE_TX[transactionHash] = CACHE_TX[transactionHash] || web3.eth.getTransaction(transactionHash) - CACHE_BLOCK[blockNumber] = CACHE_BLOCK[blockNumber] || web3.eth.getBlock(blockNumber) - - const [tx, blockInfo] = await Promise.all([CACHE_TX[transactionHash], CACHE_BLOCK[blockNumber]]) - - const isMaybeHasComment = support === false && voter === tx.from - const comment = isMaybeHasComment ? getComment(tx.input) : getComment() - - return { - id: `${transactionHash}-${voter}`, - proposalId, - ...returnValues, - ...comment, - - revote: false, - votes: fromWei(returnValues.votes), - transactionHash, - from: tx.from, - delegator: voter === tx.from ? null : tx.from, - timestamp: blockInfo.timestamp, - blockNumber - } - }) - - let newProposalComments = await Promise.all(promises) - newProposalComments = newProposalComments - .filter(Boolean) - .concat(proposalComments) - .sort((a, b) => (b.timestamp - a.timestamp || b.delegator ? -1 : 0)) - - const voters = {} - newProposalComments = newProposalComments.map((comment) => { - const revote = voters[comment.voter] ?? false - voters[comment.voter] = true - return { ...comment, revote } - }) - - commit('SAVE_PROPOSAL_COMMENTS', newProposalComments) - } catch (e) { - console.error('fetchProposalComments', e.message) - } - - commit('SAVE_FETCHING_PROPOSAL_COMMENTS', false) - }, async fetchUserData({ getters, rootGetters, commit, rootState, dispatch }) { try { commit('SAVE_FETCHING_LOCKED_BALANCE', true) diff --git a/store/governance/proposal.js b/store/governance/proposal.js new file mode 100644 index 0000000..be2df91 --- /dev/null +++ b/store/governance/proposal.js @@ -0,0 +1,269 @@ +/* eslint-disable no-console */ +/* eslint-disable import/order */ + +import { utils } from 'ethers' +import uniqBy from 'lodash/uniqBy' +import chunk from 'lodash/chunk' + +import { lookupAddresses, createBatchRequestCallback } from '@/services' +import { CHUNK_COUNT_PER_BATCH_REQUEST } from '@/constants' + +const { toWei, fromWei, toBN } = require('web3-utils') + +const CACHE_TX = {} +const CACHE_BLOCK = {} + +const parseComment = (calldata, govInstance) => { + const empty = { contact: '', message: '' } + if (!calldata || !govInstance) return empty + + const methodLength = 4 // length of castDelegatedVote method + const result = utils.defaultAbiCoder.decode( + ['address[]', 'uint256', 'bool'], + utils.hexDataSlice(calldata, methodLength) + ) + const data = govInstance.methods.castDelegatedVote(...result).encodeABI() + const dataLength = utils.hexDataLength(data) + + try { + const str = utils.defaultAbiCoder.decode(['string'], utils.hexDataSlice(calldata, dataLength)) + const [contact, message] = JSON.parse(str) + return { contact, message } + } catch { + return empty + } +} + +const createProposalComment = (resultAll, votedEvent) => { + const { transactionHash, returnValues, blockNumber } = votedEvent + const { voter } = returnValues + + const comment = parseComment() + + const percentage = + toBN(votedEvent.returnValues.votes) + .mul(toBN(10000)) + .divRound(resultAll) + .toNumber() / 100 + + return { + id: `${transactionHash}-${voter}`, + percentage, + ...returnValues, + votes: fromWei(returnValues.votes), + transactionHash, + blockNumber, + + ...comment, + + ens: { + delegator: null, + voter: null + }, + delegator: null, + timestamp: null + } +} + +const createFetchCommentWithMessage = (web3, batch, govInstance) => async (proposalComment) => { + const { transactionHash, voter, blockNumber } = proposalComment + + if (!CACHE_TX[transactionHash]) { + CACHE_TX[transactionHash] = new Promise((resolve, reject) => { + const callback = createBatchRequestCallback(resolve, reject) + batch.add(web3.eth.getTransaction.request(transactionHash, callback)) + }) + } + + if (!CACHE_BLOCK[blockNumber]) { + CACHE_BLOCK[blockNumber] = new Promise((resolve, reject) => { + const callback = createBatchRequestCallback(resolve, reject) + batch.add(web3.eth.getBlock.request(blockNumber, callback)) + }) + } + + try { + const [tx, blockInfo] = await Promise.all([CACHE_TX[transactionHash], CACHE_BLOCK[blockNumber]]) + + const isMaybeHasComment = voter === tx.from + const comment = parseComment(isMaybeHasComment ? tx.input : null, govInstance) + + return { + ...proposalComment, + ...comment, + + delegator: voter === tx.from ? null : tx.from, + timestamp: blockInfo.timestamp + } + } catch (error) { + CACHE_TX[transactionHash] = null + CACHE_BLOCK[blockNumber] = null + return proposalComment + } +} + +const state = () => { + return { + isFetchingComments: false, + isFetchingMessages: false, + ensNames: {}, + comments: [] + } +} + +const getters = { + comments: (state) => { + const { ensNames } = state + let comments = state.comments.slice() + + comments.sort((a, b) => b.blockNumber - a.blockNumber) + comments = uniqBy(comments, 'voter') + comments.sort((a, b) => b.percentage - a.percentage) + + comments = comments.map((data) => ({ + ...data, + ens: { + delegator: ensNames[data.delegator], + voter: ensNames[data.voter] + } + })) + + return comments + } +} + +const mutations = { + SAVE_FETCHING_COMMENTS(state, status) { + state.isFetchingComments = status + }, + SAVE_FETCHING_MESSAGES(state, status) { + state.isFetchingMessages = status + }, + SAVE_ENS_NAMES(state, ensNames) { + state.ensNames = { ...state.ensNames, ...ensNames } + }, + SAVE_COMMENTS(state, comments) { + state.comments = comments + } +} + +const actions = { + async fetchComments(context, proposal) { + const { commit, dispatch, state } = context + let { comments } = state + let newComments = [] + + if (comments[0]?.id !== proposal.id) { + commit('SAVE_COMMENTS', []) + comments = [] + } + + commit('SAVE_FETCHING_COMMENTS', true) + newComments = await dispatch('fetchVotedEvents', { proposal, comments }) + commit('SAVE_FETCHING_COMMENTS', false) + + if (!newComments) return + commit('SAVE_COMMENTS', newComments.concat(comments)) + dispatch('fetchEnsNames', { comments: newComments }) + + commit('SAVE_FETCHING_MESSAGES', true) + // TODO: TC-163 - add pagination + newComments = await dispatch('fetchCommentsMessages', { comments: newComments }) + commit('SAVE_FETCHING_MESSAGES', false) + + if (!newComments) return + commit('SAVE_COMMENTS', newComments.concat(comments)) + }, + async fetchVotedEvents(context, { proposal, comments }) { + const { rootGetters } = context + let { blockNumber: fromBlock } = proposal + + const netId = rootGetters['metamask/netId'] + const govInstance = rootGetters['governance/gov/govContract']({ netId }) + + if (comments[0]?.id === proposal.id) { + fromBlock = comments[0].blockNumber + 1 + } + + try { + let votedEvents = await govInstance.getPastEvents('Voted', { + filter: { + // support: [false], + proposalId: proposal.id + }, + fromBlock, + toBlock: 'latest' + }) + + console.log('fetchVotedEvents', votedEvents.length) + + votedEvents = votedEvents.sort((a, b) => b.blockNumber - a.blockNumber) + votedEvents = uniqBy(votedEvents, 'returnValues.voter') + + console.log('fetchVotedEvents uniq', votedEvents.length) + + const resultAll = toBN(toWei(proposal.results.for)).add(toBN(toWei(proposal.results.against))) + let newComments = votedEvents.map((votedEvent) => createProposalComment(resultAll, votedEvent)) + newComments = newComments.concat(comments) + return newComments + } catch (e) { + console.error('fetchVotedEvents', e.message) + return null + } + }, + async fetchCommentsMessages(context, { comments }) { + const { rootGetters } = context + + const netId = rootGetters['metamask/netId'] + const govInstance = rootGetters['governance/gov/govContract']({ netId }) + const web3 = rootGetters['governance/gov/getWeb3']({ netId }) + const commentListChunks = chunk(comments, CHUNK_COUNT_PER_BATCH_REQUEST) + + let results = [] + + try { + for await (const list of commentListChunks) { + const batch = new web3.BatchRequest() + const fetchCommentsWithMessages = createFetchCommentWithMessage(web3, batch, govInstance) + const promises = list.map(fetchCommentsWithMessages) + batch.execute() + const result = await Promise.all(promises) + + results = results.concat(result) + } + + return results + } catch (e) { + console.error('fetchCommentsMessages', e.message) + } + }, + async fetchEnsNames(context, { comments }) { + const { rootGetters, commit } = context + + const netId = rootGetters['metamask/netId'] + const web3 = rootGetters['governance/gov/getWeb3']({ netId }) + + try { + const addresses = comments + .map((_) => _.voter) + .flat() + .filter(Boolean) + + console.log('fetchEnsNames', addresses.length) + + const ensNames = await lookupAddresses(addresses, web3) + + commit('SAVE_ENS_NAMES', ensNames) + } catch (e) { + console.error('fetchEnsNames', e.message) + } + } +} + +export default { + namespaced: true, + state, + getters, + mutations, + actions +} diff --git a/utils/stringUtils.js b/utils/stringUtils.js index a7a885b..38b9a5b 100644 --- a/utils/stringUtils.js +++ b/utils/stringUtils.js @@ -10,6 +10,30 @@ export const sliceAddress = (address) => { return '0x' + hashRender(address.slice(2)) } +export const sliceEnsName = (name, size = 4, separator = '...') => { + const chars = [...name] + + const last = name + .split('.') + .pop() + .slice(-size) + + if (chars[0]?.length === 2 && last) { + // 🐵🍆💦.eth -> 🐵🍆💦.eth + if (chars.length - 4 <= 4) return name + + // 🦍🦍🦍🦍🦍🦍🦍.eth -> 🦍🦍🦍...eth + return [].concat(chars.slice(0, 3), separator, last).join('') + } + + if (chars.length <= 2 * size + 2 + separator.length) return name + if (!name.includes('.')) return sliceAddress(name, size, separator) + + return last.length + ? [].concat(chars.slice(0, 2 * size - last.length), separator, last).join('') + : [].concat(chars.slice(0, size), separator, chars.slice(-size)).join('') +} + const semVerRegex = /^(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:-(?(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ export const parseSemanticVersion = (version) => {