Browse Source

Merge branch 'develop' into feature/pinner

# Conflicts:
#	packages/concordia-contracts/contracts/Forum.sol
#	packages/concordia-contracts/package.json
#	yarn.lock
develop
Apostolos Fanakis 4 years ago
parent
commit
5d584afa19
  1. 1
      .dockerignore
  2. 10
      docker/README.md
  3. 1
      packages/concordia-app/.env.development.example
  4. 12
      packages/concordia-app/package.json
  5. 0
      packages/concordia-app/patches/web3-eth+1.3.3.patch
  6. BIN
      packages/concordia-app/public/favicon.ico
  7. 1
      packages/concordia-app/public/locales/en/translation.json
  8. 5
      packages/concordia-app/src/Routes.jsx
  9. 35
      packages/concordia-app/src/assets/About.md
  10. 6
      packages/concordia-app/src/assets/css/index.css
  11. 2
      packages/concordia-app/src/assets/css/loading-component.css
  12. BIN
      packages/concordia-app/src/assets/images/app_logo.png
  13. 1
      packages/concordia-app/src/assets/images/app_logo.svg
  14. 1
      packages/concordia-app/src/assets/images/app_logo_circle.svg
  15. 2
      packages/concordia-app/src/assets/particles.js
  16. 2
      packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx
  17. 36
      packages/concordia-app/src/components/PostList/PostListRow/index.jsx
  18. 8
      packages/concordia-app/src/components/PostList/index.jsx
  19. 7
      packages/concordia-app/src/constants/configuration/defaults.js
  20. 10
      packages/concordia-app/src/constants/contracts/ContractNames.js
  21. 23
      packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/index.jsx
  22. 9
      packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/styles.css
  23. 2
      packages/concordia-app/src/layouts/RegisterLayout/styles.css
  24. 8
      packages/concordia-app/src/options/drizzleOptions.js
  25. 7
      packages/concordia-app/src/redux/sagas/orbitSaga.js
  26. 67
      packages/concordia-app/src/utils/drizzleUtils.js
  27. 43
      packages/concordia-app/src/views/About/index.jsx
  28. 2
      packages/concordia-app/src/views/Register/index.jsx
  29. 2
      packages/concordia-app/src/views/Register/styles.css
  30. 5
      packages/concordia-app/src/views/Topic/TopicView/index.jsx
  31. 8
      packages/concordia-app/src/views/Topic/index.jsx
  32. 60
      packages/concordia-contracts-provider/.eslintrc.js
  33. 24
      packages/concordia-contracts-provider/.gitignore
  34. 26
      packages/concordia-contracts-provider/package.json
  35. 11
      packages/concordia-contracts-provider/src/constants.js
  36. 31
      packages/concordia-contracts-provider/src/controllers/download.js
  37. 37
      packages/concordia-contracts-provider/src/controllers/upload.js
  38. 25
      packages/concordia-contracts-provider/src/index.js
  39. 30
      packages/concordia-contracts-provider/src/middleware/upload.js
  40. 14
      packages/concordia-contracts-provider/src/routes/web.js
  41. 17
      packages/concordia-contracts-provider/src/utils/storageUtils.js
  42. 4
      packages/concordia-contracts/.eslintrc.js
  43. 8
      packages/concordia-contracts/.solhint.json
  44. 5
      packages/concordia-contracts/constants/config/defaults.js
  45. 5
      packages/concordia-contracts/contracts/Forum.sol
  46. 10
      packages/concordia-contracts/contracts/Migrations.sol
  47. 111
      packages/concordia-contracts/contracts/PostVoting.sol
  48. 155
      packages/concordia-contracts/contracts/Voting.sol
  49. 9
      packages/concordia-contracts/index.js
  50. 8
      packages/concordia-contracts/migrations/2_deploy_contracts.js
  51. 10
      packages/concordia-contracts/package.json
  52. 112
      packages/concordia-contracts/test/TestVoting.sol
  53. 32
      packages/concordia-contracts/utils/contractsProviderUtils.js
  54. 4841
      yarn.lock

1
.dockerignore

@ -13,6 +13,7 @@ docker/
packages/*/node_modules
packages/*/dist
packages/*/coverage
packages/*/*.env*
# TO-NEVER-DO: exclude the build folder of the contracts package, it's needed for building the application image.
packages/concordia-app/build

10
docker/README.md

@ -36,7 +36,7 @@ Furthermore, we provide an image that builds the contracts and handles their mig
### Ganache
The Dockerfile is provided in the path `./ganache`. The image makes use of the environment variables described
bellow.
below.
| Environment variable | Default value | Usage |
| --- | --- | --- |
@ -66,7 +66,7 @@ This is a provision system that compiles and deploys the contracts to any Ethere
A Dockerfile is provided in the path `./concordia-contracts` that will build the contracts used by Concordia and
handle their deployment to any Ethereum network defined using env-vars upon container run. Dockerfile contains three
useful stages, described in the table bellow.
useful stages, described in the table below.
| Stage name | Entrypoint | Usage |
| --- | --- | --- |
@ -74,7 +74,7 @@ useful stages, described in the table bellow.
| test | Runs contract tests | Compiles contracts and runs tests using blockchain defined by env vars |
| runtime | Migrates contracts | Compiles contracts and migrates to the blockchain defined by env vars. Does **not** run tests |
The image makes use of the environment variables described bellow.
The image makes use of the environment variables described below.
| Environment variable | Default value | Usage |
| --- | --- | --- |
@ -92,7 +92,7 @@ the image.
### Application
The Dockerfile provided in the path `./concordia-application` builds the application for production and serves
the resulting build using an nginx server. Dockerfile contains two useful stages, described in the table bellow.
the resulting build using an nginx server. Dockerfile contains two useful stages, described in the table below.
| Stage name | Entrypoint | Usage |
| --- | --- | --- |
@ -100,7 +100,7 @@ the resulting build using an nginx server. Dockerfile contains two useful stages
| runtime | Serves application | Builds for production and serves it through nginx |
The image makes use of the environment variables described bellow.
The image makes use of the environment variables described below.
| Environment variable | Default value | Usage |
| --- | --- | --- |

1
packages/concordia-app/.env.development.example

@ -7,6 +7,5 @@ HOST=localhost
PORT=7000
# Variables needed in runtime (in browser)
# Carefull, IPFS won't accept localhost as a valid hostname
REACT_APP_RENDEZVOUS_HOST=127.0.0.1
REACT_APP_RENDEZVOUS_PORT=9090

12
packages/concordia-app/package.json

