diff --git a/package-lock.json b/package-lock.json index 06406c87..63439cc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "balancer-gov", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2987,6 +2987,14 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, + "autolinker": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-3.14.1.tgz", + "integrity": "sha512-yvsRHIaY51EYDml6MGlbqyJGfl4n7zezGYf+R7gvM8c5LNpRGc4SISkvgAswSS8SWxk/OrGCylKV9mJyVstz7w==", + "requires": { + "tslib": "^1.9.3" + } + }, "autoprefixer": { "version": "9.8.0", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.0.tgz", @@ -12118,6 +12126,15 @@ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" }, + "remarkable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-2.0.1.tgz", + "integrity": "sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==", + "requires": { + "argparse": "^1.0.10", + "autolinker": "^3.11.0" + } + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -12400,6 +12417,53 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sanitize-html": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.1.tgz", + "integrity": "sha512-C+N7E+7ikYaLHdb9lEkQaFOgmj+9ddZ311Ixs/QsBsoLD411/vdLweiFyGqrswUVgLqagOS5NCDxcEPH7trObQ==", + "requires": { + "htmlparser2": "^4.1.0", + "lodash": "^4.17.15", + "postcss": "^7.0.27", + "srcset": "^2.0.1" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" + }, + "domhandler": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz", + "integrity": "sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==", + "requires": { + "domelementtype": "^2.0.1" + } + }, + "domutils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.1.0.tgz", + "integrity": "sha512-CD9M0Dm1iaHfQ1R/TI+z3/JWp/pgub0j4jIQKH89ARR4ATAV2nbaOQS5XxU9maJP5jHaPdDDQSEHuE2UmpUTKg==", + "requires": { + "dom-serializer": "^0.2.1", + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0" + } + }, + "htmlparser2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", + "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "domutils": "^2.0.0", + "entities": "^2.0.0" + } + } + } + }, "sass-graph": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", @@ -13176,6 +13240,11 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "srcset": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-2.0.1.tgz", + "integrity": "sha512-00kZI87TdRKwt+P8jj8UZxbfp7mK2ufxcIMWvhAOZNJTRROimpHeruWrGvCZneiuVDLqdyHefVp748ECTnyUBQ==" + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", diff --git a/package.json b/package.json index 91935aa5..c5458875 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "balancer-gov", - "version": "0.1.0", + "version": "0.1.1", "repository": "balancer-labs/balancer-gov", "license": "GPL-3.0", "scripts": { @@ -42,6 +42,8 @@ "numeral": "^2.0.4", "primer-support": "^5.0.0", "redis": "^3.0.2", + "remarkable": "^2.0.1", + "sanitize-html": "^1.27.1", "serve-static": "^1.14.1", "stylus": "^0.54.8", "stylus-loader": "^3.0.2", diff --git a/server/api.ts b/server/api.ts index ad5ac7fc..09810dac 100644 --- a/server/api.ts +++ b/server/api.ts @@ -1,20 +1,11 @@ import express from 'express'; -import { getDefaultProvider } from '@ethersproject/providers'; import redis from './redis'; import pinata from './pinata'; import relayer from './relayer'; -import { verify, jsonParse } from './utils'; -const router = express.Router(); - -let currentBlockNumber: number = 0; +import { verify, jsonParse, sendError } from './utils'; +import pkg from '../package.json'; -setInterval(async () => { - const defaultProvider = getDefaultProvider(); - const blockNumber: any = await defaultProvider.getBlockNumber(); - currentBlockNumber = parseInt(blockNumber); - await redis.setAsync('currentBlockNumber', currentBlockNumber); - console.log('Block number', currentBlockNumber); -}, 8e3) +const router = express.Router(); router.get('/:token/proposals', async (req, res) => { const { token } = req.params; @@ -41,58 +32,65 @@ router.get('/:token/proposal/:id', async (req, res) => { router.post('/message', async (req, res) => { const body = req.body; const msg = jsonParse(body.msg); + const ts = (Date.now() / 1e3).toFixed(); + const minBlock = (3600 * 24) / 15; + + if (!body || !body.address || !body.msg || !body.sig) + return sendError(res, 'wrong message body'); if ( - !currentBlockNumber || - !body || - !body.address || - !body.msg || - !body.sig || Object.keys(msg).length !== 5 || - !msg.version || !msg.token || - !msg.type || - !['proposal', 'vote'].includes(msg.type) || - Object.keys(msg.payload).length === 0 || - msg.type === 'proposal' && ( - Object.keys(msg.payload).length !== 5 || + !msg.payload || + Object.keys(msg.payload).length === 0 + ) return sendError(res, 'wrong signed message'); + + if (!msg.timestamp || typeof msg.timestamp !== 'string' || msg.timestamp > ts) + return sendError(res, 'wrong timestamp'); + + if (!msg.version || msg.version !== pkg.version) + return sendError(res, 'wrong version'); + + if (!msg.type || !['proposal', 'vote'].includes(msg.type)) + return sendError(res, 'wrong message type'); + + if (!await verify(body.address, body.msg, body.sig)) + return sendError(res, 'wrong signature'); + + if (msg.type === 'proposal') { + if ( + Object.keys(msg.payload).length !== 6 || !msg.payload.name || msg.payload.name.length > 128 || !msg.payload.body || - msg.payload.body.length > 10240 || + msg.payload.body.length > 5120 || !msg.payload.choices || msg.payload.choices.length < 2 || - !msg.payload.startBlock || - currentBlockNumber > msg.payload.startBlock || - !msg.payload.endBlock || - msg.payload.startBlock >= msg.payload.endBlock - ) || - msg.type === 'vote' && ( - Object.keys(msg.payload).length !== 2 || - !msg.payload.proposal || - !msg.payload.choice - ) - ) { - console.log('unauthorized', body); - return res.status(500).json({ error: 'unauthorized' }); - } + !msg.payload.snapshot + ) return sendError(res, 'wrong format proposal'); - if (!await verify(body.address, body.msg, body.sig)) { - console.log('wrong signature', body); - return res.status(500).json({ error: 'wrong signature' }); + if ( + !msg.payload.start || + ts > msg.payload.start || + !msg.payload.end || + msg.payload.start >= msg.payload.end + ) return sendError(res, 'wrong proposal period'); } if (msg.type === 'vote') { + if ( + Object.keys(msg.payload).length !== 2 || + !msg.payload.proposal || + !msg.payload.choice + ) return sendError(res, 'wrong format vote'); + const proposalRedis = await redis.hgetAsync(`token:${msg.token}:proposals`, msg.payload.proposal); const proposal = jsonParse(proposalRedis); if ( !proposalRedis || - currentBlockNumber > proposal.msg.payload.endBlock || - proposal.msg.payload.startBlock > currentBlockNumber - ) { - console.log('wrong vote', body); - return res.status(500).json({ error: 'wrong vote' }); - } + ts > proposal.msg.payload.end || + proposal.msg.payload.start > ts + ) return sendError(res, 'wrong vote'); } const authorIpfsRes = await pinata.pinJSONToIPFS({ diff --git a/server/utils.ts b/server/utils.ts index becfba96..c62d327b 100644 --- a/server/utils.ts +++ b/server/utils.ts @@ -16,3 +16,10 @@ export async function verify(address, msg, sig) { export function clone(item) { return JSON.parse(JSON.stringify(item)); } + +export function sendError(res, description) { + return res.status(500).json({ + error: 'unauthorized', + error_description: description + }); +} diff --git a/src/components/Block/Votes.vue b/src/components/Block/Votes.vue index abd16c66..b9255dd2 100644 --- a/src/components/Block/Votes.vue +++ b/src/components/Block/Votes.vue @@ -12,12 +12,10 @@ class="px-4 py-3 border-top d-flex" > -
- -
+
@@ -38,7 +36,7 @@ See more diff --git a/src/components/Modal/About.vue b/src/components/Modal/About.vue index 90149b2e..d7ad2979 100644 --- a/src/components/Modal/About.vue +++ b/src/components/Modal/About.vue @@ -2,13 +2,28 @@

About

+
+ + {{ pkg.version }} +
+
+ + {{ pkg.license }} +
{{ network === 'homestead' ? 'mainnet' : network }}
- {{ web3.blockNumber }} + + {{ $n(web3.blockNumber) }} + +
diff --git a/src/components/Modal/Confirm.vue b/src/components/Modal/Confirm.vue index 8fe63a2f..64a1fdb7 100644 --- a/src/components/Modal/Confirm.vue +++ b/src/components/Modal/Confirm.vue @@ -5,7 +5,7 @@

Are you sure you want to vote for this option?

This action cannot be undone.

-

+

Option {{ selectedChoice }}: {{ proposal.msg.payload.choices[selectedChoice - 1] }}

@@ -42,13 +42,16 @@ export default { }; }, methods: { - ...mapActions(['vote']), + ...mapActions(['send']), async handleSubmit() { this.loading = true; - await this.vote({ + await this.send({ token: this.token, - proposal: this.id, - choice: this.selectedChoice + type: 'vote', + payload: { + proposal: this.id, + choice: this.selectedChoice + } }); this.$emit('reload'); this.$emit('close'); diff --git a/src/components/Modal/SelectDate.vue b/src/components/Modal/SelectDate.vue new file mode 100644 index 00000000..118aa721 --- /dev/null +++ b/src/components/Modal/SelectDate.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/components/RowProposal.vue b/src/components/RowProposal.vue index 5fc707c2..f3d2d74a 100644 --- a/src/components/RowProposal.vue +++ b/src/components/RowProposal.vue @@ -1,6 +1,6 @@ diff --git a/src/components/State.vue b/src/components/State.vue index c46ce202..4bc0f2d4 100644 --- a/src/components/State.vue +++ b/src/components/State.vue @@ -14,11 +14,10 @@ export default { }, computed: { state() { - const { blockNumber } = this.web3; - const { startBlock, endBlock } = this.proposal.msg.payload; - if (blockNumber > endBlock) return { name: 'Closed', class: 'bg-purple' }; - if (blockNumber > startBlock) - return { name: 'Active', class: 'bg-green' }; + const ts = (Date.now() / 1e3).toFixed(); + const { start, end } = this.proposal.msg.payload; + if (ts > end) return { name: 'Closed', class: 'bg-purple' }; + if (ts > start) return { name: 'Active', class: 'bg-green' }; return { name: 'Pending' }; } } diff --git a/src/components/Ui/Calendar.vue b/src/components/Ui/Calendar.vue new file mode 100644 index 00000000..0e92e1b8 --- /dev/null +++ b/src/components/Ui/Calendar.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/src/components/Ui/Markdown.vue b/src/components/Ui/Markdown.vue new file mode 100644 index 00000000..06b37ac3 --- /dev/null +++ b/src/components/Ui/Markdown.vue @@ -0,0 +1,40 @@ + + + + diff --git a/src/components/User.vue b/src/components/User.vue index 719dd629..ec7ce9a7 100644 --- a/src/components/User.vue +++ b/src/components/User.vue @@ -1,6 +1,6 @@