TC-1 | Add supported tail calldata for vote
This commit is contained in:
parent
498e1908e1
commit
3cef4c4d5b
@ -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;
|
||||||
|
122
components/ProposalCommentFormModal.vue
Normal file
122
components/ProposalCommentFormModal.vue
Normal 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>
|
@ -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 })
|
||||||
|
122
components/governance/ProposalComment.vue
Normal file
122
components/governance/ProposalComment.vue
Normal 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>
|
27
components/governance/ProposalCommentsSkeleton.vue
Normal file
27
components/governance/ProposalCommentsSkeleton.vue
Normal 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>
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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": {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user