@ -3,9 +3,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"start": "cross-env REACT_APP_VERSION=$npm_package_version REACT_APP_NAME=$npm_package_name react-scripts start",
"build": "cross-env REACT_APP_VERSION=$npm_package_version REACT_APP_NAME=$npm_package_name react-scripts build",
"test": "cross-env REACT_APP_VERSION=$npm_package_version REACT_APP_NAME=$npm_package_name react-scripts test",
"eject": "react-scripts eject",
"postinstall": "patch-package",
"analyze": "react-scripts build && source-map-explorer 'build/static/js/*.js' --gzip",
@ -38,18 +38,20 @@
"react": "~16.13.1",
"react-dom": "~16.13.1",
"react-i18next": "^11.7.3",
"react-markdown": "^5.0.3",
"react-particles-js": "^3.4.0",
"react-redux": "~7.2.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "~3.4.3",
"redux-saga": "~1.1.3",
"react-timeago": "~5.2.0",
"redux-saga": "~1.1.3",
"semantic-ui-css": "~2.4.1",
"semantic-ui-react": "~1.2.1",
"web3": "1.3.0"
"web3": "~1.3.3"
},
"devDependencies": {
"cross-env": "^7.0.3",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.1.0",
"eslint-plugin-import": "^2.20.2",

0
packages/concordia-app/patches/web3-eth+1.3.0.patch → packages/concordia-app/patches/web3-eth+1.3.3.patch

BIN
packages/concordia-app/public/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

1
packages/concordia-app/public/locales/en/translation.json

@ -62,6 +62,7 @@
"register.form.sign.up.step.error.message.header": "Form contains errors",
"register.form.sign.up.step.title": "Sign Up",
"register.p.account.address": "Account address:",
"topbar.button.about": "About",
"topbar.button.clear.databases": "Clear databases",
"topbar.button.create.topic": "Create topic",
"topbar.button.profile": "Profile",

5
packages/concordia-app/src/Routes.jsx

@ -39,6 +39,11 @@ const routesConfig = [
path: '/home',
component: lazy(() => import('./views/Home')),
},
{
exact: true,
path: '/about',
component: lazy(() => import('./views/About')),
},
{
exact: true,
path: '/topics/:id(\\bnew\\b|\\d+)',

35
packages/concordia-app/src/assets/About.md

@ -0,0 +1,35 @@
# About Concordia
## What
Concordia is a forum platform (remember forums? 🤩) that focuses on user privacy and direct democratic voting. It is a
FOSS distributed via its Gitlab [repository][concordia-repository] and Docker [repository][concordia-docker-hub] under
the [MIT][concordia-license] license.
## Why
The value of privacy, freedom of speech and democracy are diminishing in modern software. Even more so in social media
platforms. Users are called to select between being the product of companies that sell their personal information and
being shut out of the modern, digital society.
Concordia, much like other projects of this kind, provides an alternative to this predicament.
## How
Concordia uses decentralized technologies, namely the Ethereum blockchain and its smart contracts, as well as the
distributed database OrbitDB that's based on the decentralized network IPFS. These technologies make Concordia
impervious to censorship and guaranty the immutability of user information and anonymity while enabling user
authentication that makes trusted, direct voting possible.
You can read more about the technological stack in Concordia's [whitepaper][concordia-whitepaper].
---
Developed by [apostolof][devs-apostolof-profile], [ezerous][devs-ezerous-profile]
[concordia-repository]: https://gitlab.com/ecentrics/apella
[concordia-docker-hub]: https://hub.docker.com/repository/docker/ecentrics/apella-app
[concordia-license]: https://gitlab.com/ecentrics/apella/-/blob/master/LICENSE.md
[devs-apostolof-profile]: https://gitlab.com/Apostolof
[devs-ezerous-profile]: https://gitlab.com/Ezerous
[concordia-whitepaper]: https://whitepaper.concordia.ecentrics.net

6
packages/concordia-app/src/assets/css/index.css

@ -1,6 +1,10 @@
body.app {
overflow: auto;
margin: 1em !important;
margin: 0;
}
div {
word-break: break-word;
}
#root {

2
packages/concordia-app/src/assets/css/loading-component.css

@ -3,7 +3,7 @@ body {
}
.loading-screen {
margin-top: 10em;
margin-top: 12em;
text-align: center;
font-size: large;
}

BIN
packages/concordia-app/src/assets/images/app_logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

1
packages/concordia-app/src/assets/images/app_logo.svg

@ -0,0 +1 @@
<svg id="e12732d2-a5d8-4802-8cce-7bb947dee9fc" data-name="Main" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 330.81 373.92"><defs><style>.ea73fbdf-f048-4e97-bc07-bb0b2969bee0,.f07aef9f-38fd-428f-bc73-47ca525dc184,.f4a4d906-3c6f-4179-bb65-49dff726c11f{fill:#ea6954;}.afa3e173-b557-4a70-97cf-07566c42bb6b,.ea73fbdf-f048-4e97-bc07-bb0b2969bee0,.eb772659-6642-46b7-9d47-de390253f8eb,.f07aef9f-38fd-428f-bc73-47ca525dc184,.f4a4d906-3c6f-4179-bb65-49dff726c11f{stroke:#ea6954;stroke-miterlimit:10;}.ea73fbdf-f048-4e97-bc07-bb0b2969bee0{stroke-width:4px;}.afa3e173-b557-4a70-97cf-07566c42bb6b,.eb772659-6642-46b7-9d47-de390253f8eb{fill:none;}.eb772659-6642-46b7-9d47-de390253f8eb{stroke-width:8px;}.afa3e173-b557-4a70-97cf-07566c42bb6b{stroke-width:5px;}.f4a4d906-3c6f-4179-bb65-49dff726c11f{stroke-width:6px;}.f07aef9f-38fd-428f-bc73-47ca525dc184{stroke-width:10px;}</style></defs><title>concordia_logo_clean_transp</title><g id="b83a01e3-c11e-43ae-b1d1-41016566a065" data-name="Thin lines"><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="6.54" y1="159.57" x2="214.18" y2="40.02"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="268.01" y1="67.02" x2="8.19" y2="217.01"/><path class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" d="M147.33,310.48" transform="translate(-84.98 -63.19)"/><path class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" d="M93.17,340.61" transform="translate(-84.98 -63.19)"/><path class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" d="M194.38,339.38" transform="translate(-84.98 -63.19)"/><path class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" d="M143.26,369.07" transform="translate(-84.98 -63.19)"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="164.08" y1="307.55" x2="218.73" y2="276.32"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="164.08" y1="307.55" x2="164.08" y2="307.55"/><path class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" d="M250.4,70.47" transform="translate(-84.98 -63.19)"/><path class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" d="M250.4,131.07" transform="translate(-84.98 -63.19)"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="214.18" y1="40.02" x2="213.85" y2="98.18"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="268.07" y1="72.66" x2="268.07" y2="128.48"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="61.62" y1="248.08" x2="60.76" y2="251.27"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="109.4" y1="276.19" x2="109.4" y2="276.19"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="60.76" y1="185.89" x2="60.76" y2="246.48"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="164.08" y1="307.55" x2="8.13" y2="216.98"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="164.39" y1="8.19" x2="164.08" y2="307.55"/></g><g id="fed0cce0-4af9-4b2d-b863-61c942ac6516" data-name="Thick lines"><polyline class="eb772659-6642-46b7-9d47-de390253f8eb" points="8.19 217.19 8.93 99.77 164.08 8.19"/><polyline class="eb772659-6642-46b7-9d47-de390253f8eb" points="322.52 98.08 268.38 130.07 164.08 69.47 61.56 128.48 61.56 186.08"/><polyline class="eb772659-6642-46b7-9d47-de390253f8eb" points="218.73 276.32 322.29 215.69 321.5 276.78 164.08 369.28 8.19 277.19"/></g><g id="fc525e57-c3b6-4537-a6f4-917e8af7caa9" data-name="Medium lines"><polyline class="afa3e173-b557-4a70-97cf-07566c42bb6b" points="164.08 8.19 164.08 68.78 214.18 40.02 214.18 98.18 268.27 67.88 268.28 130.28"/><polyline class="afa3e173-b557-4a70-97cf-07566c42bb6b" points="8.19 277.19 61.62 248.08 61.62 304.28 109.4 276.19 109.4 336.28 164.06 307.55 164.06 369.28"/></g><g id="b1c4230e-0310-46b2-a1fb-d6a53c5bc9b8" data-name="Small nodes"><circle class="f4a4d906-3c6f-4179-bb65-49dff726c11f" cx="268.28" cy="67.88" r="3.19"/><circle class="f4a4d906-3c6f-4179-bb65-49dff726c11f" cx="109.4" cy="276.19" r="3.19"/></g><g id="ef16cbf3-3f8b-4797-841e-58de76523415" data-name="Big nodes"><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="61.56" cy="186.08" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="164.08" cy="8.19" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="164.08" cy="307.55" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="8.19" cy="217.01" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="61.62" cy="248.08" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="322.62" cy="98.08" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="214.18" cy="40.02" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="218.73" cy="276.32" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="8.19" cy="277.19" r="3.19"/></g></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

1
packages/concordia-app/src/assets/images/app_logo_circle.svg

@ -0,0 +1 @@
<svg id="b37e941a-4c43-4ef7-9a51-e1598f898259" data-name="Main" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 537.4 537.4"><defs><style>.ac1237be-2158-4fc0-ae9f-071b2ff57398{fill:#0b2540;}.b45f1c22-1b20-401d-9f53-3289cddfc3c4,.be317747-31f7-43a4-9fba-03772684f044,.be531e54-2bea-4919-990d-82f88a84fa29{fill:#ea6954;}.a4966b61-2d1b-4dfd-b016-51913d3feac9,.b45f1c22-1b20-401d-9f53-3289cddfc3c4,.be317747-31f7-43a4-9fba-03772684f044,.be531e54-2bea-4919-990d-82f88a84fa29,.e5dfb83f-a67f-4542-9762-d2465cc0a2c6{stroke:#ea6954;stroke-miterlimit:10;}.b45f1c22-1b20-401d-9f53-3289cddfc3c4{stroke-width:4px;}.a4966b61-2d1b-4dfd-b016-51913d3feac9,.e5dfb83f-a67f-4542-9762-d2465cc0a2c6{fill:none;}.a4966b61-2d1b-4dfd-b016-51913d3feac9{stroke-width:8px;}.e5dfb83f-a67f-4542-9762-d2465cc0a2c6{stroke-width:5px;}.be317747-31f7-43a4-9fba-03772684f044{stroke-width:6px;}.be531e54-2bea-4919-990d-82f88a84fa29{stroke-width:10px;}</style></defs><title>concordia_logo_clean</title><g id="aa7412e7-d10f-4a17-bb5d-8cce7f8be73c" data-name="Background"><circle class="ac1237be-2158-4fc0-ae9f-071b2ff57398" cx="268.7" cy="268.7" r="268.7"/></g><g id="b4a70f8e-cb9b-4d04-a6af-3bbf6423659c" data-name="Thin lines"><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="110.22" y1="241.47" x2="317.86" y2="121.92"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="371.69" y1="148.92" x2="111.87" y2="298.91"/><path class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" d="M166,329.18"/><path class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" d="M111.87,359.31"/><path class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" d="M213.08,358.08"/><path class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" d="M162,387.77"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="267.76" y1="389.44" x2="322.41" y2="358.21"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="267.76" y1="389.44" x2="267.76" y2="389.44"/><path class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" d="M269.1,89.17"/><path class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" d="M269.1,149.77"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="317.86" y1="121.92" x2="317.53" y2="180.07"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="371.75" y1="154.56" x2="371.75" y2="210.37"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="165.3" y1="329.98" x2="164.44" y2="333.16"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="213.08" y1="358.08" x2="213.08" y2="358.08"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="164.44" y1="267.78" x2="164.44" y2="328.38"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="267.76" y1="389.44" x2="111.81" y2="298.88"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="268.07" y1="90.08" x2="267.76" y2="389.44"/></g><g id="ad2424b4-f499-4d57-ba69-57371a617f7c" data-name="Thick lines"><polyline class="a4966b61-2d1b-4dfd-b016-51913d3feac9" points="111.87 299.08 112.61 181.66 267.76 90.08"/><polyline class="a4966b61-2d1b-4dfd-b016-51913d3feac9" points="426.2 179.98 372.06 211.97 267.76 151.37 165.24 210.37 165.24 267.97"/><polyline class="a4966b61-2d1b-4dfd-b016-51913d3feac9" points="322.41 358.21 425.97 297.59 425.18 358.68 267.76 451.17 111.87 359.08"/></g><g id="b157ebcc-6b21-403c-8fdd-bc0b20aa202f" data-name="Medium lines"><polyline class="e5dfb83f-a67f-4542-9762-d2465cc0a2c6" points="267.76 90.08 267.76 150.67 317.86 121.92 317.86 180.07 371.96 149.77 371.96 212.17"/><polyline class="e5dfb83f-a67f-4542-9762-d2465cc0a2c6" points="111.87 359.08 165.3 329.98 165.3 386.17 213.08 358.08 213.08 418.17 267.74 389.44 267.74 451.17"/></g><g id="bab675c2-16d2-4e69-a23f-8e6406c1997e" data-name="Small nodes"><circle class="be317747-31f7-43a4-9fba-03772684f044" cx="371.96" cy="149.77" r="3.19"/><circle class="be317747-31f7-43a4-9fba-03772684f044" cx="213.08" cy="358.08" r="3.19"/></g><g id="b3217dd3-e6fd-4786-bb13-4cd0eac59017" data-name="Big nodes"><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="165.24" cy="267.97" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="267.76" cy="90.08" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="267.76" cy="389.44" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="111.87" cy="298.91" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="165.3" cy="329.98" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="426.3" cy="179.98" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="317.86" cy="121.92" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="322.41" cy="358.21" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="111.87" cy="359.08" r="3.19"/></g></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

2
packages/concordia-app/src/assets/particles.js

@ -21,7 +21,7 @@ const particlesOptions = {
opacity: {
anim: {
enable: true,
speed: 1.3,
speed: 0.6,
opacity_min: 0.05,
},
},

2
packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx

@ -8,7 +8,7 @@ import metamaskLogo from '../../../assets/images/metamask_logo.svg';
import ethereumLogo from '../../../assets/images/ethereum_logo.svg';
import ipfsLogo from '../../../assets/images/ipfs_logo.svg';
import orbitdbLogo from '../../../assets/images/orbitdb_logo.svg';
import appLogo from '../../../assets/images/app_logo.png';
import appLogo from '../../../assets/images/app_logo.svg';
const LoadingComponent = (props) => {
useEffect(() => function cleanup() {

36
packages/concordia-app/src/components/PostList/PostListRow/index.jsx

@ -1,8 +1,8 @@
import React, {
memo, useEffect, useMemo, useState,
memo, useEffect, useMemo, useState, useCallback,
} from 'react';
import {
Dimmer, Icon, Image, Feed, Placeholder,
Dimmer, Icon, Image, Feed, Placeholder, Ref,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
@ -22,12 +22,13 @@ const { orbit } = breeze;
const PostListRow = (props) => {
const {
id: postId, postIndexInTopic, postCallHash, loading,
id: postId, postIndex, postCallHash, loading, focus,
} = props;
const getPostResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getPost);
const [postAuthorAddress, setPostAuthorAddress] = useState(null);
const [postAuthor, setPostAuthor] = useState(null);
const [timeAgo, setTimeAgo] = useState(null);
const [topicId, setTopicId] = useState(null);
const [postContent, setPostContent] = useState(null);
const [postAuthorMeta, setPostAuthorMeta] = useState(null);
const userAddress = useSelector((state) => state.user.address);
@ -41,6 +42,7 @@ const PostListRow = (props) => {
setPostAuthorAddress(getPostResults[postCallHash].value[0]);
setPostAuthor(getPostResults[postCallHash].value[1]);
setTimeAgo(getPostResults[postCallHash].value[2] * 1000);
setTopicId(getPostResults[postCallHash].value[3]);
}
}, [getPostResults, loading, postCallHash]);
@ -116,18 +118,31 @@ const PostListRow = (props) => {
return authorAvatar;
}, [authorAvatar, postAuthorAddress]);
const focusRef = useCallback((node) => {
if (focus && node !== null) {
node.scrollIntoView({ behavior: 'smooth' });
}
}, [focus]);
return useMemo(() => (
<Dimmer.Dimmable as={Feed.Event} blurring dimmed={loading}>
<Dimmer.Dimmable
as={Feed.Event}
blurring
dimmed={loading}
id={`post-${postId}`}
>
<Ref innerRef={focusRef}>
<Feed.Label className="post-profile-picture">
{authorAvatarLink}
</Feed.Label>
</Ref>
<Feed.Content>
<Feed.Summary>
<div>
<Link to={`/topics/${topicId}/#post-${postId}`}>
<span className="post-summary-meta-index">
{t('post.list.row.post.id', { id: postIndexInTopic })}
{t('post.list.row.post.id', { id: postIndex })}
</span>
</div>
</Link>
{postAuthor !== null && setPostAuthorAddress !== null && timeAgo !== null
? (
<>
@ -147,19 +162,22 @@ const PostListRow = (props) => {
</Feed.Content>
</Dimmer.Dimmable>
), [
authorAvatarLink, loading, postAuthor, postAuthorAddress, postContent, postIndexInTopic, t, timeAgo,
authorAvatarLink, focusRef, loading, postAuthor, postAuthorAddress, postContent, postId, postIndex, t, timeAgo,
topicId,
]);
};
PostListRow.defaultProps = {
loading: false,
focus: false,
};
PostListRow.propTypes = {
id: PropTypes.number.isRequired,
postIndexInTopic: PropTypes.number.isRequired,
postIndex: PropTypes.number.isRequired,
postCallHash: PropTypes.string,
loading: PropTypes.bool,
focus: PropTypes.bool,
};
export default memo(PostListRow);

8
packages/concordia-app/src/components/PostList/index.jsx

@ -11,7 +11,7 @@ import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames';
const { contracts: { [FORUM_CONTRACT]: { methods: { getPost: { cacheCall: getPostChainData } } } } } = drizzle;
const PostList = (props) => {
const { postIds, loading } = props;
const { postIds, loading, focusOnPost } = props;
const [getPostCallHashes, setGetPostCallHashes] = useState([]);
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
@ -47,14 +47,15 @@ const PostList = (props) => {
return (
<PostListRow
id={postId}
postIndexInTopic={index + 1}
postIndex={index + 1}
key={postId}
postCallHash={postHash && postHash.hash}
loading={postHash === undefined}
focus={postId === focusOnPost}
/>
);
});
}, [getPostCallHashes, loading, postIds]);
}, [focusOnPost, getPostCallHashes, loading, postIds]);
return (
<Dimmer.Dimmable as={Feed} blurring dimmed={loading} id="post-list" size="large">
@ -67,6 +68,7 @@ const PostList = (props) => {
PostList.propTypes = {
postIds: PropTypes.arrayOf(PropTypes.number).isRequired,
loading: PropTypes.bool,
focusOnPost: PropTypes.number,
};
export default PostList;

7
packages/concordia-app/src/constants/configuration/defaults.js

@ -3,5 +3,12 @@ export const WEB3_PORT_DEFAULT = '8545';
export const WEB3_PORT_SOCKET_TIMEOUT_DEFAULT = 30000;
export const WEB3_PORT_SOCKET_CONNECT_MAX_ATTEMPTS_DEFAULT = 3;
export const REACT_APP_CONCORDIA_HOST_DEFAULT = '127.0.0.1';
export const REACT_APP_CONCORDIA_PORT_DEFAULT = '7000';
export const REACT_APP_RENDEZVOUS_HOST_DEFAULT = '127.0.0.1';
export const REACT_APP_RENDEZVOUS_PORT_DEFAULT = '9090';
export const REACT_APP_CONTRACTS_SUPPLIER_HOST_DEFAULT = '127.0.0.1';
export const REACT_APP_CONTRACTS_SUPPLIER_PORT_DEFAULT = '8400';
export const REACT_APP_CONTRACTS_VERSION_HASH_DEFAULT = 'latest';

10
packages/concordia-app/src/constants/contracts/ContractNames.js

@ -1 +1,11 @@
export const FORUM_CONTRACT = 'Forum';
export const POST_VOTING_CONTRACT = 'PostVoting';
export const VOTING_CONTRACT = 'Voting';
const CONTRACTS = [
FORUM_CONTRACT,
POST_VOTING_CONTRACT,
VOTING_CONTRACT,
];
export default CONTRACTS;

23
packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/index.jsx

@ -4,8 +4,9 @@ import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router';
import { useSelector } from 'react-redux';
import AppContext from '../../../components/AppContext';
import appLogo from '../../../assets/images/app_logo.png';
import ClearDatabasesModal from '../../../components/ClearDatabasesModal';
import appLogo from '../../../assets/images/app_logo.svg';
import './styles.css';
const MainLayoutMenu = () => {
const hasSignedUp = useSelector((state) => state.user.hasSignedUp);
@ -30,12 +31,13 @@ const MainLayoutMenu = () => {
return (
<AppContext.Consumer>
{() => (
<Menu color="black" inverted>
<Menu inverted>
<Menu.Item
id="home-button"
link
name="home"
key="home"
onClick={() => { history.push('/'); }}
onClick={() => history.push('/')}
>
<img src={appLogo} alt="app_logo" />
</Menu.Item>
@ -45,7 +47,7 @@ const MainLayoutMenu = () => {
link
name="create-topic"
key="create-topic"
onClick={() => { history.push('/topics/new'); }}
onClick={() => history.push('/topics/new')}
position="right"
>
{t('topbar.button.create.topic')}
@ -57,7 +59,7 @@ const MainLayoutMenu = () => {
link
name="profile"
key="profile"
onClick={() => { history.push('/profile'); }}
onClick={() => history.push('/profile')}
>
{t('topbar.button.profile')}
</Menu.Item>
@ -67,16 +69,23 @@ const MainLayoutMenu = () => {
link
name="register"
key="register"
onClick={() => { history.push('/auth/register'); }}
onClick={() => history.push('/auth/register')}
>
{t('topbar.button.register')}
</Menu.Item>
)}
<Menu.Item
link
name="about"
key="about"
onClick={() => history.push('/about')}
>
{t('topbar.button.about')}
</Menu.Item>
</Menu.Menu>
<Dropdown key="overflow" item direction="left">
<Dropdown.Menu>
<Dropdown.Item
link
name="clear-databases"
key="clear-databases"
onClick={handleClearDatabasesClick}

9
packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/styles.css

@ -0,0 +1,9 @@
#home-button {
padding: 1.2em;
}
.ui.inverted.menu {
background: #0B2540 !important;
border-radius: 0;
margin-bottom: 5em;
}

2
packages/concordia-app/src/layouts/RegisterLayout/styles.css

@ -4,5 +4,5 @@
bottom: 0;
left: 0;
z-index: -1;
background: rgba(0, 0, 0, 0) linear-gradient(45deg, rgb(45, 54, 76) 0%, rgb(37, 45, 63) 100%) repeat scroll 0 0;
background: rgba(0, 0, 0, 0) linear-gradient(45deg, rgb(37, 45, 63) 0%, rgb(11,37,64) 100%) repeat scroll 0 0;
}

8
packages/concordia-app/src/options/drizzleOptions.js

@ -2,13 +2,19 @@
import { contracts } from 'concordia-contracts';
import web3Options from './web3Options';
import appEvents from '../constants/contracts/events';
import downloadContractArtifactsSync from '../utils/drizzleUtils';
const drizzleOptions = {
web3: web3Options,
contracts,
events: { ...appEvents },
reloadWindowOnNetworkChange: true,
reloadWindowOnAccountChange: true, // We need it to reinitialize breeze and create new Orbit databases
};
if (process.env.REACT_APP_USE_EXTERNAL_CONTRACTS_SUPPLIER) {
drizzleOptions.contracts = downloadContractArtifactsSync();
} else {
drizzleOptions.contracts = contracts;
}
export default drizzleOptions;

7
packages/concordia-app/src/redux/sagas/orbitSaga.js

@ -5,8 +5,9 @@ import {
import { breezeActions } from '@ezerous/breeze';
import { drizzleActions } from '@ezerous/drizzle';
import { forumContract } from 'concordia-contracts';
import { contracts } from 'concordia-contracts';
import { EthereumContractIdentityProvider } from '@ezerous/eth-identity-provider';
import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames';
function* initOrbitDatabases(action) {
const { account, breeze } = action;
@ -23,7 +24,9 @@ function* orbitSaga() {
const { drizzle: { web3 } } = res[0];
const networkId = yield call([web3.eth.net, web3.eth.net.getId]);
const contractAddress = forumContract.networks[networkId].address;
const contractAddress = contracts
.find((contract) => contract.contractName === FORUM_CONTRACT)
.networks[networkId].address;
EthereumContractIdentityProvider.setContractAddress(contractAddress);
EthereumContractIdentityProvider.setWeb3(web3);

67
packages/concordia-app/src/utils/drizzleUtils.js

@ -0,0 +1,67 @@
import {
REACT_APP_CONCORDIA_HOST_DEFAULT,
REACT_APP_CONCORDIA_PORT_DEFAULT,
REACT_APP_CONTRACTS_SUPPLIER_HOST_DEFAULT,
REACT_APP_CONTRACTS_SUPPLIER_PORT_DEFAULT,
REACT_APP_CONTRACTS_VERSION_HASH_DEFAULT,
} from '../constants/configuration/defaults';
import CONTRACTS from '../constants/contracts/ContractNames';
function getContractsDownloadRequest() {
const CONTRACTS_SUPPLIER_HOST = process.env.REACT_APP_CONTRACTS_SUPPLIER_HOST
|| REACT_APP_CONTRACTS_SUPPLIER_HOST_DEFAULT;
const CONTRACTS_SUPPLIER_PORT = process.env.REACT_APP_CONTRACTS_SUPPLIER_PORT
|| REACT_APP_CONTRACTS_SUPPLIER_PORT_DEFAULT;
const CONTRACTS_VERSION_HASH = process.env.REACT_APP_CONTRACTS_VERSION_HASH
|| REACT_APP_CONTRACTS_VERSION_HASH_DEFAULT;
const HOST = process.env.REACT_APP_CONCORDIA_HOST || REACT_APP_CONCORDIA_HOST_DEFAULT;
const PORT = process.env.REACT_APP_CONCORDIA_PORT || REACT_APP_CONCORDIA_PORT_DEFAULT;
const xhrRequest = new XMLHttpRequest();
xhrRequest.open('GET',
`http://${CONTRACTS_SUPPLIER_HOST}:${CONTRACTS_SUPPLIER_PORT}/contracts/${CONTRACTS_VERSION_HASH}`,
false);
xhrRequest.setRequestHeader('Access-Control-Allow-Origin', `${HOST}:${PORT}`);
xhrRequest.setRequestHeader('Access-Control-Allow-Credentials', 'true');
return xhrRequest;
}
function validateRemoteContracts(remoteContracts) {
if (remoteContracts.length !== CONTRACTS.length) {
throw new Error(`Version mismatch detected. Artifacts brought ${remoteContracts.length} contracts but app
expected ${CONTRACTS.length}`);
}
const contractsPresentStatus = CONTRACTS.map((contract) => ({
contract,
present: remoteContracts.includes((remoteContract) => remoteContract.contractName === contract),
}));
if (contractsPresentStatus.reduce((accumulator, contract) => accumulator && contract.present, true)) {
throw new Error(`Contracts missing from artifacts. Supplier didn't bring ${contractsPresentStatus
.filter((contractPresentStatus) => contractPresentStatus.present === false)
.map((contractPresentStatus) => contractPresentStatus.contract)
.join(', ')}.`);
}
}
const downloadContractArtifactsSync = () => {
const xhrRequest = getContractsDownloadRequest();
xhrRequest.send(null);
if (xhrRequest.status === 200) {
const contractsRawData = xhrRequest.responseText;
const remoteContracts = JSON.parse(contractsRawData);
validateRemoteContracts(remoteContracts);
return remoteContracts;
}
throw new Error(`Remote contract artifacts download request failed!\n${xhrRequest.responseText}`);
};
export default downloadContractArtifactsSync;

43
packages/concordia-app/src/views/About/index.jsx

@ -0,0 +1,43 @@
import React, {
memo, useEffect, useState,
} from 'react';
import ReactMarkdown from 'react-markdown';
import { Container, Image } from 'semantic-ui-react';
import AboutMd from '../../assets/About.md';
import appLogo from '../../assets/images/app_logo_circle.svg';
const targetBlank = () => ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
);
const About = () => {
const [aboutMd, setAboutMd] = useState('');
useEffect(() => {
fetch(AboutMd)
.then((response) => response.text())
.then((text) => {
setAboutMd(text);
});
}, []);
return (
<Container id="about-container">
<div style={{ textAlign: 'center' }}>
<Image src={appLogo} size="small" centered />
{`v${process.env.REACT_APP_VERSION}`}
</div>
<ReactMarkdown
source={aboutMd}
renderers={{
link: targetBlank(),
linkReference: targetBlank(),
}}
/>
</Container>
);
};
export default memo(About);

2
packages/concordia-app/src/views/Register/index.jsx

@ -46,7 +46,7 @@ const Register = () => {
}, [currentStep, pushNextStep]);
return (
<div className="centered form-card-container">
<div className="register-centered form-card-container">
<Card fluid>
<Card.Content>
{

2
packages/concordia-app/src/views/Register/styles.css

@ -1,4 +1,4 @@
.centered {
.register-centered {
position: fixed;
top: 50%;
left: 50%;

5
packages/concordia-app/src/views/Topic/TopicView/index.jsx

@ -24,7 +24,7 @@ const { orbit } = breeze;
const TopicView = (props) => {
const {
topicId, topicAuthorAddress: initialTopicAuthorAddress, topicAuthor: initialTopicAuthor,
timestamp: initialTimestamp, postIds: initialPostIds,
timestamp: initialTimestamp, postIds: initialPostIds, focusOnPost,
} = props;
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
@ -174,7 +174,7 @@ const TopicView = (props) => {
</Step>
</Step.Group>
</Dimmer.Dimmable>
<PostList postIds={postIds || []} loading={postIds === null} />
<PostList postIds={postIds || []} loading={postIds === null} focusOnPost={focusOnPost} />
{topicSubject !== null && postIds !== null && hasSignedUp && (
<PostCreate
topicId={topicId}
@ -192,6 +192,7 @@ TopicView.propTypes = {
topicAuthor: PropTypes.string,
timestamp: PropTypes.number,
postIds: PropTypes.arrayOf(PropTypes.number),
focusOnPost: PropTypes.number,
};
export default TopicView;

8
packages/concordia-app/src/views/Topic/index.jsx

@ -1,18 +1,22 @@
import React from 'react';
import { useRouteMatch } from 'react-router';
import { useLocation, useRouteMatch } from 'react-router';
import TopicCreate from './TopicCreate';
import TopicView from './TopicView';
const Topic = () => {
const match = useRouteMatch();
const { id: topicId } = match.params;
const location = useLocation();
const postHash = location.hash;
const postId = postHash ? postHash.substring('#post-'.length) : null;
const focusPostId = postId ? parseInt(postId, 10) : null;
return topicId === 'new'
? (
<TopicCreate />
)
: (
<TopicView topicId={parseInt(topicId, 10)} />
<TopicView topicId={parseInt(topicId, 10)} focusOnPost={focusPostId} />
);
};

60
packages/concordia-contracts-provider/.eslintrc.js

@ -0,0 +1,60 @@
module.exports = {
env: {
browser: true,
es6: true,
jest: true,
},
extends: [
'plugin:react/recommended',
'airbnb',
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
},
parser: 'babel-eslint',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: [
'react',
'react-hooks',
],
rules: {
'react/jsx-props-no-spreading': 'off',
'import/extensions': 'off',
'react/jsx-indent': [
'error',
4,
{
checkAttributes: true,
indentLogicalExpressions: true,
},
],
'react/require-default-props': 'off',
'react/prop-types': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
'max-len': ['warn', { code: 120, tabWidth: 4 }],
'no-unused-vars': 'warn',
'no-console': 'warn',
'no-shadow': 'warn',
'no-multi-str': 'warn',
'jsx-a11y/label-has-associated-control': [2, {
labelAttributes: ['label'],
controlComponents: ['Input'],
depth: 3,
}],
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx'],
},
},
},
};

24
packages/concordia-contracts-provider/.gitignore

@ -0,0 +1,24 @@
# Node
/node_modules
# IDE
.DS_Store
.idea
# Build Directories
/build
/src/build
# Logs
/log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Misc
.env.local
.env.development.local
.env.test.local
.env.production.local
contracts-uploads

26
packages/concordia-contracts-provider/package.json

@ -0,0 +1,26 @@
{
"name": "concordia-contracts-provider",
"description": "A server that provides built contracts for Concordia.",
"version": "0.1.0",
"private": true,
"main": "src/index.js",
"scripts": {
"start": "node -r esm src/index.js"
},
"license": "MIT",
"dependencies": {
"cors": "^2.8.5",
"esm": "~3.2.25",
"express": "^4.17.1",
"lodash": "^4.17.20",
"multer": "^1.4.2",
"multiparty": "^4.2.2"
},
"devDependencies": {
"eslint": "^7.19.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0"
}
}

11
packages/concordia-contracts-provider/src/constants.js

@ -0,0 +1,11 @@
import path from 'path';
const PROVIDER_PORT = '8400';
const UPLOAD_CONTRACTS_DIRECTORY = path.join(__dirname, '..', 'contracts-uploads');
const CORS_ALLOWED_ORIGINS = ['http://127.0.0.1:7000', 'http://localhost:7000'];
export default {
port: PROVIDER_PORT,
uploadsDirectory: UPLOAD_CONTRACTS_DIRECTORY,
corsAllowedOrigins: CORS_ALLOWED_ORIGINS,
};

31
packages/concordia-contracts-provider/src/controllers/download.js

@ -0,0 +1,31 @@
import * as fs from 'fs';
import path from 'path';
import { getStorageLocation, getTagsDirectory } from '../utils/storageUtils';
const downloadContracts = async (req, res) => {
const { params: { hash: hashOrTag } } = req;
let directoryPath = getStorageLocation(hashOrTag);
if (!fs.existsSync(directoryPath)) {
const tagsDirectory = getTagsDirectory();
if (fs.existsSync(tagsDirectory)) {
const tagFilePath = path.join(tagsDirectory, hashOrTag);
const tagReference = fs.readFileSync(tagFilePath, 'utf-8');
directoryPath = getStorageLocation(tagReference);
}
}
const contracts = [];
fs.readdirSync(directoryPath).forEach((contractFilename) => {
const rawContractData = fs.readFileSync(path.join(`${directoryPath}/${contractFilename}`), 'utf-8');
const contractJson = JSON.parse(rawContractData);
contracts.push(contractJson);
});
res.send(contracts);
};
export default downloadContracts;

37
packages/concordia-contracts-provider/src/controllers/upload.js

@ -0,0 +1,37 @@
import path from 'path';
import fs from 'fs';
import upload from '../middleware/upload';
import { getTagsDirectory } from '../utils/storageUtils';
const addOrTransferTag = (tag, hash) => {
const tagsDirectory = getTagsDirectory();
const tagFilePath = path.join(tagsDirectory, tag);
fs.mkdirSync(tagsDirectory, { recursive: true });
fs.writeFileSync(tagFilePath, hash);
};
const uploadContracts = async (req, res) => {
try {
await upload(req, res);
const { body: { tag } } = req;
const { params: { hash } } = req;
if (tag) {
addOrTransferTag(tag, hash);
}
if (req.files.length <= 0) {
return res.send('You must select at least 1 file.');
}
return res.send('Files have been uploaded.');
} catch (error) {
console.log(error);
return res.send(`Error when trying upload many files: ${error}`);
}
};
export default uploadContracts;

25
packages/concordia-contracts-provider/src/index.js

@ -0,0 +1,25 @@
import express from 'express';
import cors from 'cors';
import initRoutes from './routes/web';
import constants from './constants';
const PROVIDER_PORT = process.env.CONTRACTS_PROVIDER_PORT || constants.port;
const ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS
? process.env.CORS_ALLOWED_ORIGINS.split(';')
: constants.corsAllowedOrigins;
const app = express();
const corsOptions = {
origin: ALLOWED_ORIGINS,
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
};
app.use(express.urlencoded({ extended: true }));
app.use(cors(corsOptions));
initRoutes(app);
app.listen(PROVIDER_PORT, () => {
console.log(`Contracts provider listening at http://127.0.0.1:${PROVIDER_PORT}`);
});

30
packages/concordia-contracts-provider/src/middleware/upload.js

@ -0,0 +1,30 @@
import * as util from 'util';
import * as fs from 'fs';
import multer from 'multer';
import { getStorageLocation } from '../utils/storageUtils';
const storage = multer.diskStorage({
destination: (req, file, callback) => {
const { params: { hash } } = req;
const contractsPath = getStorageLocation(hash);
fs.mkdirSync(contractsPath, { recursive: true });
callback(null, contractsPath);
},
filename: (req, file, callback) => {
const match = ['application/json'];
if (match.indexOf(file.mimetype) === -1) {
const message = `<strong>${file.originalname}</strong> is invalid. Only JSON files are accepted.`;
return callback(message, null);
}
const filename = `${file.originalname}`;
callback(null, filename);
},
});
const uploadFiles = multer({ storage }).array('contracts');
const uploadFilesMiddleware = util.promisify(uploadFiles);
export default uploadFilesMiddleware;

14
packages/concordia-contracts-provider/src/routes/web.js

@ -0,0 +1,14 @@
import express from 'express';
import downloadContracts from '../controllers/download';
import uploadContracts from '../controllers/upload';
const router = express.Router();
const routes = (app) => {
router.get('/contracts/:hash', downloadContracts);
router.post('/contracts/:hash', uploadContracts);
return app.use('/', router);
};
export default routes;

17
packages/concordia-contracts-provider/src/utils/storageUtils.js

@ -0,0 +1,17 @@
import path from 'path';
import constants from '../constants';
export const getStorageLocation = (hash) => {
const UPLOADS_DIRECTORY = process.env.UPLOAD_CONTRACTS_DIRECTORY || constants.uploadsDirectory;
if (hash) {
return path.join(UPLOADS_DIRECTORY, hash);
}
return UPLOADS_DIRECTORY;
};
export const getTagsDirectory = () => {
const uploadsPath = getStorageLocation();
return path.join(uploadsPath, '/tags');
};

4
packages/concordia-contracts/.eslintrc.js

@ -31,7 +31,9 @@ module.exports = {
'no-unused-vars': 'warn',
'no-console': 'warn',
'no-shadow': 'warn',
"no-multi-str": "warn"
'no-multi-str': 'warn',
'one-var': ["error", { "uninitialized": "always" }],
'one-var-declaration-per-line': ['error', 'initializations']
},
'settings': {
'import/resolver': {

8
packages/concordia-contracts/.solhint.json

@ -1,3 +1,9 @@
{
"extends": "solhint:default"
"extends": "solhint:recommended",
"rules": {
"compiler-version": ["error","~0.8.0"],
"func-visibility": ["warn",{"ignoreConstructors" : true}],
"not-rely-on-time": "off",
"state-visibility": "off"
}
}

5
packages/concordia-contracts/constants/config/defaults.js

@ -4,6 +4,9 @@ const DEVELOP_CHAIN_PORT_DEFAULT = '8545';
const TEST_CHAIN_HOST_DEFAULT = '127.0.0.1';
const TEST_CHAIN_PORT_DEFAULT = '8546';
const CONTRACTS_PROVIDER_HOST_DEFAULT = '127.0.0.1';
const CONTRACTS_PROVIDER_PORT_DEFAULT = '8400';
module.exports = {
develop: {
chainHost: DEVELOP_CHAIN_HOST_DEFAULT,
@ -13,4 +16,6 @@ module.exports = {
chainHost: TEST_CHAIN_HOST_DEFAULT,
chainPort: TEST_CHAIN_PORT_DEFAULT,
},
contractsProviderHost: CONTRACTS_PROVIDER_HOST_DEFAULT,
contractsProviderPort: CONTRACTS_PROVIDER_PORT_DEFAULT,
};

5
packages/concordia-contracts/contracts/Forum.sol

@ -177,4 +177,9 @@ contract Forum {
posts[postID].topicID
);
}
function getPostAuthor(uint postID) public view returns (address) {
require(postExists(postID), POST_DOES_NOT_EXIST);
return posts[postID].author;
}
}

10
packages/concordia-contracts/contracts/Migrations.sol

@ -3,7 +3,7 @@ pragma solidity 0.8.0;
contract Migrations {
address public owner;
uint public last_completed_migration;
uint public lastCompletedMigration;
constructor() {
owner = msg.sender;
@ -14,11 +14,11 @@ contract Migrations {
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
lastCompletedMigration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
function upgrade(address newAddress) public restricted {
Migrations upgraded = Migrations(newAddress);
upgraded.setCompleted(lastCompletedMigration);
}
}

111
packages/concordia-contracts/contracts/PostVoting.sol

@ -0,0 +1,111 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
import "./Forum.sol";
contract PostVoting {
Forum public forum;
constructor(Forum addr) {
forum = Forum(addr);
}
enum Option { DEFAULT, UP, DOWN } // DEFAULT -> 0, UP -> 1, DOWN -> 2
struct PostBallot {
mapping(address => Option) votes;
mapping(Option => address[]) voters;
}
mapping(uint => PostBallot) postBallots;
event UserVotedPost(address userAddress, uint postID, Option option);
function getVote(uint postID, address voter) public view returns (Option) {
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST());
return postBallots[postID].votes[voter];
}
// Gets vote count for a specific option (Option.UP/ Option.DOWN only!)
function getVoteCount(uint postID, Option option) private view returns (uint) {
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST());
return (postBallots[postID].voters[option].length);
}
function getUpvoteCount(uint postID) public view returns (uint) {
return (getVoteCount(postID, Option.UP));
}
function getDownvoteCount(uint postID) public view returns (uint) {
return (getVoteCount(postID, Option.DOWN));
}
// Gets voters for a specific option (Option.UP/ Option.DOWN)
function getVoters(uint postID, Option option) private view returns (address[] memory) {
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST());
return (postBallots[postID].voters[option]);
}
function getUpvoters(uint postID) public view returns (address[] memory) {
return (getVoters(postID, Option.UP));
}
function getDownvoters(uint postID) public view returns (address[] memory) {
return (getVoters(postID, Option.DOWN));
}
function getVoterIndex(uint postID, address voter) private view returns (uint) {
require(forum.hasUserSignedUp(voter), forum.USER_HAS_NOT_SIGNED_UP());
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST());
PostBallot storage postBallot = postBallots[postID];
Option votedOption = getVote(postID, voter);
address[] storage optionVoters = postBallot.voters[votedOption];
for (uint voterIndex = 0; voterIndex < optionVoters.length; voterIndex++)
if (optionVoters[voterIndex] == voter)
return voterIndex;
revert("Couldn't find voter's index!");
}
function vote(uint postID, Option option) private {
address voter = msg.sender;
require(forum.hasUserSignedUp(voter), forum.USER_HAS_NOT_SIGNED_UP());
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST());
address postAuthor = forum.getPostAuthor(postID);
require(voter != postAuthor, "Post's author cannot vote for it.");
PostBallot storage postBallot = postBallots[postID];
Option prevOption = postBallot.votes[voter];
if (prevOption == option)
return;
// Remove previous vote if exists
if (prevOption != Option.DEFAULT) {
uint voterIndex = getVoterIndex(postID, voter);
// Swap with last voter address and delete vote
postBallot.voters[prevOption][voterIndex] = postBallot.voters[prevOption][postBallot.voters[prevOption].length - 1];
postBallot.voters[prevOption].pop();
}
// Add new vote
if (option != Option.DEFAULT)
postBallot.voters[option].push(voter);
postBallot.votes[voter] = option;
emit UserVotedPost(voter, postID, option);
}
function upvote(uint postID) public {
vote(postID, Option.UP);
}
function downvote(uint postID) public {
vote(postID, Option.DOWN);
}
function unvote(uint postID) public {
vote(postID, Option.DEFAULT);
}
}

155
packages/concordia-contracts/contracts/Voting.sol

@ -0,0 +1,155 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
import "./Forum.sol";
contract Voting {
// Error messages for require()
string constant TOPIC_POLL_DIFFERENT_CREATOR = "Only topic's author can create a poll.";
string constant POLL_EXISTS = "Poll already exists.";
string constant POLL_DOES_NOT_EXIST = "Poll does not exist.";
string constant INVALID_OPTION = "Invalid option.";
string constant USER_HAS_NOT_VOTED = "User hasn't voted.";
Forum public forum;
constructor(Forum addr) {
forum = Forum(addr);
}
struct Poll {
uint topicID;
uint numOptions;
string dataHash;
mapping(address => uint) votes;
mapping(uint => address[]) voters;
bool enableVoteChanges;
uint timestamp;
}
mapping(uint => Poll) polls;
event PollCreated(uint topicID);
event UserVotedPoll(address userAddress, uint topicID, uint vote);
function pollExists(uint topicID) public view returns (bool) {
if (polls[topicID].timestamp != 0)
return true;
return false;
}
function createPoll(uint topicID, uint numOptions, string memory dataHash, bool enableVoteChanges) public returns (uint) {
require(forum.hasUserSignedUp(msg.sender), forum.USER_HAS_NOT_SIGNED_UP());
require(forum.topicExists(topicID), forum.TOPIC_DOES_NOT_EXIST());
require(forum.getTopicAuthor(topicID) == msg.sender, TOPIC_POLL_DIFFERENT_CREATOR);
require(!pollExists(topicID), POLL_EXISTS);
Poll storage poll = polls[topicID];
poll.topicID = topicID;
poll.numOptions = numOptions;
poll.dataHash = dataHash;
poll.enableVoteChanges = enableVoteChanges;
poll.timestamp = block.timestamp;
emit PollCreated(topicID);
return topicID;
}
function getPollInfo(uint topicID) public view returns (uint, string memory, uint, uint) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
uint totalVotes = getTotalVotes(topicID);
return (
polls[topicID].numOptions,
polls[topicID].dataHash,
polls[topicID].timestamp,
totalVotes
);
}
function isOptionValid(uint topicID, uint option) public view returns (bool) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
if (option <= polls[topicID].numOptions) // Option 0 is valid as well (no option chosen)
return true;
return false;
}
function hasVoted(uint topicID, address voter) public view returns (bool) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
if (polls[topicID].votes[voter] != 0)
return true;
return false;
}
function getVote(uint topicID, address voter) public view returns (uint) {
require(hasVoted(topicID, voter), USER_HAS_NOT_VOTED);
return polls[topicID].votes[voter];
}
function getVoteCount(uint topicID, uint option) public view returns (uint) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
require(isOptionValid(topicID, option), INVALID_OPTION);
return (polls[topicID].voters[option].length);
}
function getTotalVotes(uint topicID) public view returns (uint) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
Poll storage poll = polls[topicID];
uint totalVotes = 0;
for (uint pollOption = 1; pollOption <= poll.numOptions; pollOption++)
totalVotes += poll.voters[pollOption].length;
return totalVotes;
}
// Gets voters for a specific option
function getVoters(uint topicID, uint option) public view returns (address[] memory) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
return (polls[topicID].voters[option]);
}
function getVoterIndex(uint topicID, address voter) public view returns (uint) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
require(hasVoted(topicID, voter), USER_HAS_NOT_VOTED);
Poll storage poll = polls[topicID];
uint votedOption = getVote(topicID, voter);
address[] storage optionVoters = poll.voters[votedOption];
for (uint voterIndex = 0; voterIndex < optionVoters.length; voterIndex++)
if (optionVoters[voterIndex] == voter)
return voterIndex;
revert("Couldn't find voter's index!");
}
function vote(uint topicID, uint option) public {
require(forum.hasUserSignedUp(msg.sender), forum.USER_HAS_NOT_SIGNED_UP());
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
require(isOptionValid(topicID, option), INVALID_OPTION);
Poll storage poll = polls[topicID];
address voter = msg.sender;
uint prevOption = poll.votes[voter];
if (prevOption == option)
return;
// Voter hasn't voted before
if (prevOption == 0) {
poll.voters[option].push(voter);
poll.votes[voter] = option;
emit UserVotedPoll(voter, topicID, option);
}
else if (poll.enableVoteChanges) {
uint voterIndex = getVoterIndex(topicID, voter);
// Swap with last voter address and delete vote
poll.voters[prevOption][voterIndex] = poll.voters[prevOption][poll.voters[prevOption].length - 1];
poll.voters[prevOption].pop();
if (option != 0)
poll.voters[option].push(voter);
poll.votes[voter] = option;
emit UserVotedPoll(voter, topicID, option);
}
}
}

9
packages/concordia-contracts/index.js

@ -1,14 +1,15 @@
let Forum;
let Forum, Voting, PostVoting;
/* eslint-disable global-require */
try {
// eslint-disable-next-line global-require
Forum = require('./build/Forum.json');
Voting = require('./build/Voting.json');
PostVoting = require('./build/PostVoting.json');
} catch (e) {
// eslint-disable-next-line no-console
console.error("Could not require contract artifacts. Haven't you run compile yet?");
}
module.exports = {
contracts: [Forum],
forumContract: Forum,
contracts: [Forum, Voting, PostVoting],
};

8
packages/concordia-contracts/migrations/2_deploy_contracts.js

@ -1,6 +1,12 @@
const Forum = artifacts.require('Forum');
const Voting = artifacts.require('Voting');
const PostVoting = artifacts.require('PostVoting');
// eslint-disable-next-line func-names
module.exports = function (deployer) {
deployer.deploy(Forum);
return deployer.deploy(Forum)
.then(async (forum) => Promise.all([
deployer.deploy(Voting, forum.address),
deployer.deploy(PostVoting, forum.address),
]));
};

10
packages/concordia-contracts/package.json

@ -10,13 +10,15 @@
"_eslint": "yarn eslint . --format table",
"_solhint": "yarn solhint --formatter table contracts/*.sol test/*.sol",
"test": "yarn truffle test",
"migrate": "yarn _migrate --network develop",
"migrate-reset": "yarn _migrate --network develop --reset",
"_migrate": "yarn truffle migrate"
"migrate": "yarn _migrate --network develop && yarn upload",
"migrate-reset": "yarn _migrate --network develop --reset && yarn upload",
"_migrate": "yarn truffle migrate",
"upload": "node ./utils/contractsProviderUtils.js ${npm_package_version}-dev latest"
},
"dependencies": {
"@openzeppelin/contracts": "~3.3.0",
"truffle": "~5.1.58"
"truffle": "~5.1.58",
"unirest": "^0.6.0"
},
"devDependencies": {
"eslint": "^6.8.0",

112
packages/concordia-contracts/test/TestVoting.sol

@ -0,0 +1,112 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Forum.sol";
import "../contracts/Voting.sol";
contract TestVoting {
Forum forum;
uint firstTopicId;
function beforeAll() public {
forum = Forum(DeployedAddresses.Forum());
forum.signUp("testAccount");
(firstTopicId,) = forum.createTopic();
}
function testIfPollExists() public {
Voting voting = Voting(DeployedAddresses.Voting());
bool actual = voting.pollExists(firstTopicId);
Assert.equal(actual, false, "Poll should not exist");
}
function testCreatePoll() public {
Voting voting = Voting(DeployedAddresses.Voting());
uint actual = voting.createPoll(firstTopicId, 3, "asdf", false);
Assert.equal(actual, firstTopicId, "Topic Id should be 1");
}
function testGetTotalVotes() public {
Voting voting = Voting(DeployedAddresses.Voting());
uint actual = voting.getTotalVotes(firstTopicId);
Assert.equal(actual, 0, "Topic Id should be 0");
}
function testGetPollInfo() public {
Voting voting = Voting(DeployedAddresses.Voting());
(uint actualNumberOfOptions, string memory actualDataHash, , uint actualNumberOfVotes) = voting.getPollInfo(firstTopicId);
Assert.equal(actualNumberOfOptions, 3, "Number of votes should be 0");
Assert.equal(actualDataHash, "asdf", "Number of votes should be 0");
Assert.equal(actualNumberOfVotes, 0, "Number of votes should be 0");
}
function testVote() public {
Voting voting = Voting(DeployedAddresses.Voting());
voting.vote(firstTopicId, 1);
uint votesActual = voting.getTotalVotes(firstTopicId);
Assert.equal(votesActual, 1, "Number of votes should be 1");
}
function testGetVoteCount() public {
Voting voting = Voting(DeployedAddresses.Voting());
uint actualVotesOption0 = voting.getVoteCount(firstTopicId, 1);
uint actualVotesOption1 = voting.getVoteCount(firstTopicId, 2);
uint actualVotesOption2 = voting.getVoteCount(firstTopicId, 3);
Assert.equal(actualVotesOption0, 1, "Vote count is not correct");
Assert.equal(actualVotesOption1, 0, "Vote count is not correct");
Assert.equal(actualVotesOption2, 0, "Vote count is not correct");
}
function testChangeVoteWhenDisabled() public {
Voting voting = Voting(DeployedAddresses.Voting());
(uint topicId,) = forum.createTopic();
voting.createPoll(topicId, 3, "asdf", false);
voting.vote(topicId, 1);
uint actualVotesOption0 = voting.getVoteCount(topicId, 1);
uint actualVotesOption1 = voting.getVoteCount(topicId, 2);
voting.vote(topicId, 2);
uint actualVotesOption2 = voting.getVoteCount(topicId, 1);
uint actualVotesOption3 = voting.getVoteCount(topicId, 2);
Assert.equal(actualVotesOption0, 1, "Number of votes should be 1");
Assert.equal(actualVotesOption1, 0, "Number of votes should be 0");
Assert.equal(actualVotesOption2, 1, "Number of votes should be 1");
Assert.equal(actualVotesOption3, 0, "Number of votes should be 0");
}
function testChangeVoteWhenEnabled() public {
Voting voting = Voting(DeployedAddresses.Voting());
(uint topicId,) = forum.createTopic();
voting.createPoll(topicId, 3, "asdf", true);
voting.vote(topicId, 1);
uint actualVotesOption0 = voting.getVoteCount(topicId, 1);
uint actualVotesOption1 = voting.getVoteCount(topicId, 2);
voting.vote(topicId, 2);
uint actualVotesOption2 = voting.getVoteCount(topicId, 1);
uint actualVotesOption3 = voting.getVoteCount(topicId, 2);
Assert.equal(actualVotesOption0, 1, "Number of votes should be 1");
Assert.equal(actualVotesOption1, 0, "Number of votes should be 0");
Assert.equal(actualVotesOption2, 0, "Number of votes should be 0");
Assert.equal(actualVotesOption3, 1, "Number of votes should be 1");
}
}

32
packages/concordia-contracts/utils/contractsProviderUtils.js

@ -0,0 +1,32 @@
const path = require('path');
const unirest = require('unirest');
const { contracts } = require('../index');
const defaults = require('../constants/config/defaults');
const uploadContractsToProviderUnirest = (versionHash, tag) => {
const CONTRACTS_PROVIDER_HOST = process.env.CONTRACTS_PROVIDER_HOST || defaults.contractsProviderHost;
const CONTRACTS_PROVIDER_PORT = process.env.CONTRACTS_PROVIDER_PORT || defaults.contractsProviderPort;
const uploadPath = `http://${CONTRACTS_PROVIDER_HOST}:${CONTRACTS_PROVIDER_PORT}/contracts/${versionHash}`;
const request = unirest('POST', uploadPath)
.field('tag', tag);
contracts
.forEach((contract) => request
.attach('contracts', path.join(__dirname, '../', 'build/', `${contract.contractName}.json`)));
console.log(`Uploading to ${uploadPath}`);
request.end((res) => {
if (res.error) {
throw new Error(`Failed to upload contracts to provider: ${res.error}`);
}
console.log('Contracts uploaded to provider.');
});
};
const main = () => {
uploadContractsToProviderUnirest(process.argv[2], process.argv[3]);
};
main();

4841
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save