From a57309b57b21f87abdfec4cfe1a48faefc829c14 Mon Sep 17 00:00:00 2001 From: Kirill Goncharov Date: Fri, 28 Aug 2020 21:24:46 +0300 Subject: [PATCH 1/6] Frontend: Implement voting --- vue-app/package.json | 1 + vue-app/src/api/projects.ts | 2 +- vue-app/src/api/round.ts | 9 + vue-app/src/components/Cart.vue | 77 +------ vue-app/src/components/ContributionModal.vue | 200 +++++++++++++++++++ vue-app/src/main.ts | 4 + vue-app/src/utils/maci.ts | 49 +++++ yarn.lock | 12 ++ 8 files changed, 286 insertions(+), 68 deletions(-) create mode 100644 vue-app/src/components/ContributionModal.vue create mode 100644 vue-app/src/utils/maci.ts diff --git a/vue-app/package.json b/vue-app/package.json index cbbb9b36a..626c24236 100644 --- a/vue-app/package.json +++ b/vue-app/package.json @@ -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" diff --git a/vue-app/src/api/projects.ts b/vue-app/src/api/projects.ts index e0c6da036..c3a6c783b 100644 --- a/vue-app/src/api/projects.ts +++ b/vue-app/src/api/projects.ts @@ -22,7 +22,7 @@ export async function getProjects(): Promise { name: metadata.name, description: metadata.description, imageUrl: `${ipfsGatewayUrl}${metadata.imageHash}`, - index: event.args._index, + index: event.args._index.toNumber(), }) }) return projects diff --git a/vue-app/src/api/round.ts b/vue-app/src/api/round.ts index 204763351..0df5666ea 100644 --- a/vue-app/src/api/round.ts +++ b/vue-app/src/api/round.ts @@ -1,5 +1,7 @@ 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' @@ -7,6 +9,7 @@ import { provider, factory } from './core' export interface RoundInfo { fundingRoundAddress: string; maciAddress: string; + coordinatorPubKey: PubKey; nativeTokenAddress: string; nativeTokenSymbol: string; nativeTokenDecimals: number; @@ -37,6 +40,11 @@ export async function getRoundInfo(): Promise { 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, @@ -88,6 +96,7 @@ export async function getRoundInfo(): Promise { return { fundingRoundAddress, maciAddress, + coordinatorPubKey, nativeTokenAddress, nativeTokenSymbol, nativeTokenDecimals, diff --git a/vue-app/src/components/Cart.vue b/vue-app/src/components/Cart.vue index be14b651d..0e763c6bb 100644 --- a/vue-app/src/components/Cart.vue +++ b/vue-app/src/components/Cart.vue @@ -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 { - 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[]) { @@ -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 } } diff --git a/vue-app/src/components/ContributionModal.vue b/vue-app/src/components/ContributionModal.vue new file mode 100644 index 000000000..b97c70ab8 --- /dev/null +++ b/vue-app/src/components/ContributionModal.vue @@ -0,0 +1,200 @@ + + + + + + diff --git a/vue-app/src/main.ts b/vue-app/src/main.ts index 8d632abcc..ebdcc3319 100644 --- a/vue-app/src/main.ts +++ b/vue-app/src/main.ts @@ -3,6 +3,10 @@ import App from './App.vue' import router from './router' import store from './store' +import VModal from 'vue-js-modal' + +Vue.use(VModal) + Vue.config.productionTip = false new Vue({ diff --git a/vue-app/src/utils/maci.ts b/vue-app/src/utils/maci.ts new file mode 100644 index 000000000..7e79fd924 --- /dev/null +++ b/vue-app/src/utils/maci.ts @@ -0,0 +1,49 @@ +import { BigNumber } from 'ethers' +import { bigInt, genRandomSalt } from 'maci-crypto' +import { Keypair, PubKey, Command, Message } from 'maci-domainobjs' + +function bnSqrt(a: BigNumber): BigNumber { + // Take square root from a big number + // https://stackoverflow.com/a/52468569/1868395 + if (a.isZero()) { + return a + } + let x + let x1 = a.div(2) + do { + x = x1 + x1 = (x.add(a.div(x))).div(2) + } while (!x.eq(x1)) + return x +} + +export function createMessage( + userStateIndex: number, + userKeypair: Keypair, + newUserKeypair: Keypair | null, + coordinatorPubKey: PubKey, + voteOptionIndex: number | null, + voiceCredits: BigNumber | null, + nonce: number, + salt?: number, +): [Message, PubKey] { + const encKeypair = new Keypair() + if (!salt) { + salt = genRandomSalt() + } + const quadraticVoteWeight = voiceCredits ? bnSqrt(voiceCredits) : 0 + const command = new Command( + bigInt(userStateIndex), + newUserKeypair ? newUserKeypair.pubKey : userKeypair.pubKey, + bigInt(voteOptionIndex || 0), + bigInt(quadraticVoteWeight), + bigInt(nonce), + bigInt(salt), + ) + const signature = command.sign(userKeypair.privKey) + const message = command.encrypt( + signature, + Keypair.genEcdhSharedKey(encKeypair.privKey, coordinatorPubKey), + ) + return [message, encKeypair.pubKey] +} diff --git a/yarn.lock b/yarn.lock index d03627474..6bee1d29d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11507,6 +11507,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -13759,6 +13764,13 @@ vue-hot-reload-api@^2.3.0: resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2" integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog== +vue-js-modal@^2.0.0-rc.6: + version "2.0.0-rc.6" + resolved "https://registry.yarnpkg.com/vue-js-modal/-/vue-js-modal-2.0.0-rc.6.tgz#2fd596c79a713d2cbf447150abb5fefce65efd2d" + integrity sha512-bJOm7Yhrl0ur/QyXjoC3gMMmE7UxiVEcS2rl8v9iPXIe9QLvjiCSZElSOvvyps8LNuG1X0rPifZGxI/CWKCFaw== + dependencies: + resize-observer-polyfill "^1.5.1" + vue-loader@^15.9.2: version "15.9.3" resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.3.tgz#0de35d9e555d3ed53969516cac5ce25531299dda" From 9823c57d344bd3ff1de9afd4101bdebb31fc1608 Mon Sep 17 00:00:00 2001 From: Kirill Goncharov Date: Thu, 3 Sep 2020 00:23:45 +0300 Subject: [PATCH 2/6] Frontend: Set cart size limit --- vue-app/src/App.vue | 6 ++++++ vue-app/src/components/ProjectItem.vue | 8 ++++++++ vue-app/src/styles/_vars.scss | 1 + 3 files changed, 15 insertions(+) diff --git a/vue-app/src/App.vue b/vue-app/src/App.vue index 00c403761..01fc74d56 100644 --- a/vue-app/src/App.vue +++ b/vue-app/src/App.vue @@ -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 { diff --git a/vue-app/src/components/ProjectItem.vue b/vue-app/src/components/ProjectItem.vue index 0d96660a2..976f7e4b8 100644 --- a/vue-app/src/components/ProjectItem.vue +++ b/vue-app/src/components/ProjectItem.vue @@ -6,6 +6,7 @@
{{ project.description }}
diff --git a/vue-app/src/filters/index.ts b/vue-app/src/filters/index.ts new file mode 100644 index 000000000..c34326d89 --- /dev/null +++ b/vue-app/src/filters/index.ts @@ -0,0 +1,11 @@ +import Vue from 'vue' +import { FixedNumber } from 'ethers' +import { DateTime } from 'luxon' + +Vue.filter('formatDate', (value: DateTime): string | null => { + return value ? value.toLocaleString(DateTime.DATETIME_SHORT) : null +}) + +Vue.filter('formatAmount', (value: FixedNumber): string | null => { + return value ? (value._value === '0.0' ? '0' : value.toString()) : null +}) diff --git a/vue-app/src/main.ts b/vue-app/src/main.ts index ebdcc3319..0bf199186 100644 --- a/vue-app/src/main.ts +++ b/vue-app/src/main.ts @@ -2,6 +2,7 @@ import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' +import './filters' import VModal from 'vue-js-modal' diff --git a/vue-app/src/views/Home.vue b/vue-app/src/views/Home.vue index 4c6f3c088..39198edef 100644 --- a/vue-app/src/views/Home.vue +++ b/vue-app/src/views/Home.vue @@ -50,7 +50,6 @@ import Vue from 'vue' import Component from 'vue-class-component' import { FixedNumber } from 'ethers' -import { DateTime } from 'luxon' import { getContributionAmount } from '@/api/contributions' import { RoundInfo, getRoundInfo } from '@/api/round' @@ -64,14 +63,6 @@ import { SET_CURRENT_ROUND, SET_CONTRIBUTION } from '@/store/mutation-types' components: { ProjectItem, }, - filters: { - formatDate: (value: DateTime): string | null => { - return value ? value.toLocaleString(DateTime.DATETIME_SHORT) : null - }, - formatAmount: (value: FixedNumber): string | null => { - return value ? (value._value === '0.0' ? '0' : value.toString()) : null - }, - }, }) export default class Home extends Vue { From c168742b9bfb4f14828890e6c29a111fd0be6551 Mon Sep 17 00:00:00 2001 From: Kirill Goncharov Date: Thu, 3 Sep 2020 00:43:29 +0300 Subject: [PATCH 4/6] Frontend: Disable source maps in production --- vue-app/vue.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/vue-app/vue.config.js b/vue-app/vue.config.js index 640b680e7..256d446ad 100644 --- a/vue-app/vue.config.js +++ b/vue-app/vue.config.js @@ -1,3 +1,4 @@ module.exports = { publicPath: './', + productionSourceMap: false, } From 9f230a283c69b5e6e46ae88278714b4c431cd115 Mon Sep 17 00:00:00 2001 From: Kirill Goncharov Date: Thu, 3 Sep 2020 00:46:50 +0300 Subject: [PATCH 5/6] Frontend: Update favicon --- vue-app/public/favicon.ico | Bin 4286 -> 4286 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/vue-app/public/favicon.ico b/vue-app/public/favicon.ico index df36fcfb72584e00488330b560ebcf34a41c64c2..74c783347285f4bf749c847acfae7c9b1b33b915 100644 GIT binary patch literal 4286 zcmcIoTTEP46g`a+zm$IXYT5y;NkynBCKgLtyN;#3GILi${HZ zmRFnF0>jJz(HQ2<4A%HjA3STdP|MH{D6g4;GL&&(d)9UK&E*0!yxip6bLQNA_uBWI zefC}#A-2iCv^0UMFl`s&Q6WUSY;sB%WyAbN#cFC6haNO0xo!5W+y3E_-mY%P;EjRt z$%%<*BoZMsj4_9MxBdPSr_|ji#QiNM(?(&{=UOe6+|iK{_sq;paA9GA63IezfuMV5 zWW?2Gu^3fDYaEy_HwrXYyTj1i)paQ{A6Z-p6TZyN&CxyCR`cs0xi)=0J+01myTSDB zWr3DRV)*%?U}!*ZcP<>JPEQBbovWs*iV7_5bPP&T>B1NF~(kSJm5^>Y_`bUa_}Kz63#dKhgidYZw02-2I2v>StO?G&~{^z z$L`ERzTkPp?s{TPixF=On-+P7SR`h^WwT`s`~4?)-*LWkvlE}OcsTLy;rxar1_XI> zc*uW3#)6A|^13}+zV3Q|kfT^+y|CZBF)+cuYh_;3>u>e%y6S3%Bh;+G)KnA`*n_I_ zaut8iBRgn!^217ddS|LSzgd5fw|T#(CMT5|3VhTO+7e_J}pz{ z`Tj=x)2B+6ngz`H`FS-r>G6NNdKD^)T)J@af5Q(10xPb6o5jlT^!qQrZW+FQ{cjFv z#es3e_#P$8@hGtEsn?VY?zGu4=5PvkME-MHMOj&^qpQG73954={pP?@&ow? zISv9$nSV;$Hs`b2{M+2nsOI12(lhAiD?cgve32ZKzOFn=*%^D+<--d3H|UZ)HAlRV zbZ2zb&GyG+jVL}=q}o3IS^-_Z_6Ofq`|#{6_4oGD$+zF73`3f#zuJBj%bK>V{v;cd zl=^E~T7TURhiWfV+Ri_mr_^MQN7V^A_`I}?){>u4)7APLi7d97&AC<$KbLi&nd1QV zvG?E({65b}-dFn3$MpB$Ao!%}LLz){7;qUDU`VVc<{#X5I9-P6(6q|Gkekle)TsFI z1LtpldM17N{s;8UmtRqRbqy6CEuzenCzj~3&**{8+y*{K-v^!T?J11QeRD&DkgY(^ zKd~2xhxKp*=z_yw&9x$J2PMDH2glb-3}0OF`HsriTEf2(lQUOJ#?T8P77YW%-}jFtdgH2MLW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S From e5fa162658e1b6ebda58ce2771961b02691ddcc7 Mon Sep 17 00:00:00 2001 From: Kirill Goncharov Date: Thu, 3 Sep 2020 01:06:17 +0300 Subject: [PATCH 6/6] Frontend: Reload current round after contributing --- vue-app/src/components/ContributionModal.vue | 2 ++ vue-app/src/store/action-types.ts | 1 + vue-app/src/store/index.ts | 10 ++++++++-- vue-app/src/views/Home.vue | 14 +++++++------- 4 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 vue-app/src/store/action-types.ts diff --git a/vue-app/src/components/ContributionModal.vue b/vue-app/src/components/ContributionModal.vue index 03af0e645..717b47ace 100644 --- a/vue-app/src/components/ContributionModal.vue +++ b/vue-app/src/components/ContributionModal.vue @@ -32,6 +32,7 @@ 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' @@ -115,6 +116,7 @@ export default class ContributionModal extends Vue { this.$store.state.cart.slice().forEach((item) => { this.$store.commit(REMOVE_CART_ITEM, item) }) + this.$store.dispatch(LOAD_ROUND_INFO) } vote() { diff --git a/vue-app/src/store/action-types.ts b/vue-app/src/store/action-types.ts new file mode 100644 index 000000000..e1c311ad5 --- /dev/null +++ b/vue-app/src/store/action-types.ts @@ -0,0 +1 @@ +export const LOAD_ROUND_INFO = 'LOAD_ROUND_INFO' diff --git a/vue-app/src/store/index.ts b/vue-app/src/store/index.ts index d601fd535..090b9187f 100644 --- a/vue-app/src/store/index.ts +++ b/vue-app/src/store/index.ts @@ -4,7 +4,8 @@ import { FixedNumber } from 'ethers' import { Web3Provider } from '@ethersproject/providers' import { CartItem } from '@/api/contributions' -import { RoundInfo } from '@/api/round' +import { RoundInfo, getRoundInfo } from '@/api/round' +import { LOAD_ROUND_INFO } from './action-types' import { SET_WALLET_PROVIDER, SET_ACCOUNT, @@ -71,7 +72,12 @@ const store: StoreOptions = { } }, }, - actions: {}, + actions: { + async [LOAD_ROUND_INFO]({ commit }) { + const currentRound = await getRoundInfo() + commit(SET_CURRENT_ROUND, currentRound) + }, + }, modules: {}, } diff --git a/vue-app/src/views/Home.vue b/vue-app/src/views/Home.vue index 39198edef..31b33178e 100644 --- a/vue-app/src/views/Home.vue +++ b/vue-app/src/views/Home.vue @@ -52,11 +52,12 @@ import Component from 'vue-class-component' import { FixedNumber } from 'ethers' import { getContributionAmount } from '@/api/contributions' -import { RoundInfo, getRoundInfo } from '@/api/round' +import { RoundInfo } from '@/api/round' import { Project, getProjects } from '@/api/projects' import ProjectItem from '@/components/ProjectItem.vue' -import { SET_CURRENT_ROUND, SET_CONTRIBUTION } from '@/store/mutation-types' +import { LOAD_ROUND_INFO } from '@/store/action-types' +import { SET_CONTRIBUTION } from '@/store/mutation-types' @Component({ name: 'Home', @@ -73,14 +74,13 @@ export default class Home extends Vue { } private async updateCurrentRound() { - const currentRound = await getRoundInfo() - this.$store.commit(SET_CURRENT_ROUND, currentRound) + await this.$store.dispatch(LOAD_ROUND_INFO) const walletAddress = this.$store.state.account - if (currentRound && walletAddress) { + if (this.currentRound && walletAddress) { const contribution = await getContributionAmount( walletAddress, - currentRound.fundingRoundAddress, - currentRound.nativeTokenDecimals, + this.currentRound.fundingRoundAddress, + this.currentRound.nativeTokenDecimals, ) this.$store.commit(SET_CONTRIBUTION, contribution) }