TC-98 | Add ProposalComments component with tab filters
This commit is contained in:
parent
3cef4c4d5b
commit
6e01a677ae
9
app/router.scrollBehavior.js
Normal file
9
app/router.scrollBehavior.js
Normal file
@ -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
|
@ -52,11 +52,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b-button v-if="support" type="is-primary" icon-left="check" outlined @click="onCastVote(true)">
|
<b-button
|
||||||
|
v-if="support"
|
||||||
|
:disabled="!isValid"
|
||||||
|
type="is-primary"
|
||||||
|
icon-left="check"
|
||||||
|
outlined
|
||||||
|
@click="onCastVote(true)"
|
||||||
|
>
|
||||||
{{ $t('for') }}
|
{{ $t('for') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
|
|
||||||
<b-button v-else type="is-danger" icon-left="close" outlined @click="onCastVote(false)">
|
<b-button
|
||||||
|
v-else
|
||||||
|
:disabled="!isValid"
|
||||||
|
type="is-danger"
|
||||||
|
icon-left="close"
|
||||||
|
outlined
|
||||||
|
@click="onCastVote(false)"
|
||||||
|
>
|
||||||
{{ $t('against') }}
|
{{ $t('against') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
@ -88,6 +102,11 @@ export default {
|
|||||||
message: ''
|
message: ''
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
computed: {
|
||||||
|
isValid() {
|
||||||
|
return this.validate()
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
validate() {
|
validate() {
|
||||||
const { form, fields, support } = this
|
const { form, fields, support } = this
|
||||||
@ -100,9 +119,7 @@ export default {
|
|||||||
return fields.contact && fields.message
|
return fields.contact && fields.message
|
||||||
},
|
},
|
||||||
onCastVote() {
|
onCastVote() {
|
||||||
const isValid = this.validate()
|
if (this.isValid) {
|
||||||
|
|
||||||
if (isValid) {
|
|
||||||
this.$emit('castVote', this.form)
|
this.$emit('castVote', this.form)
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
}
|
}
|
||||||
|
@ -4,158 +4,159 @@
|
|||||||
<div class="column is-7-tablet is-8-desktop">
|
<div class="column is-7-tablet is-8-desktop">
|
||||||
<h1 class="title">{{ data.title }}</h1>
|
<h1 class="title">{{ data.title }}</h1>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<p>{{ data.description }}</p>
|
<p class="proposal--description">{{ data.description }}</p>
|
||||||
</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>
|
||||||
|
<ProposalComments
|
||||||
|
v-if="isEnabledGovernance && quorumPercent"
|
||||||
|
:proposal="data"
|
||||||
|
:is-initialized="isInitialized"
|
||||||
|
class="proposal--comments"
|
||||||
|
/>
|
||||||
</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 class="proposal--blocks">
|
||||||
<div class="title">{{ $t('castYourVote') }}</div>
|
<div v-if="data.status === 'active'" class="proposal-block">
|
||||||
<b-tooltip
|
<div class="title">{{ $t('castYourVote') }}</div>
|
||||||
class="fit-content"
|
|
||||||
:label="tooltipMessage"
|
|
||||||
position="is-top"
|
|
||||||
:active="readyForAction"
|
|
||||||
multilined
|
|
||||||
>
|
|
||||||
<div class="buttons buttons__halfwidth">
|
|
||||||
<b-button
|
|
||||||
:disabled="readyForAction"
|
|
||||||
type="is-primary"
|
|
||||||
:icon-left="isFetchingBalances || isSaveProposal ? '' : 'check'"
|
|
||||||
outlined
|
|
||||||
:loading="isFetchingBalances || isSaveProposal"
|
|
||||||
@click="onCastVote(true)"
|
|
||||||
>{{ $t('for') }}</b-button
|
|
||||||
>
|
|
||||||
<b-button
|
|
||||||
:disabled="readyForAction"
|
|
||||||
type="is-danger"
|
|
||||||
:icon-left="isFetchingBalances || isSaveProposal ? '' : 'close'"
|
|
||||||
outlined
|
|
||||||
:loading="isFetchingBalances || isSaveProposal"
|
|
||||||
@click="onCastVote(false)"
|
|
||||||
>{{ $t('against') }}</b-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</b-tooltip>
|
|
||||||
<i18n
|
|
||||||
v-if="voterReceipts[data.id] && voterReceipts[data.id].hasVoted"
|
|
||||||
tag="div"
|
|
||||||
path="yourCurrentVote"
|
|
||||||
>
|
|
||||||
<template v-slot:vote>
|
|
||||||
<span
|
|
||||||
:class="{
|
|
||||||
'has-text-primary': voterReceipts[data.id].support,
|
|
||||||
'has-text-danger': !voterReceipts[data.id].support
|
|
||||||
}"
|
|
||||||
>{{ $n(fromWeiToTorn(voterReceipts[data.id].balance)) }} TORN</span
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</i18n>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="data.status === 'awaitingExecution'" class="proposal-block">
|
|
||||||
<div class="title">{{ $t('executeProposal') }}</div>
|
|
||||||
<b-tooltip
|
|
||||||
class="fit-content"
|
|
||||||
:label="$t('connectYourWalletFirst')"
|
|
||||||
position="is-top"
|
|
||||||
:active="!ethAccount"
|
|
||||||
multilined
|
|
||||||
>
|
|
||||||
<b-button
|
|
||||||
type="is-primary"
|
|
||||||
icon-left="check"
|
|
||||||
outlined
|
|
||||||
:disabled="!ethAccount"
|
|
||||||
expanded
|
|
||||||
@click="onExecute"
|
|
||||||
>{{ $t('execute') }}</b-button
|
|
||||||
>
|
|
||||||
</b-tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="proposal-block">
|
|
||||||
<div class="title">{{ $t('currentResults') }}</div>
|
|
||||||
<div class="label">
|
|
||||||
{{ $t('for') }}
|
|
||||||
<span class="percent"
|
|
||||||
><number-format :value="data.results.for" /> TORN / {{ calculatePercent('for') }}%</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<b-progress :value="calculatePercent('for')" type="is-primary"></b-progress>
|
|
||||||
<div class="label">
|
|
||||||
{{ $t('against') }}
|
|
||||||
<span class="percent"
|
|
||||||
><number-format :value="data.results.against" class="value" /> TORN /
|
|
||||||
{{ calculatePercent('against') }}%</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<b-progress :value="calculatePercent('against')" type="is-danger"></b-progress>
|
|
||||||
<div class="label">
|
|
||||||
{{ $t('quorum') }}
|
|
||||||
<b-tooltip
|
<b-tooltip
|
||||||
:label="
|
class="fit-content"
|
||||||
$t('quorumTooltip', {
|
:label="tooltipMessage"
|
||||||
days: $tc('dayPlural', votingPeriod),
|
|
||||||
votes: $n(quorumVotes, 'compact')
|
|
||||||
})
|
|
||||||
"
|
|
||||||
size="is-medium"
|
|
||||||
position="is-top"
|
position="is-top"
|
||||||
|
:active="readyForAction"
|
||||||
multilined
|
multilined
|
||||||
>
|
>
|
||||||
<button class="button is-primary has-icon">
|
<div class="buttons buttons__halfwidth">
|
||||||
<span class="icon icon-info"></span>
|
<b-button
|
||||||
</button>
|
:disabled="readyForAction"
|
||||||
|
type="is-primary"
|
||||||
|
:icon-left="isFetchingBalances || isCastingVote ? '' : 'check'"
|
||||||
|
outlined
|
||||||
|
:loading="isFetchingBalances || isCastingVote"
|
||||||
|
@click="onCastVote(true)"
|
||||||
|
>{{ $t('for') }}</b-button
|
||||||
|
>
|
||||||
|
<b-button
|
||||||
|
:disabled="readyForAction"
|
||||||
|
type="is-danger"
|
||||||
|
:icon-left="isFetchingBalances || isCastingVote ? '' : 'close'"
|
||||||
|
outlined
|
||||||
|
:loading="isFetchingBalances || isCastingVote"
|
||||||
|
@click="onCastVote(false)"
|
||||||
|
>{{ $t('against') }}</b-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
<span class="percent"
|
<i18n
|
||||||
><number-format :value="isQuorumCompleted ? quorumVotes : quorumResult" class="value" /> TORN /
|
v-if="voterReceipts[data.id] && voterReceipts[data.id].hasVoted"
|
||||||
{{ quorumPercent }}%</span
|
tag="div"
|
||||||
|
path="yourCurrentVote"
|
||||||
>
|
>
|
||||||
|
<template v-slot:vote>
|
||||||
|
<span
|
||||||
|
:class="{
|
||||||
|
'has-text-primary': voterReceipts[data.id].support,
|
||||||
|
'has-text-danger': !voterReceipts[data.id].support
|
||||||
|
}"
|
||||||
|
>{{ $n(fromWeiToTorn(voterReceipts[data.id].balance)) }} TORN</span
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</i18n>
|
||||||
</div>
|
</div>
|
||||||
<b-progress :value="quorumPercent" type="is-violet"></b-progress>
|
<div v-else-if="data.status === 'awaitingExecution'" class="proposal-block">
|
||||||
</div>
|
<div class="title">{{ $t('executeProposal') }}</div>
|
||||||
<div class="proposal-block">
|
<b-tooltip
|
||||||
<div class="title">{{ $t('information') }}</div>
|
class="fit-content"
|
||||||
<div class="columns is-multiline is-small" :class="{ 'has-countdown': countdown }">
|
:label="$t('connectYourWalletFirst')"
|
||||||
<div class="column is-full-small">
|
position="is-top"
|
||||||
<strong>{{ $t('proposalAddress') }}</strong>
|
:active="!ethAccount"
|
||||||
<div class="value">
|
multilined
|
||||||
<a :href="contractUrl" class="address" target="_blank" rel="noopener noreferrer">
|
>
|
||||||
{{ data.target }}
|
<b-button
|
||||||
</a>
|
type="is-primary"
|
||||||
|
icon-left="check"
|
||||||
|
outlined
|
||||||
|
:disabled="!ethAccount"
|
||||||
|
expanded
|
||||||
|
@click="onExecute"
|
||||||
|
>{{ $t('execute') }}</b-button
|
||||||
|
>
|
||||||
|
</b-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="proposal-block">
|
||||||
|
<div class="title">{{ $t('currentResults') }}</div>
|
||||||
|
<div class="label">
|
||||||
|
{{ $t('for') }}
|
||||||
|
<span class="percent"
|
||||||
|
><number-format :value="data.results.for" /> TORN / {{ calculatePercent('for') }}%</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<b-progress :value="calculatePercent('for')" type="is-primary"></b-progress>
|
||||||
|
<div class="label">
|
||||||
|
{{ $t('against') }}
|
||||||
|
<span class="percent"
|
||||||
|
><number-format :value="data.results.against" class="value" /> TORN /
|
||||||
|
{{ calculatePercent('against') }}%</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<b-progress :value="calculatePercent('against')" type="is-danger"></b-progress>
|
||||||
|
<div class="label">
|
||||||
|
{{ $t('quorum') }}
|
||||||
|
<b-tooltip
|
||||||
|
:label="
|
||||||
|
$t('quorumTooltip', {
|
||||||
|
days: $tc('dayPlural', votingPeriod),
|
||||||
|
votes: $n(quorumVotes, 'compact')
|
||||||
|
})
|
||||||
|
"
|
||||||
|
size="is-medium"
|
||||||
|
position="is-top"
|
||||||
|
multilined
|
||||||
|
>
|
||||||
|
<button class="button is-primary has-icon">
|
||||||
|
<span class="icon icon-info"></span>
|
||||||
|
</button>
|
||||||
|
</b-tooltip>
|
||||||
|
<span class="percent"
|
||||||
|
><number-format :value="isQuorumCompleted ? quorumVotes : quorumResult" class="value" /> TORN
|
||||||
|
/ {{ quorumPercent }}%</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<b-progress :value="quorumPercent" type="is-violet"></b-progress>
|
||||||
|
</div>
|
||||||
|
<div class="proposal-block">
|
||||||
|
<div class="title">{{ $t('information') }}</div>
|
||||||
|
<div class="columns is-multiline is-small" :class="{ 'has-countdown': countdown }">
|
||||||
|
<div class="column is-full-small">
|
||||||
|
<strong>{{ $t('proposalAddress') }}</strong>
|
||||||
|
<div class="value">
|
||||||
|
<a :href="contractUrl" class="address" target="_blank" rel="noopener noreferrer">
|
||||||
|
{{ data.target }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="column is-half-small">
|
||||||
<div class="column is-half-small">
|
<strong>{{ $t('id') }}</strong>
|
||||||
<strong>{{ $t('id') }}</strong>
|
<div class="value">{{ data.id }}</div>
|
||||||
<div class="value">{{ data.id }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-half-small">
|
|
||||||
<strong>{{ $t('status') }}</strong>
|
|
||||||
<div class="value">
|
|
||||||
<b-tag :type="getStatusType(data.status)">{{ $t(data.status) }}</b-tag>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="column is-half-small">
|
||||||
<div class="column is-half-small">
|
<strong>{{ $t('status') }}</strong>
|
||||||
<strong>{{ $t('startDate') }}</strong>
|
<div class="value">
|
||||||
<div class="value">{{ $moment.unix(data.startTime).format('llll') }}</div>
|
<b-tag :type="getStatusType(data.status)">{{ $t(data.status) }}</b-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half-small">
|
</div>
|
||||||
<strong>{{ $t('endDate') }}</strong>
|
<div class="column is-half-small">
|
||||||
<div class="value">{{ $moment.unix(data.endTime).format('llll') }}</div>
|
<strong>{{ $t('startDate') }}</strong>
|
||||||
</div>
|
<div class="value">{{ $moment.unix(data.startTime).format('llll') }}</div>
|
||||||
<div v-if="countdown" class="column is-full-small">
|
</div>
|
||||||
<strong>{{ $t(timerLabel) }}</strong>
|
<div class="column is-half-small">
|
||||||
<div class="value">
|
<strong>{{ $t('endDate') }}</strong>
|
||||||
{{ countdown }}
|
<div class="value">{{ $moment.unix(data.endTime).format('llll') }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="countdown" class="column is-full-small">
|
||||||
|
<strong>{{ $t(timerLabel) }}</strong>
|
||||||
|
<div class="value">
|
||||||
|
{{ countdown }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -168,8 +169,7 @@
|
|||||||
<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 ProposalComments from './ProposalComments.vue'
|
||||||
import ProposalComment from './ProposalComment.vue'
|
|
||||||
import NumberFormat from '@/components/NumberFormat'
|
import NumberFormat from '@/components/NumberFormat'
|
||||||
import ProposalCommentFormModal from '@/components/ProposalCommentFormModal.vue'
|
import ProposalCommentFormModal from '@/components/ProposalCommentFormModal.vue'
|
||||||
|
|
||||||
@ -177,8 +177,7 @@ const { toBN, fromWei, toWei } = require('web3-utils')
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ProposalCommentsSkeleton,
|
ProposalComments,
|
||||||
ProposalComment,
|
|
||||||
NumberFormat
|
NumberFormat
|
||||||
},
|
},
|
||||||
mixins: [quorum],
|
mixins: [quorum],
|
||||||
@ -196,7 +195,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('governance/gov', ['proposals', 'voterReceipts', 'proposalComments', 'isSaveProposal']),
|
...mapState('governance/gov', ['proposals', 'voterReceipts', 'isCastingVote']),
|
||||||
...mapState('metamask', ['ethAccount', 'isInitialized']),
|
...mapState('metamask', ['ethAccount', 'isInitialized']),
|
||||||
...mapGetters('txHashKeeper', ['addressExplorerUrl']),
|
...mapGetters('txHashKeeper', ['addressExplorerUrl']),
|
||||||
...mapGetters('metamask', ['networkConfig']),
|
...mapGetters('metamask', ['networkConfig']),
|
||||||
@ -205,7 +204,6 @@ export default {
|
|||||||
'constants',
|
'constants',
|
||||||
'votingPeriod',
|
'votingPeriod',
|
||||||
'isFetchingBalances',
|
'isFetchingBalances',
|
||||||
'isFetchingProposalComments',
|
|
||||||
'isEnabledGovernance'
|
'isEnabledGovernance'
|
||||||
]),
|
]),
|
||||||
readyForAction() {
|
readyForAction() {
|
||||||
@ -239,9 +237,7 @@ export default {
|
|||||||
isInitialized: {
|
isInitialized: {
|
||||||
handler(isInitialized) {
|
handler(isInitialized) {
|
||||||
if (isInitialized && this.isEnabledGovernance) {
|
if (isInitialized && this.isEnabledGovernance) {
|
||||||
const { id } = this.data
|
this.fetchReceipt({ id: this.data.id })
|
||||||
this.fetchReceipt({ id })
|
|
||||||
this.fetchProposalComments(this.data)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
immediate: true
|
immediate: true
|
||||||
@ -282,13 +278,7 @@ export default {
|
|||||||
clearTimeout(this.timeId)
|
clearTimeout(this.timeId)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('governance/gov', [
|
...mapActions('governance/gov', ['castVote', 'executeProposal', 'fetchReceipt', 'fetchProposals']),
|
||||||
'castVote',
|
|
||||||
'executeProposal',
|
|
||||||
'fetchReceipt',
|
|
||||||
'fetchProposals',
|
|
||||||
'fetchProposalComments'
|
|
||||||
]),
|
|
||||||
getStatusType(status) {
|
getStatusType(status) {
|
||||||
let statusType = ''
|
let statusType = ''
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@ -393,3 +383,20 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.proposal {
|
||||||
|
&--description {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--comments {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--blocks {
|
||||||
|
position: sticky;
|
||||||
|
top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -2,39 +2,71 @@
|
|||||||
<div class="proposals-box">
|
<div class="proposals-box">
|
||||||
<div class="columns is-gapless">
|
<div class="columns is-gapless">
|
||||||
<div class="column proposals-box--tags">
|
<div class="column proposals-box--tags">
|
||||||
<div
|
<div class="proposals-box--tag-item">
|
||||||
class="tag"
|
<div class="tag proposals-box--id">
|
||||||
:class="{
|
<span><number-format :value="votes" /> TORN</span>
|
||||||
'proposals-box--revote': revote,
|
</div>
|
||||||
'is-primary': support,
|
|
||||||
'is-danger': !support
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span><number-format :value="votes" /> TORN</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b-tooltip v-if="delegator" :label="delegator" position="is-top">
|
<div class="proposals-box--tag-item">
|
||||||
<div class="tag proposals-box--id">{{ $t('delegated') }}</div>
|
<b-tooltip
|
||||||
</b-tooltip>
|
:label="ens.voter || voter"
|
||||||
|
position="is-top"
|
||||||
|
:multilined="ens.voter && ens.voter.length > 50"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
:href="addressExplorerUrl(voter)"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="tag proposals-box--id is-link"
|
||||||
|
v-text="shortVoter"
|
||||||
|
/>
|
||||||
|
</b-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<b-tooltip :label="voter" position="is-top">
|
<div v-if="delegator" class="proposals-box--tag-item">
|
||||||
<div class="tag proposals-box--id">{{ shortVoter }}</div>
|
<b-tooltip
|
||||||
</b-tooltip>
|
:label="ens.delegator || delegator"
|
||||||
</div>
|
position="is-top"
|
||||||
<div class="column is-narrow proposals-box--date">
|
:multilined="ens.delegator && ens.delegator.length > 50"
|
||||||
<div class="date">
|
>
|
||||||
<span>{{ $t('date') }}:</span> {{ date }}
|
<a
|
||||||
|
target="_blank"
|
||||||
|
:href="addressExplorerUrl(delegator)"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="tag proposals-box--id is-link"
|
||||||
|
v-text="$t('delegate')"
|
||||||
|
/>
|
||||||
|
</b-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="proposals-box--tag-item is-percentage">
|
||||||
|
<div class="tag proposals-box--id is-percentage">{{ percentage || '~0.1' }}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span v-if="contact" class="proposals-box--title">{{ contact }}</span>
|
<div class="proposals-box--comment">
|
||||||
<div v-if="message" class="proposals-box--info" v-text="message" />
|
<b-icon
|
||||||
|
:icon="support ? 'check' : 'close'"
|
||||||
|
:type="support ? 'is-primary' : 'is-danger'"
|
||||||
|
class="proposals-box--status-icon"
|
||||||
|
/>
|
||||||
|
<span v-if="loading" class="proposals-box--skeleton">
|
||||||
|
<b-skeleton height="21" width="260" style="width: auto;" />
|
||||||
|
</span>
|
||||||
|
<template v-else>
|
||||||
|
<span v-if="contact" class="proposals-box--title">{{ contact }}</span>
|
||||||
|
<span v-if="message" class="proposals-box--info">{{ message }}</span>
|
||||||
|
<span v-if="!contact && !message">-</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { sliceAddress } from '@/utils'
|
import { mapGetters } from 'vuex'
|
||||||
|
import { sliceAddress, sliceEnsName } from '@/utils'
|
||||||
import NumberFormat from '@/components/NumberFormat'
|
import NumberFormat from '@/components/NumberFormat'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -43,6 +75,10 @@ export default {
|
|||||||
},
|
},
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
contact: {
|
contact: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
@ -55,10 +91,6 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
timestamp: {
|
|
||||||
type: Number,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
votes: {
|
votes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
@ -67,56 +99,105 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
revote: {
|
percentage: {
|
||||||
type: Boolean,
|
type: Number,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
delegator: {
|
delegator: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
|
},
|
||||||
|
ens: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
validator: (props) => 'delegator' in props && 'voter' in props
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: (vm) => ({
|
computed: {
|
||||||
shortVoter: sliceAddress(vm.voter),
|
...mapGetters('txHashKeeper', ['addressExplorerUrl']),
|
||||||
date: [vm.$moment.unix(vm.timestamp).format('l'), vm.$moment.unix(vm.timestamp).format('hh:mm')].join(' ')
|
|
||||||
})
|
shortVoter() {
|
||||||
|
return sliceEnsName(this.ens.voter || '') || sliceAddress(this.voter)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
$margin: 0.714rem;
|
||||||
|
|
||||||
.proposals-box {
|
.proposals-box {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&.is-link {
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: #363636;
|
||||||
|
transition: background-color 0.15s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(#363636, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-percentage {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: transparent;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--tags {
|
&--tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(100px, auto));
|
margin: calc(#{-$margin * 0.5}) !important;
|
||||||
display: grid;
|
|
||||||
grid-row-gap: 0.714rem;
|
|
||||||
grid-column-gap: 0.714rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--date {
|
&--tag-item {
|
||||||
display: flex;
|
margin: calc(#{$margin * 0.5});
|
||||||
align-items: center;
|
width: auto;
|
||||||
}
|
min-width: 110px;
|
||||||
|
|
||||||
&--title {
|
@media screen and (max-width: 600px) {
|
||||||
display: flex;
|
width: calc(50% - #{$margin});
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-percentage {
|
||||||
|
min-width: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--title,
|
&--title,
|
||||||
&--info {
|
&--info {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--revote {
|
&--status-icon {
|
||||||
text-decoration: line-through;
|
vertical-align: middle;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--comment {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--skeleton {
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
91
components/governance/ProposalComments.vue
Normal file
91
components/governance/ProposalComments.vue
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div class="proposal-comments">
|
||||||
|
<div class="proposals-list--header proposal-comments--header">
|
||||||
|
<b-field class="field-tabs">
|
||||||
|
<template v-for="item in tabs">
|
||||||
|
<b-radio-button :key="item.id" v-model="currentTab" :native-value="item.id" type="is-primary">
|
||||||
|
<span>{{ $t(item.label) }}</span>
|
||||||
|
</b-radio-button>
|
||||||
|
</template>
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProposalCommentsSkeleton v-if="isFetchingComments" :size="comments.length ? 1 : 3" />
|
||||||
|
<ProposalComment
|
||||||
|
v-for="item in commentsFiltered"
|
||||||
|
:key="item.id"
|
||||||
|
v-bind="item"
|
||||||
|
:loading="isFetchingMessages"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapActions, mapGetters, mapState } from 'vuex'
|
||||||
|
import ProposalCommentsSkeleton from './ProposalCommentsSkeleton.vue'
|
||||||
|
import ProposalComment from './ProposalComment.vue'
|
||||||
|
|
||||||
|
const TabTypes = {
|
||||||
|
all: 'all',
|
||||||
|
for: 'for',
|
||||||
|
against: 'against'
|
||||||
|
}
|
||||||
|
|
||||||
|
const TAB_LIST = [
|
||||||
|
{ id: TabTypes.all, label: 'all' },
|
||||||
|
{ id: TabTypes.for, label: 'for' },
|
||||||
|
{ id: TabTypes.against, label: 'against' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ProposalCommentsSkeleton,
|
||||||
|
ProposalComment
|
||||||
|
},
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
proposal: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
tabs: TAB_LIST,
|
||||||
|
currentTab: TAB_LIST[0].id
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapState('governance/proposal', ['isFetchingComments', 'isFetchingMessages']),
|
||||||
|
...mapGetters('governance/proposal', ['comments']),
|
||||||
|
|
||||||
|
commentsFiltered() {
|
||||||
|
const { comments } = this
|
||||||
|
|
||||||
|
switch (this.currentTab) {
|
||||||
|
case TabTypes.for:
|
||||||
|
return comments.filter((_) => _.support === true)
|
||||||
|
|
||||||
|
case TabTypes.against:
|
||||||
|
return comments.filter((_) => _.support === false)
|
||||||
|
|
||||||
|
case TabTypes.all:
|
||||||
|
default:
|
||||||
|
return comments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchComments(this.proposal)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions('governance/proposal', ['fetchComments'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.proposal-comments {
|
||||||
|
&--header {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -2,12 +2,26 @@
|
|||||||
<div>
|
<div>
|
||||||
<div v-for="index in size" :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">
|
||||||
<div class="proposals-box--title">
|
<div class="proposals-box--title">
|
||||||
<b-skeleton height="21" width="210" />
|
<div class="proposals-box--skeleton">
|
||||||
|
<b-skeleton height="30" width="110" />
|
||||||
|
</div>
|
||||||
|
<div class="proposals-box--skeleton">
|
||||||
|
<b-skeleton height="30" width="110" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="proposals-box--skeleton is-percentage">
|
||||||
|
<b-skeleton height="30" width="36" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="proposals-box--info">
|
<div class="proposals-box--comment">
|
||||||
<b-skeleton height="21" width="260" />
|
<div class="proposals-box--skeleton">
|
||||||
|
<b-skeleton height="21" width="21" />
|
||||||
|
</div>
|
||||||
|
<div class="proposals-box--skeleton">
|
||||||
|
<b-skeleton height="21" width="260" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -25,3 +39,27 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$margin: 0.714rem;
|
||||||
|
|
||||||
|
.proposals-box {
|
||||||
|
&--title {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--skeleton {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&.is-percentage {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--comment {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -95,3 +95,5 @@ export const DUMMY_NONCE = '0x11111111111111111111111111111111111111111111111111
|
|||||||
|
|
||||||
export const DUMMY_WITHDRAW_DATA =
|
export const DUMMY_WITHDRAW_DATA =
|
||||||
'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111'
|
'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111'
|
||||||
|
|
||||||
|
export const CHUNK_COUNT_PER_BATCH_REQUEST = 200
|
||||||
|
@ -285,10 +285,10 @@
|
|||||||
},
|
},
|
||||||
"proposalComment": {
|
"proposalComment": {
|
||||||
"modal-title": "Title: Proposal #{id}",
|
"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": "Contact",
|
||||||
"form-contact-placeholder": "Enter contact (optional)",
|
"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": "Message",
|
||||||
"form-message-placeholder": "Enter message",
|
"form-message-placeholder": "Enter message",
|
||||||
"form-message-opt-placeholder": "Enter message (optional)",
|
"form-message-opt-placeholder": "Enter message (optional)",
|
||||||
|
@ -116,7 +116,7 @@ export default {
|
|||||||
ensSubdomainKey: 'mainnet-tornado',
|
ensSubdomainKey: 'mainnet-tornado',
|
||||||
pollInterval: 15,
|
pollInterval: 15,
|
||||||
constants: {
|
constants: {
|
||||||
GOVERNANCE_TORNADOCASH_BLOCK: 11474695,
|
GOVERNANCE_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
|
||||||
@ -535,7 +535,7 @@ export default {
|
|||||||
ensSubdomainKey: 'goerli-tornado',
|
ensSubdomainKey: 'goerli-tornado',
|
||||||
pollInterval: 15,
|
pollInterval: 15,
|
||||||
constants: {
|
constants: {
|
||||||
GOVERNANCE_TORNADOCASH_BLOCK: 3945171,
|
GOVERNANCE_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
|
||||||
|
@ -9,6 +9,7 @@ export * from './events'
|
|||||||
export { default as graph } from './graph'
|
export { default as graph } from './graph'
|
||||||
export { default as schema } from './schema'
|
export { default as schema } from './schema'
|
||||||
export { default as walletConnectConnector } from './walletConnect'
|
export { default as walletConnectConnector } from './walletConnect'
|
||||||
|
export * from './lookupAddress'
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
window.graph = graph
|
window.graph = graph
|
||||||
|
114
services/lookupAddress.js
Normal file
114
services/lookupAddress.js
Normal file
@ -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
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
/* 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 { utils } from 'ethers'
|
||||||
import { ToastProgrammatic as Toast } from 'buefy'
|
import { ToastProgrammatic as Toast } from 'buefy'
|
||||||
@ -16,19 +17,17 @@ 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,
|
isCastingVote: false,
|
||||||
proposals: [],
|
proposals: [],
|
||||||
voterReceipts: [],
|
voterReceipts: [],
|
||||||
hasActiveProposals: false,
|
hasActiveProposals: false,
|
||||||
@ -74,9 +73,6 @@ 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))
|
||||||
@ -108,11 +104,8 @@ 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) {
|
SAVE_CASTING_VOTE(state, status) {
|
||||||
this._vm.$set(state, 'isSaveProposal', status)
|
this._vm.$set(state, 'isCastingVote', 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)
|
||||||
@ -129,9 +122,6 @@ 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)
|
||||||
},
|
},
|
||||||
@ -359,7 +349,7 @@ const actions = {
|
|||||||
const { getters, rootGetters, commit, rootState, dispatch, state } = context
|
const { getters, rootGetters, commit, rootState, dispatch, state } = context
|
||||||
const { id, support, contact = '', message = '' } = payload
|
const { id, support, contact = '', message = '' } = payload
|
||||||
|
|
||||||
commit('SAVE_SAVE_PROPOSAL', true)
|
commit('SAVE_CASTING_VOTE', true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { ethAccount } = rootState.metamask
|
const { ethAccount } = rootState.metamask
|
||||||
@ -433,7 +423,7 @@ const actions = {
|
|||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
dispatch('loading/disable', {}, { root: true })
|
dispatch('loading/disable', {}, { root: true })
|
||||||
commit('SAVE_SAVE_PROPOSAL', false)
|
commit('SAVE_CASTING_VOTE', false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async executeProposal({ getters, rootGetters, commit, rootState, dispatch }, { id }) {
|
async executeProposal({ getters, rootGetters, commit, rootState, dispatch }, { id }) {
|
||||||
@ -669,7 +659,7 @@ const actions = {
|
|||||||
|
|
||||||
const [events, statuses] = await Promise.all([
|
const [events, statuses] = await Promise.all([
|
||||||
govInstance.getPastEvents('ProposalCreated', {
|
govInstance.getPastEvents('ProposalCreated', {
|
||||||
fromBlock: config.constants.GOVERNANCE_TORNADOCASH_BLOCK,
|
fromBlock: config.constants.GOVERNANCE_BLOCK,
|
||||||
toBlock: 'latest'
|
toBlock: 'latest'
|
||||||
}),
|
}),
|
||||||
aggregatorContract.methods.getAllProposals(govInstance._address).call()
|
aggregatorContract.methods.getAllProposals(govInstance._address).call()
|
||||||
@ -819,14 +809,14 @@ const actions = {
|
|||||||
filter: {
|
filter: {
|
||||||
to: ethAccount
|
to: ethAccount
|
||||||
},
|
},
|
||||||
fromBlock: config.constants.GOVERNANCE_TORNADOCASH_BLOCK,
|
fromBlock: config.constants.GOVERNANCE_BLOCK,
|
||||||
toBlock: 'latest'
|
toBlock: 'latest'
|
||||||
})
|
})
|
||||||
let undelegatedAccs = await govInstance.getPastEvents('Undelegated', {
|
let undelegatedAccs = await govInstance.getPastEvents('Undelegated', {
|
||||||
filter: {
|
filter: {
|
||||||
from: ethAccount
|
from: ethAccount
|
||||||
},
|
},
|
||||||
fromBlock: config.constants.GOVERNANCE_TORNADOCASH_BLOCK,
|
fromBlock: config.constants.GOVERNANCE_BLOCK,
|
||||||
toBlock: 'latest'
|
toBlock: 'latest'
|
||||||
})
|
})
|
||||||
delegatedAccs = delegatedAccs.map((acc) => acc.returnValues.account)
|
delegatedAccs = delegatedAccs.map((acc) => acc.returnValues.account)
|
||||||
@ -883,109 +873,6 @@ 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)
|
||||||
|
269
store/governance/proposal.js
Normal file
269
store/governance/proposal.js
Normal file
@ -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
|
||||||
|
}
|
@ -10,6 +10,30 @@ export const sliceAddress = (address) => {
|
|||||||
return '0x' + hashRender(address.slice(2))
|
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 = /^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
const semVerRegex = /^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
|
|
||||||
export const parseSemanticVersion = (version) => {
|
export const parseSemanticVersion = (version) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user