TC-1 | Add supported tail calldata for vote

This commit is contained in:
Andrey Pastukhov 2022-07-14 19:31:37 +00:00
parent 498e1908e1
commit 3cef4c4d5b
No known key found for this signature in database
GPG Key ID: A841818F95ED6301
11 changed files with 517 additions and 35 deletions

View File

@ -73,13 +73,17 @@
padding: 1.429rem; padding: 1.429rem;
background: #1f1f1f; background: #1f1f1f;
border-radius: 6px; border-radius: 6px;
cursor: pointer;
.title { &.is-link {
cursor: pointer;
}
&--title {
color: #fff; color: #fff;
font-size: 1.143rem; font-size: 1.143rem;
margin-bottom: 1rem; margin-bottom: 1rem;
line-height: 1.286; line-height: 1.286;
font-weight: 600;
} }
&--info { &--info {
@ -105,6 +109,7 @@
margin-right: 0.714rem; margin-right: 0.714rem;
&.tag,
.tag { .tag {
color: #fff; color: #fff;
background: #363636; background: #363636;

View File

@ -0,0 +1,122 @@
<template>
<div class="modal-card box box-modal">
<header class="box-modal-header is-spaced">
<div class="box-modal-title">{{ $t('proposalComment.modal-title', { id: proposal.id }) }}</div>
<button type="button" class="delete" @click="$emit('close')" />
</header>
<p class="detail" v-text="$t('proposalComment.modal-subtitle')" />
<div class="columns is-multiline">
<div class="column is-12">
<b-field>
<template #label>
{{ $t('proposalComment.form-contact') }}
<b-tooltip
:label="$t('proposalComment.form-contact-tooltip')"
size="is-medium"
position="is-top"
multilined
>
<button class="button is-primary has-icon">
<span class="icon icon-info"></span>
</button>
</b-tooltip>
</template>
<b-input
v-model.trim="form.contact"
:maxlength="limit / 2"
:has-counter="false"
:placeholder="$t('proposalComment.form-contact-placeholder')"
/>
</b-field>
</div>
<div class="column is-12">
<b-field
:message="fields.message ? '' : $t('proposalComment.form-message-required')"
:type="{ 'is-warning': !fields.message && !support }"
:label="$t('proposalComment.form-message')"
>
<b-input
v-model="form.message"
:maxlength="limit"
type="textarea"
:placeholder="
support
? $t('proposalComment.form-message-opt-placeholder')
: $t('proposalComment.form-message-placeholder')
"
/>
</b-field>
</div>
</div>
<b-button v-if="support" type="is-primary" icon-left="check" outlined @click="onCastVote(true)">
{{ $t('for') }}
</b-button>
<b-button v-else type="is-danger" icon-left="close" outlined @click="onCastVote(false)">
{{ $t('against') }}
</b-button>
</div>
</template>
<script>
const MESSAGE_LIMIT = 100
export default {
props: {
support: {
type: Boolean,
required: true
},
proposal: {
type: Object,
required: true,
validator: (prop) => 'id' in prop
}
},
data: () => ({
limit: MESSAGE_LIMIT,
fields: {
contact: true,
message: true
},
form: {
contact: '',
message: ''
}
}),
methods: {
validate() {
const { form, fields, support } = this
fields.contact = form.contact.length <= this.limit
fields.message = support
? form.message.length <= this.limit
: form.message.length > 2 && form.message.length <= this.limit
return fields.contact && fields.message
},
onCastVote() {
const isValid = this.validate()
if (isValid) {
this.$emit('castVote', this.form)
this.$emit('close')
}
}
}
}
</script>
<style lang="scss" scoped>
.box-modal {
overflow: initial !important;
}
.detail {
margin-bottom: 1.25rem;
}
</style>

View File

@ -6,6 +6,14 @@
<div class="description"> <div class="description">
<p>{{ data.description }}</p> <p>{{ data.description }}</p>
</div> </div>
<div>
<ProposalCommentsSkeleton
v-if="isFetchingProposalComments"
:size="proposalComments.length ? 1 : 3"
/>
<ProposalComment v-for="item in proposalComments" :key="item.id" v-bind="item" />
</div>
</div> </div>
<div class="column is-5-tablet is-4-desktop"> <div class="column is-5-tablet is-4-desktop">
<div v-if="data.status === 'active'" class="proposal-block"> <div v-if="data.status === 'active'" class="proposal-block">
@ -21,18 +29,18 @@
<b-button <b-button
:disabled="readyForAction" :disabled="readyForAction"
type="is-primary" type="is-primary"
:icon-left="isFetchingBalances ? '' : 'check'" :icon-left="isFetchingBalances || isSaveProposal ? '' : 'check'"
outlined outlined
:loading="isFetchingBalances" :loading="isFetchingBalances || isSaveProposal"
@click="onCastVote(true)" @click="onCastVote(true)"
>{{ $t('for') }}</b-button >{{ $t('for') }}</b-button
> >
<b-button <b-button
:disabled="readyForAction" :disabled="readyForAction"
type="is-danger" type="is-danger"
:icon-left="isFetchingBalances ? '' : 'close'" :icon-left="isFetchingBalances || isSaveProposal ? '' : 'close'"
outlined outlined
:loading="isFetchingBalances" :loading="isFetchingBalances || isSaveProposal"
@click="onCastVote(false)" @click="onCastVote(false)"
>{{ $t('against') }}</b-button >{{ $t('against') }}</b-button
> >
@ -160,11 +168,17 @@
<script> <script>
import { mapState, mapActions, mapGetters } from 'vuex' import { mapState, mapActions, mapGetters } from 'vuex'
import quorum from './mixins/quorum' import quorum from './mixins/quorum'
import ProposalCommentsSkeleton from './ProposalCommentsSkeleton.vue'
import ProposalComment from './ProposalComment.vue'
import NumberFormat from '@/components/NumberFormat' import NumberFormat from '@/components/NumberFormat'
import ProposalCommentFormModal from '@/components/ProposalCommentFormModal.vue'
const { toBN, fromWei, toWei } = require('web3-utils') const { toBN, fromWei, toWei } = require('web3-utils')
export default { export default {
components: { components: {
ProposalCommentsSkeleton,
ProposalComment,
NumberFormat NumberFormat
}, },
mixins: [quorum], mixins: [quorum],
@ -182,7 +196,7 @@ export default {
} }
}, },
computed: { computed: {
...mapState('governance/gov', ['proposals', 'voterReceipts']), ...mapState('governance/gov', ['proposals', 'voterReceipts', 'proposalComments', 'isSaveProposal']),
...mapState('metamask', ['ethAccount', 'isInitialized']), ...mapState('metamask', ['ethAccount', 'isInitialized']),
...mapGetters('txHashKeeper', ['addressExplorerUrl']), ...mapGetters('txHashKeeper', ['addressExplorerUrl']),
...mapGetters('metamask', ['networkConfig']), ...mapGetters('metamask', ['networkConfig']),
@ -191,6 +205,7 @@ export default {
'constants', 'constants',
'votingPeriod', 'votingPeriod',
'isFetchingBalances', 'isFetchingBalances',
'isFetchingProposalComments',
'isEnabledGovernance' 'isEnabledGovernance'
]), ]),
readyForAction() { readyForAction() {
@ -224,7 +239,9 @@ export default {
isInitialized: { isInitialized: {
handler(isInitialized) { handler(isInitialized) {
if (isInitialized && this.isEnabledGovernance) { if (isInitialized && this.isEnabledGovernance) {
this.fetchReceipt({ id: this.data.id }) const { id } = this.data
this.fetchReceipt({ id })
this.fetchProposalComments(this.data)
} }
}, },
immediate: true immediate: true
@ -265,7 +282,13 @@ export default {
clearTimeout(this.timeId) clearTimeout(this.timeId)
}, },
methods: { methods: {
...mapActions('governance/gov', ['castVote', 'executeProposal', 'fetchReceipt', 'fetchProposals']), ...mapActions('governance/gov', [
'castVote',
'executeProposal',
'fetchReceipt',
'fetchProposals',
'fetchProposalComments'
]),
getStatusType(status) { getStatusType(status) {
let statusType = '' let statusType = ''
switch (status) { switch (status) {
@ -299,7 +322,24 @@ export default {
.toNumber() .toNumber()
}, },
onCastVote(support) { onCastVote(support) {
this.castVote({ id: this.data.id, support }) const { id } = this.data
this.$buefy.modal.open({
parent: this,
component: ProposalCommentFormModal,
hasModalCard: true,
width: 440,
customClass: 'is-pinned',
props: {
support,
proposal: this.data
},
events: {
castVote: ({ contact, message }) => {
this.castVote({ id, support, contact, message })
}
}
})
}, },
onExecute() { onExecute() {
this.executeProposal({ id: this.data.id }) this.executeProposal({ id: this.data.id })

View File

@ -0,0 +1,122 @@
<template>
<div class="proposals-box">
<div class="columns is-gapless">
<div class="column proposals-box--tags">
<div
class="tag"
:class="{
'proposals-box--revote': revote,
'is-primary': support,
'is-danger': !support
}"
>
<span><number-format :value="votes" /> TORN</span>
</div>
<b-tooltip v-if="delegator" :label="delegator" position="is-top">
<div class="tag proposals-box--id">{{ $t('delegated') }}</div>
</b-tooltip>
<b-tooltip :label="voter" position="is-top">
<div class="tag proposals-box--id">{{ shortVoter }}</div>
</b-tooltip>
</div>
<div class="column is-narrow proposals-box--date">
<div class="date">
<span>{{ $t('date') }}:</span> {{ date }}
</div>
</div>
</div>
<span v-if="contact" class="proposals-box--title">{{ contact }}</span>
<div v-if="message" class="proposals-box--info" v-text="message" />
</div>
</template>
<script>
import { sliceAddress } from '@/utils'
import NumberFormat from '@/components/NumberFormat'
export default {
components: {
NumberFormat
},
inheritAttrs: false,
props: {
contact: {
type: String,
required: true
},
message: {
type: String,
required: true
},
support: {
type: Boolean,
required: true
},
timestamp: {
type: Number,
required: true
},
votes: {
type: String,
required: true
},
voter: {
type: String,
required: true
},
revote: {
type: Boolean,
required: true
},
delegator: {
type: String,
default: ''
}
},
data: (vm) => ({
shortVoter: sliceAddress(vm.voter),
date: [vm.$moment.unix(vm.timestamp).format('l'), vm.$moment.unix(vm.timestamp).format('hh:mm')].join(' ')
})
}
</script>
<style lang="scss" scoped>
.proposals-box {
cursor: default;
.tag {
margin: 0;
width: 100%;
}
&--tags {
display: flex;
flex-wrap: wrap;
grid-template-columns: repeat(auto-fill, minmax(100px, auto));
display: grid;
grid-row-gap: 0.714rem;
grid-column-gap: 0.714rem;
}
&--date {
display: flex;
align-items: center;
}
&--title {
display: flex;
}
&--title,
&--info {
word-break: break-word;
}
&--revote {
text-decoration: line-through;
}
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<div>
<div v-for="index in size" :key="index" class="proposals-box">
<div class="columns is-gapless">
<div class="column is-8-tablet is-9-desktop">
<div class="proposals-box--title">
<b-skeleton height="21" width="210" />
</div>
<div class="proposals-box--info">
<b-skeleton height="21" width="260" />
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
size: {
type: Number,
default: 3
}
}
}
</script>

View File

@ -10,6 +10,7 @@
<b-skeleton width="60%"></b-skeleton> <b-skeleton width="60%"></b-skeleton>
<b-skeleton width="60%"></b-skeleton> <b-skeleton width="60%"></b-skeleton>
</div> </div>
<ProposalCommentsSkeleton />
</div> </div>
<div class="column is-5-tablet is-4-desktop"> <div class="column is-5-tablet is-4-desktop">
<div class="proposal-block"> <div class="proposal-block">
@ -77,3 +78,13 @@
</div> </div>
</div> </div>
</template> </template>
<script>
import ProposalCommentsSkeleton from './ProposalCommentsSkeleton.vue'
export default {
components: {
ProposalCommentsSkeleton
}
}
</script>

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="proposals-box" @click="onClick"> <div class="proposals-box is-link" @click="onClick">
<div class="columns is-gapless"> <div class="columns is-gapless">
<div class="column is-8-tablet is-9-desktop"> <div class="column is-8-tablet is-9-desktop">
<div class="title"> <div class="proposals-box--title">
{{ data.title }} {{ data.title }}
</div> </div>
<div class="proposals-box--info"> <div class="proposals-box--info">

View File

@ -1,9 +1,9 @@
<template> <template>
<div> <div>
<div v-for="(item, index) in emptyArray" :key="index" class="proposals-box"> <div v-for="index in size" :key="index" class="proposals-box">
<div class="columns is-gapless"> <div class="columns is-gapless">
<div class="column is-8-tablet is-9-desktop"> <div class="column is-8-tablet is-9-desktop">
<div class="title"> <div class="proposals-box--title">
<b-skeleton height="28" width="210"></b-skeleton> <b-skeleton height="28" width="210"></b-skeleton>
</div> </div>
<div class="proposals-box--info"> <div class="proposals-box--info">
@ -39,17 +39,11 @@
<script> <script>
export default { export default {
components: {},
props: { props: {
size: { size: {
type: Number, type: Number,
default: 5 default: 5
} }
},
data() {
return {
emptyArray: Array(this.size).fill('')
}
} }
} }
</script> </script>

View File

@ -283,6 +283,17 @@
"description": "Description is required" "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", "executed": "Executed",
"proposalDoesNotExist": "The proposal doesn't exist. Please go back to the list.", "proposalDoesNotExist": "The proposal doesn't exist. Please go back to the list.",
"errorPage": { "errorPage": {

View File

@ -116,6 +116,7 @@ export default {
ensSubdomainKey: 'mainnet-tornado', ensSubdomainKey: 'mainnet-tornado',
pollInterval: 15, pollInterval: 15,
constants: { constants: {
GOVERNANCE_TORNADOCASH_BLOCK: 11474695,
NOTE_ACCOUNT_BLOCK: 11842486, NOTE_ACCOUNT_BLOCK: 11842486,
ENCRYPTED_NOTES_BLOCK: 14248730, ENCRYPTED_NOTES_BLOCK: 14248730,
MINING_BLOCK_TIME: 15 MINING_BLOCK_TIME: 15
@ -534,6 +535,7 @@ export default {
ensSubdomainKey: 'goerli-tornado', ensSubdomainKey: 'goerli-tornado',
pollInterval: 15, pollInterval: 15,
constants: { constants: {
GOVERNANCE_TORNADOCASH_BLOCK: 3945171,
NOTE_ACCOUNT_BLOCK: 4131375, NOTE_ACCOUNT_BLOCK: 4131375,
ENCRYPTED_NOTES_BLOCK: 4131375, ENCRYPTED_NOTES_BLOCK: 4131375,
MINING_BLOCK_TIME: 15 MINING_BLOCK_TIME: 15

View File

@ -1,11 +1,12 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
/* eslint-disable import/order */ /* eslint-disable import/order */
import Web3 from 'web3' import Web3 from 'web3'
import { utils } from 'ethers'
import { ToastProgrammatic as Toast } from 'buefy' import { ToastProgrammatic as Toast } from 'buefy'
import networkConfig from '@/networkConfig' 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' import AggregatorABI from '@/abis/Aggregator.abi.json'
const { numberToHex, toWei, fromWei, toBN, hexToNumber, hexToNumberString } = require('web3-utils') const { numberToHex, toWei, fromWei, toBN, hexToNumber, hexToNumberString } = require('web3-utils')
@ -15,16 +16,19 @@ const state = () => {
approvalAmount: 'unlimited', approvalAmount: 'unlimited',
lockedBalance: '0', lockedBalance: '0',
isFetchingLockedBalance: false, isFetchingLockedBalance: false,
isFetchingProposalComments: false,
currentDelegate: '0x0000000000000000000000000000000000000000', currentDelegate: '0x0000000000000000000000000000000000000000',
timestamp: 0, timestamp: 0,
delegatedBalance: '0', delegatedBalance: '0',
isFetchingDelegatedBalance: false, isFetchingDelegatedBalance: false,
delegators: [], delegators: [],
proposalComments: [],
latestProposalId: { latestProposalId: {
value: null, value: null,
status: null status: null
}, },
isFetchingProposals: true, isFetchingProposals: true,
isSaveProposal: false,
proposals: [], proposals: [],
voterReceipts: [], voterReceipts: [],
hasActiveProposals: false, hasActiveProposals: false,
@ -39,13 +43,20 @@ const state = () => {
} }
const getters = { const getters = {
govContract: (state, getters, rootState) => ({ netId }) => { getConfig: (state, getters, rootState) => ({ netId }) => {
const config = networkConfig[`netId${netId}`] return networkConfig[`netId${netId}`]
},
getWeb3: (state, getters, rootState) => ({ netId }) => {
const { url } = rootState.settings[`netId${netId}`].rpc 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'] const address = config['governance.contract.tornadocash.eth']
if (address) { if (address) {
const web3 = new Web3(url) const web3 = getters.getWeb3({ netId })
return new web3.eth.Contract(ERC20ABI, address) const contract = new web3.eth.Contract(GovernanceABI, address)
return contract
} }
return null return null
@ -63,6 +74,9 @@ const getters = {
return isFetchingProposals return isFetchingProposals
}, },
isFetchingProposalComments: (state) => {
return state.isFetchingProposalComments
},
votingPower: (state) => { votingPower: (state) => {
return toBN(state.lockedBalance) return toBN(state.lockedBalance)
.add(toBN(state.delegatedBalance)) .add(toBN(state.delegatedBalance))
@ -94,6 +108,12 @@ const mutations = {
SAVE_FETCHING_PROPOSALS(state, status) { SAVE_FETCHING_PROPOSALS(state, status) {
this._vm.$set(state, 'isFetchingProposals', 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 }) { SAVE_LOCKED_BALANCE(state, { balance }) {
this._vm.$set(state, 'lockedBalance', balance) this._vm.$set(state, 'lockedBalance', balance)
}, },
@ -109,6 +129,9 @@ const mutations = {
SAVE_DELEGATEE(state, { currentDelegate }) { SAVE_DELEGATEE(state, { currentDelegate }) {
this._vm.$set(state, 'currentDelegate', currentDelegate) this._vm.$set(state, 'currentDelegate', currentDelegate)
}, },
SAVE_PROPOSAL_COMMENTS(state, proposalComments) {
state.proposalComments = proposalComments
},
SAVE_PROPOSALS(state, proposals) { SAVE_PROPOSALS(state, proposals) {
this._vm.$set(state, 'proposals', proposals) this._vm.$set(state, 'proposals', proposals)
}, },
@ -152,6 +175,7 @@ const proposalIntervalConstants = [
// 'VOTING_DELAY', // 'VOTING_DELAY',
'VOTING_PERIOD' 'VOTING_PERIOD'
] ]
const govConstants = ['PROPOSAL_THRESHOLD', 'QUORUM_VOTES'] const govConstants = ['PROPOSAL_THRESHOLD', 'QUORUM_VOTES']
const actions = { 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 { try {
const { ethAccount } = rootState.metamask const { ethAccount } = rootState.metamask
const netId = rootGetters['metamask/netId'] const netId = rootGetters['metamask/netId']
const govInstance = getters.govContract({ netId }) const govInstance = getters.govContract({ netId })
const delegators = [...state.delegators] const delegators = [...state.delegators]
const web3 = getters.getWeb3({ netId })
if (toBN(state.lockedBalance).gt(toBN('0'))) { if (toBN(state.lockedBalance).gt(toBN('0'))) {
delegators.push(ethAccount) delegators.push(ethAccount)
} }
const gas = await govInstance.methods const data = govInstance.methods.castDelegatedVote(delegators, id, support).encodeABI()
.castDelegatedVote(delegators, id, support) let dataWithTail = data
.estimateGas({ from: ethAccount, value: 0 })
const data = await govInstance.methods.castDelegatedVote(delegators, id, support).encodeABI() 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 = { const callParams = {
method: 'eth_sendTransaction', method: 'eth_sendTransaction',
params: { params: {
to: govInstance._address, to: govInstance._address,
gas: numberToHex(gas + 30000), gas: numberToHex(gas + 30000),
data data: dataWithTail
}, },
watcherParams: { watcherParams: {
title: support ? 'votingFor' : 'votingAgainst', title: support ? 'votingFor' : 'votingAgainst',
@ -392,6 +433,7 @@ const actions = {
) )
} finally { } finally {
dispatch('loading/disable', {}, { root: true }) dispatch('loading/disable', {}, { root: true })
commit('SAVE_SAVE_PROPOSAL', false)
} }
}, },
async executeProposal({ getters, rootGetters, commit, rootState, dispatch }, { id }) { async executeProposal({ getters, rootGetters, commit, rootState, dispatch }, { id }) {
@ -619,6 +661,7 @@ const actions = {
const netId = rootGetters['metamask/netId'] const netId = rootGetters['metamask/netId']
const aggregatorContract = getters.aggregatorContract const aggregatorContract = getters.aggregatorContract
const govInstance = getters.govContract({ netId }) const govInstance = getters.govContract({ netId })
const config = getters.getConfig({ netId })
if (!govInstance) { if (!govInstance) {
return return
@ -626,7 +669,7 @@ const actions = {
const [events, statuses] = await Promise.all([ const [events, statuses] = await Promise.all([
govInstance.getPastEvents('ProposalCreated', { govInstance.getPastEvents('ProposalCreated', {
fromBlock: 0, fromBlock: config.constants.GOVERNANCE_TORNADOCASH_BLOCK,
toBlock: 'latest' toBlock: 'latest'
}), }),
aggregatorContract.methods.getAllProposals(govInstance._address).call() aggregatorContract.methods.getAllProposals(govInstance._address).call()
@ -663,7 +706,7 @@ const actions = {
} }
proposals = events proposals = events
.map(({ returnValues }, index) => { .map(({ returnValues, blockNumber }, index) => {
const id = Number(returnValues.id) const id = Number(returnValues.id)
const { state, startTime, endTime, forVotes, againstVotes } = statuses[index] const { state, startTime, endTime, forVotes, againstVotes } = statuses[index]
const { title, description } = parseDescription({ id, text: returnValues.description }) const { title, description } = parseDescription({ id, text: returnValues.description })
@ -677,6 +720,7 @@ const actions = {
endTime: Number(endTime), endTime: Number(endTime),
startTime: Number(startTime), startTime: Number(startTime),
status: ProposalState[Number(state)], status: ProposalState[Number(state)],
blockNumber,
results: { results: {
for: fromWei(forVotes), for: fromWei(forVotes),
against: fromWei(againstVotes) against: fromWei(againstVotes)
@ -767,6 +811,7 @@ const actions = {
} }
const netId = rootGetters['metamask/netId'] const netId = rootGetters['metamask/netId']
const config = getters.getConfig({ netId })
const aggregatorContract = getters.aggregatorContract const aggregatorContract = getters.aggregatorContract
const govInstance = getters.govContract({ netId }) const govInstance = getters.govContract({ netId })
@ -774,14 +819,14 @@ const actions = {
filter: { filter: {
to: ethAccount to: ethAccount
}, },
fromBlock: 0, fromBlock: config.constants.GOVERNANCE_TORNADOCASH_BLOCK,
toBlock: 'latest' toBlock: 'latest'
}) })
let undelegatedAccs = await govInstance.getPastEvents('Undelegated', { let undelegatedAccs = await govInstance.getPastEvents('Undelegated', {
filter: { filter: {
from: ethAccount from: ethAccount
}, },
fromBlock: 0, fromBlock: config.constants.GOVERNANCE_TORNADOCASH_BLOCK,
toBlock: 'latest' toBlock: 'latest'
}) })
delegatedAccs = delegatedAccs.map((acc) => acc.returnValues.account) delegatedAccs = delegatedAccs.map((acc) => acc.returnValues.account)
@ -838,6 +883,109 @@ const actions = {
console.error('fetchReceipt', e.message) 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 }) { async fetchUserData({ getters, rootGetters, commit, rootState, dispatch }) {
try { try {
commit('SAVE_FETCHING_LOCKED_BALANCE', true) commit('SAVE_FETCHING_LOCKED_BALANCE', true)