+
@@ -39,17 +39,11 @@
diff --git a/langs/en.json b/langs/en.json
index 16d9c65..1e05885 100644
--- a/langs/en.json
+++ b/langs/en.json
@@ -283,6 +283,17 @@
"description": "Description is required"
}
},
+ "proposalComment": {
+ "modal-title": "Title: Proposal #{id}",
+ "modal-subtitle": "Please provide feedback about your decision. Why are you against of 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-message": "Message",
+ "form-message-placeholder": "Enter message",
+ "form-message-opt-placeholder": "Enter message (optional)",
+ "form-message-required": "Message required"
+ },
"executed": "Executed",
"proposalDoesNotExist": "The proposal doesn't exist. Please go back to the list.",
"errorPage": {
diff --git a/networkConfig.js b/networkConfig.js
index cbc67f6..a361120 100644
--- a/networkConfig.js
+++ b/networkConfig.js
@@ -116,6 +116,7 @@ export default {
ensSubdomainKey: 'mainnet-tornado',
pollInterval: 15,
constants: {
+ GOVERNANCE_TORNADOCASH_BLOCK: 11474695,
NOTE_ACCOUNT_BLOCK: 11842486,
ENCRYPTED_NOTES_BLOCK: 14248730,
MINING_BLOCK_TIME: 15
@@ -534,6 +535,7 @@ export default {
ensSubdomainKey: 'goerli-tornado',
pollInterval: 15,
constants: {
+ GOVERNANCE_TORNADOCASH_BLOCK: 3945171,
NOTE_ACCOUNT_BLOCK: 4131375,
ENCRYPTED_NOTES_BLOCK: 4131375,
MINING_BLOCK_TIME: 15
diff --git a/store/governance/gov.js b/store/governance/gov.js
index 1237f50..8d94b0b 100644
--- a/store/governance/gov.js
+++ b/store/governance/gov.js
@@ -1,11 +1,12 @@
/* eslint-disable no-console */
/* eslint-disable import/order */
import Web3 from 'web3'
+import { utils } from 'ethers'
import { ToastProgrammatic as Toast } from 'buefy'
import networkConfig from '@/networkConfig'
-import ERC20ABI from '@/abis/Governance.abi.json'
+import GovernanceABI from '@/abis/Governance.abi.json'
import AggregatorABI from '@/abis/Aggregator.abi.json'
const { numberToHex, toWei, fromWei, toBN, hexToNumber, hexToNumberString } = require('web3-utils')
@@ -15,16 +16,19 @@ 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,
proposals: [],
voterReceipts: [],
hasActiveProposals: false,
@@ -39,13 +43,20 @@ const state = () => {
}
const getters = {
- govContract: (state, getters, rootState) => ({ netId }) => {
- const config = networkConfig[`netId${netId}`]
+ getConfig: (state, getters, rootState) => ({ netId }) => {
+ return networkConfig[`netId${netId}`]
+ },
+ getWeb3: (state, getters, rootState) => ({ netId }) => {
const { url } = rootState.settings[`netId${netId}`].rpc
+ return new Web3(url)
+ },
+ govContract: (state, getters, rootState) => ({ netId }) => {
+ const config = getters.getConfig({ netId })
const address = config['governance.contract.tornadocash.eth']
if (address) {
- const web3 = new Web3(url)
- return new web3.eth.Contract(ERC20ABI, address)
+ const web3 = getters.getWeb3({ netId })
+ const contract = new web3.eth.Contract(GovernanceABI, address)
+ return contract
}
return null
@@ -63,6 +74,9 @@ const getters = {
return isFetchingProposals
},
+ isFetchingProposalComments: (state) => {
+ return state.isFetchingProposalComments
+ },
votingPower: (state) => {
return toBN(state.lockedBalance)
.add(toBN(state.delegatedBalance))
@@ -94,6 +108,12 @@ 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_LOCKED_BALANCE(state, { balance }) {
this._vm.$set(state, 'lockedBalance', balance)
},
@@ -109,6 +129,9 @@ 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)
},
@@ -152,6 +175,7 @@ const proposalIntervalConstants = [
// 'VOTING_DELAY',
'VOTING_PERIOD'
]
+
const govConstants = ['PROPOSAL_THRESHOLD', 'QUORUM_VOTES']
const actions = {
@@ -331,28 +355,45 @@ const actions = {
})
}
},
- async castVote({ getters, rootGetters, commit, rootState, dispatch, state }, { id, support }) {
+ async castVote(context, payload) {
+ const { getters, rootGetters, commit, rootState, dispatch, state } = context
+ const { id, support, contact = '', message = '' } = payload
+
+ commit('SAVE_SAVE_PROPOSAL', true)
+
try {
const { ethAccount } = rootState.metamask
const netId = rootGetters['metamask/netId']
const govInstance = getters.govContract({ netId })
const delegators = [...state.delegators]
+ const web3 = getters.getWeb3({ netId })
if (toBN(state.lockedBalance).gt(toBN('0'))) {
delegators.push(ethAccount)
}
- const gas = await govInstance.methods
- .castDelegatedVote(delegators, id, support)
- .estimateGas({ from: ethAccount, value: 0 })
- const data = await govInstance.methods.castDelegatedVote(delegators, id, support).encodeABI()
+ const data = govInstance.methods.castDelegatedVote(delegators, id, support).encodeABI()
+ let dataWithTail = data
+
+ if (contact || message) {
+ const value = JSON.stringify([contact, message])
+ const tail = utils.defaultAbiCoder.encode(['string'], [value])
+ dataWithTail = utils.hexConcat([data, tail])
+ }
+
+ const gas = await web3.eth.estimateGas({
+ from: ethAccount,
+ to: govInstance._address,
+ value: 0,
+ data: dataWithTail
+ })
const callParams = {
method: 'eth_sendTransaction',
params: {
to: govInstance._address,
gas: numberToHex(gas + 30000),
- data
+ data: dataWithTail
},
watcherParams: {
title: support ? 'votingFor' : 'votingAgainst',
@@ -392,6 +433,7 @@ const actions = {
)
} finally {
dispatch('loading/disable', {}, { root: true })
+ commit('SAVE_SAVE_PROPOSAL', false)
}
},
async executeProposal({ getters, rootGetters, commit, rootState, dispatch }, { id }) {
@@ -619,6 +661,7 @@ const actions = {
const netId = rootGetters['metamask/netId']
const aggregatorContract = getters.aggregatorContract
const govInstance = getters.govContract({ netId })
+ const config = getters.getConfig({ netId })
if (!govInstance) {
return
@@ -626,7 +669,7 @@ const actions = {
const [events, statuses] = await Promise.all([
govInstance.getPastEvents('ProposalCreated', {
- fromBlock: 0,
+ fromBlock: config.constants.GOVERNANCE_TORNADOCASH_BLOCK,
toBlock: 'latest'
}),
aggregatorContract.methods.getAllProposals(govInstance._address).call()
@@ -663,7 +706,7 @@ const actions = {
}
proposals = events
- .map(({ returnValues }, index) => {
+ .map(({ returnValues, blockNumber }, index) => {
const id = Number(returnValues.id)
const { state, startTime, endTime, forVotes, againstVotes } = statuses[index]
const { title, description } = parseDescription({ id, text: returnValues.description })
@@ -677,6 +720,7 @@ const actions = {
endTime: Number(endTime),
startTime: Number(startTime),
status: ProposalState[Number(state)],
+ blockNumber,
results: {
for: fromWei(forVotes),
against: fromWei(againstVotes)
@@ -767,6 +811,7 @@ const actions = {
}
const netId = rootGetters['metamask/netId']
+ const config = getters.getConfig({ netId })
const aggregatorContract = getters.aggregatorContract
const govInstance = getters.govContract({ netId })
@@ -774,14 +819,14 @@ const actions = {
filter: {
to: ethAccount
},
- fromBlock: 0,
+ fromBlock: config.constants.GOVERNANCE_TORNADOCASH_BLOCK,
toBlock: 'latest'
})
let undelegatedAccs = await govInstance.getPastEvents('Undelegated', {
filter: {
from: ethAccount
},
- fromBlock: 0,
+ fromBlock: config.constants.GOVERNANCE_TORNADOCASH_BLOCK,
toBlock: 'latest'
})
delegatedAccs = delegatedAccs.map((acc) => acc.returnValues.account)
@@ -838,6 +883,109 @@ 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)