Skip to content

Commit

Permalink
Merge pull request #111 from xuhcc/voting
Browse files Browse the repository at this point in the history
Voting
  • Loading branch information
xuhcc authored Sep 8, 2020
2 parents 8dce70f + e5fa162 commit afe36b6
Show file tree
Hide file tree
Showing 17 changed files with 332 additions and 86 deletions.
1 change: 1 addition & 0 deletions vue-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"maci-domainobjs": "^0.1.8",
"vue": "^2.6.11",
"vue-class-component": "^7.2.2",
"vue-js-modal": "^2.0.0-rc.6",
"vue-property-decorator": "^8.3.0",
"vue-router": "^3.1.5",
"vuex": "^3.1.2"
Expand Down
Binary file modified vue-app/public/favicon.ico
Binary file not shown.
6 changes: 6 additions & 0 deletions vue-app/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ html {
background-color: $highlight-color;
color: $bg-secondary-color;
}
&[disabled],
&[disabled]:hover {
background-color: $button-disabled-color !important;
color: $text-color !important;
}
}
#app {
Expand Down
2 changes: 1 addition & 1 deletion vue-app/src/api/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function getProjects(): Promise<Project[]> {
name: metadata.name,
description: metadata.description,
imageUrl: `${ipfsGatewayUrl}${metadata.imageHash}`,
index: event.args._index,
index: event.args._index.toNumber(),
})
})
return projects
Expand Down
9 changes: 9 additions & 0 deletions vue-app/src/api/round.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { ethers, BigNumber, FixedNumber } from 'ethers'
import { DateTime } from 'luxon'
import { bigInt } from 'maci-crypto'
import { PubKey } from 'maci-domainobjs'

import { FundingRound, ERC20 } from './abi'
import { provider, factory } from './core'

export interface RoundInfo {
fundingRoundAddress: string;
maciAddress: string;
coordinatorPubKey: PubKey;
nativeTokenAddress: string;
nativeTokenSymbol: string;
nativeTokenDecimals: number;
Expand Down Expand Up @@ -37,6 +40,11 @@ export async function getRoundInfo(): Promise<RoundInfo | null> {
provider,
)
const maciAddress = await fundingRound.maci()
const coordinatorPubKeyRaw = await fundingRound.coordinatorPubKey()
const coordinatorPubKey = new PubKey([
bigInt(coordinatorPubKeyRaw.x),
bigInt(coordinatorPubKeyRaw.y),
])
const nativeTokenAddress = await fundingRound.nativeToken()
const nativeToken = new ethers.Contract(
nativeTokenAddress,
Expand Down Expand Up @@ -88,6 +96,7 @@ export async function getRoundInfo(): Promise<RoundInfo | null> {
return {
fundingRoundAddress,
maciAddress,
coordinatorPubKey,
nativeTokenAddress,
nativeTokenSymbol,
nativeTokenDecimals,
Expand Down
77 changes: 10 additions & 67 deletions vue-app/src/components/Cart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,66 +35,18 @@
import Vue from 'vue'
import Component from 'vue-class-component'
import { DateTime } from 'luxon'
import { Contract, FixedNumber } from 'ethers'
import { Web3Provider } from '@ethersproject/providers'
import { parseFixed } from '@ethersproject/bignumber'
import { Keypair } from 'maci-domainobjs'
import ContributionModal from '@/components/ContributionModal.vue'
import { CartItem } from '@/api/contributions'
import {
ADD_CART_ITEM,
UPDATE_CART_ITEM,
REMOVE_CART_ITEM,
SET_CONTRIBUTION,
} from '@/store/mutation-types'
import { getEventArg } from '@/utils/contracts'
import { FundingRound, ERC20, MACI } from '@/api/abi'
const CART_STORAGE_KEY = 'clrfund-cart'
interface ContributorData {
privateKey: string;
stateIndex: number;
contribution: FixedNumber;
voiceCredits: number;
}
async function contribute(
provider: Web3Provider,
tokenAddress: string,
tokenDecimals: number,
fundingRoundAddress: string,
maciAddress: string,
amount: number,
): Promise<ContributorData> {
const signer = provider.getSigner()
const token = new Contract(tokenAddress, ERC20, signer)
const amountRaw = parseFixed(amount.toString(), tokenDecimals)
// Approve transfer
const allowance = await token.allowance(signer.getAddress(), fundingRoundAddress)
if (allowance < amountRaw) {
await token.approve(fundingRoundAddress, amountRaw)
}
// Contribute
const contributorKeypair = new Keypair()
const fundingRound = new Contract(fundingRoundAddress, FundingRound, signer)
const contributionTx = await fundingRound.contribute(
contributorKeypair.pubKey.asContractParam(),
amountRaw,
)
// Get state index and amount of voice credits
const maci = new Contract(maciAddress, MACI, signer)
const stateIndex = await getEventArg(contributionTx, maci, 'SignUp', '_stateIndex')
const voiceCredits = await getEventArg(contributionTx, maci, 'SignUp', '_voiceCreditBalance')
return {
privateKey: contributorKeypair.privKey.serialize(),
stateIndex,
contribution: FixedNumber.fromValue(amountRaw, tokenDecimals),
voiceCredits,
}
}
@Component({
watch: {
cart(items: CartItem[]) {
Expand Down Expand Up @@ -157,24 +109,15 @@ export default class Cart extends Vue {
}
async contribute() {
const walletProvider = this.$store.state.walletProvider
const currentRound = this.$store.state.currentRound
if (!walletProvider || !currentRound) {
return
}
const contributorData = await contribute(
walletProvider,
currentRound.nativeTokenAddress,
currentRound.nativeTokenDecimals,
currentRound.fundingRoundAddress,
currentRound.maciAddress,
this.total,
this.$modal.show(
ContributionModal,
{ },
{
clickToClose: false,
height: 'auto',
width: 450,
},
)
this.$store.commit(SET_CONTRIBUTION, contributorData.contribution)
this.cart.slice().forEach((item) => {
this.$store.commit(REMOVE_CART_ITEM, item)
})
console.info(contributorData) // eslint-disable-line no-console
}
}
</script>
Expand Down
202 changes: 202 additions & 0 deletions vue-app/src/components/ContributionModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<template>
<div class="modal-body">
<div v-if="contributionStep === 1">
<h3>Step 1 of 4: Approve</h3>
<div>Please confirm transaction in your wallet</div>
<div class="loader"></div>
</div>
<div v-if="contributionStep === 2">
<h3>Step 2 of 4: Contribute</h3>
<div>Please confirm transaction in your wallet</div>
<div class="loader"></div>
</div>
<div v-if="contributionStep === 3">
<h3>Step 3 of 4: Are you being bribed?</h3>
<button class="btn" @click="vote()">No</button>
</div>
<div v-if="contributionStep === 4">
<h3>Step 4 of 4: Vote</h3>
<div>Please send this transaction to {{ currentRound.fundingRoundAddress }} after {{ currentRound.contributionDeadline | formatDate }}:</div>
<div class="hex">{{ voteTxData }}</div>
<button class="btn" @click="$emit('close')">Done</button>
</div>
</div>
</template>

<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import { BigNumber, Contract, FixedNumber, Signer } from 'ethers'
import { parseFixed } from '@ethersproject/bignumber'
import { Keypair, PubKey, Message } from 'maci-domainobjs'
import { CartItem } from '@/api/contributions'
import { RoundInfo } from '@/api/round'
import { LOAD_ROUND_INFO } from '@/store/action-types'
import { REMOVE_CART_ITEM, SET_CONTRIBUTION } from '@/store/mutation-types'
import { getEventArg } from '@/utils/contracts'
import { createMessage } from '@/utils/maci'
import { FundingRound, ERC20, MACI } from '@/api/abi'
const VOICE_CREDIT_FACTOR = BigNumber.from(10).pow(4 + 18 - 9)
interface Contributor {
keypair: Keypair;
stateIndex: number;
contribution: FixedNumber;
voiceCredits: BigNumber;
}
@Component
export default class ContributionModal extends Vue {
contributionStep = 1
private amount: BigNumber = BigNumber.from(0)
private votes: [number, BigNumber][] = []
private contributor?: Contributor
voteTxData = ''
mounted() {
const { nativeTokenDecimals } = this.currentRound
this.$store.state.cart.forEach((item: CartItem) => {
const amountRaw = parseFixed(item.amount.toString(), nativeTokenDecimals)
const voiceCredits = amountRaw.div(VOICE_CREDIT_FACTOR)
this.votes.push([item.index, voiceCredits])
this.amount = this.amount.add(voiceCredits.mul(VOICE_CREDIT_FACTOR))
})
this.contribute()
}
get currentRound(): RoundInfo {
return this.$store.state.currentRound
}
private getSigner(): Signer {
const provider = this.$store.state.walletProvider
return provider.getSigner()
}
private async contribute() {
const signer = this.getSigner()
const {
nativeTokenAddress,
nativeTokenDecimals,
maciAddress,
fundingRoundAddress,
} = this.currentRound
const token = new Contract(nativeTokenAddress, ERC20, signer)
// Approve transfer
const allowance = await token.allowance(signer.getAddress(), fundingRoundAddress)
if (allowance < this.amount) {
await token.approve(fundingRoundAddress, this.amount)
}
this.contributionStep += 1
// Contribute
const contributorKeypair = new Keypair()
const fundingRound = new Contract(fundingRoundAddress, FundingRound, signer)
const contributionTx = await fundingRound.contribute(
contributorKeypair.pubKey.asContractParam(),
this.amount,
)
// Get state index and amount of voice credits
const maci = new Contract(maciAddress, MACI, signer)
const stateIndex = await getEventArg(contributionTx, maci, 'SignUp', '_stateIndex')
const voiceCredits = await getEventArg(contributionTx, maci, 'SignUp', '_voiceCreditBalance')
this.contributionStep += 1
this.contributor = {
keypair: contributorKeypair,
stateIndex,
contribution: FixedNumber.fromValue(this.amount, nativeTokenDecimals),
voiceCredits,
}
// Set contribution and clear the cart
this.$store.commit(SET_CONTRIBUTION, this.contributor.contribution)
this.$store.state.cart.slice().forEach((item) => {
this.$store.commit(REMOVE_CART_ITEM, item)
})
this.$store.dispatch(LOAD_ROUND_INFO)
}
vote() {
if (!this.contributor) {
return
}
this.contributionStep += 1
const { coordinatorPubKey, fundingRoundAddress } = this.currentRound
const messages: Message[] = []
const encPubKeys: PubKey[] = []
let nonce = 1
for (const [recipientIndex, voiceCredits] of this.votes) {
const [message, encPubKey] = createMessage(
this.contributor.stateIndex,
this.contributor.keypair, null,
coordinatorPubKey,
recipientIndex, voiceCredits, nonce,
)
messages.push(message)
encPubKeys.push(encPubKey)
nonce += 1
}
const fundingRound = new Contract(fundingRoundAddress, FundingRound)
this.voteTxData = fundingRound.interface.encodeFunctionData('submitMessageBatch', [
messages.map((msg) => msg.asContractParam()),
encPubKeys.map((key) => key.asContractParam()),
])
}
}
</script>


<style scoped lang="scss">
@import '../styles/vars';
.modal-body {
background-color: $bg-light-color;
font-family: Inter, sans-serif;
font-size: 14px;
padding: 10px 20px 20px;
text-align: center;
}
.loader {
display: block;
width: 40px;
height: 40px;
margin: 20px auto;
}
.loader:after {
content: " ";
display: block;
width: 32px;
height: 32px;
margin: 4px;
border-radius: 50%;
border: 6px solid #fff;
border-color: #fff transparent #fff transparent;
animation: loader 1.2s linear infinite;
}
@keyframes loader {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.hex {
background-color: #666;
font-family: monospace;
font-size: 12px;
height: 50px;
padding: 5px;
margin: 10px 0;
overflow-y: scroll;
text-align: left;
word-wrap: break-word;
}
</style>
8 changes: 8 additions & 0 deletions vue-app/src/components/ProjectItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<div class="project-description">{{ project.description }}</div>
<button
class="btn contribute-btn"
:disabled="!canContribute()"
@click="contribute(project)"
>
Contribute
Expand All @@ -21,11 +22,18 @@ import { Component, Prop } from 'vue-property-decorator'
import { Project } from '@/api/projects'
import { ADD_CART_ITEM } from '@/store/mutation-types'
// A size of message batch
const CART_MAX_SIZE = 10
@Component
export default class ProjectItem extends Vue {
@Prop()
project!: Project;
canContribute() {
return this.$store.state.cart.length < CART_MAX_SIZE
}
contribute(project: Project) {
this.$store.commit(ADD_CART_ITEM, { ...project, amount: 0 })
}
Expand Down
Loading

0 comments on commit afe36b6

Please sign in to comment.