├── .babelrc
├── .browserslistrc
├── .commitlintrc.json
├── .env
├── .env.development
├── .env.production
├── .eslintrc.json
├── .github
├── dependabot.yml
└── workflows
│ └── sentry-release.yml
├── .gitignore
├── .husky
├── .gitignore
├── commit-msg
└── pre-commit
├── .lintstagedrc.json
├── .prettierignore
├── .prettierrc.json
├── .stylelintrc.json
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── SECURITY.md
├── config-overrides.js
├── netlify.toml
├── package.json
├── public
├── favicon.ico
├── icon-192.png
├── icon-512.png
├── index.html
├── manifest.json
├── stake-kleros-logo.png
└── sw.js
├── scripts
└── sentry-release.sh
├── src
├── adapters
│ └── antd
│ │ ├── button.js
│ │ └── index.js
├── api
│ └── side-chain
│ │ ├── chain-params.js
│ │ ├── create-side-chain-api.js
│ │ ├── create-watch-token.js
│ │ ├── index.js
│ │ ├── react-adapters.js
│ │ ├── request-switch-chain.js
│ │ └── xdai-api.js
├── assets
│ ├── contracts
│ │ ├── kleros-liquid-extra-views.json
│ │ ├── kleros-liquid.json
│ │ ├── kleros.json
│ │ ├── pinakion.json
│ │ ├── policy-registry.json
│ │ ├── token-bridge-ethereum.json
│ │ ├── token-bridge-xdai.json
│ │ ├── uniswap-v2-factory.json
│ │ ├── uniswap-v2-router-02.json
│ │ ├── wrapped-pinakion.json
│ │ └── x-pinakion.json
│ ├── images
│ │ ├── OneInch.svg
│ │ ├── acropolis.svg
│ │ ├── alert.svg
│ │ ├── arrow-down.svg
│ │ ├── arrow-up.svg
│ │ ├── balancer.svg
│ │ ├── bell.svg
│ │ ├── bitfinex.svg
│ │ ├── breadcrumb.svg
│ │ ├── close.svg
│ │ ├── dark-logo.png
│ │ ├── deversifi.png
│ │ ├── document.svg
│ │ ├── etherscan-logo.png
│ │ ├── folder.svg
│ │ ├── gateio.svg
│ │ ├── gavel.svg
│ │ ├── ghost.svg
│ │ ├── github.svg
│ │ ├── guardarian.svg
│ │ ├── hexagon.svg
│ │ ├── hourglass.svg
│ │ ├── idex.svg
│ │ ├── image.svg
│ │ ├── info.png
│ │ ├── info.svg
│ │ ├── kleros-logo-flat-light.svg
│ │ ├── kleros.svg
│ │ ├── kyber.png
│ │ ├── light-purple-arrow.svg
│ │ ├── link.svg
│ │ ├── linkedin.svg
│ │ ├── logo.svg
│ │ ├── loopring.svg
│ │ ├── mail.svg
│ │ ├── okex.svg
│ │ ├── omnibridge.png
│ │ ├── paraswap.jpg
│ │ ├── pdf.svg
│ │ ├── present.svg
│ │ ├── purple-arrow.svg
│ │ ├── question-circle.svg
│ │ ├── reward.png
│ │ ├── reward.svg
│ │ ├── right-arrow.svg
│ │ ├── scales.svg
│ │ ├── section-arrow-background.svg
│ │ ├── section-arrow.svg
│ │ ├── skills.png
│ │ ├── skills.svg
│ │ ├── spinner.svg
│ │ ├── stPNK.png
│ │ ├── stake-kleros-logo.png
│ │ ├── sushiswap.png
│ │ ├── swapr.svg
│ │ ├── telegram.svg
│ │ ├── transak.png
│ │ ├── underline.svg
│ │ ├── uniswap.svg
│ │ ├── video.svg
│ │ ├── x.svg
│ │ └── xPNK.png
│ └── policies
│ │ ├── 9ffaRetGoVpJqrJi3wrqvEhADpPT1yZaKS7azcxEq1X7MJWGbCMBzjGNbcKeCnkneHvyBwmsfwF7QZAuuQLrh3dqc4
│ │ ├── 9ffdfp3Hm9CsVpYA9FAxAEVV9iitYy9GRs7XfrMuBPNqF9dKsXPxUYNebDnQ6Z32Ycw7ZyjQ3Tp8KYSMzjg2Uskxgm
│ │ ├── 9fferP2QNWuLxkaR79eUS7cFPbcmS3gGsog3ibWLAAyGTgfUrEYeDZSwkcGZvkTMFRpqvW2bxKDfNL2jHCnknrjGdW
│ │ ├── 9ffjVwGsqEWJ5uFbkuU7PgVARrRDhGoJAJp6FCeTkBD1juQChg9BvB6CbfQrrNsJKZJXsbvDGvmiN7Nb2piafdBMPW
│ │ ├── Bccx3Zffpi6gfK38PpVQitC3emyRinSf6BE9PNcnyLfMPjVDZ1aChk7zPYYnR9RRcwJyLhko4ZxKZWVfCyaGTt3iZq
│ │ ├── Bcd1WbSfBY5yYwnTrS5ddGemKuGt6WUyE2LptK1kBR4WhXudrM6Erd651VeGvtwmQaNmNC1C7JPVVy1bVomD8dn64i
│ │ ├── Bcd1X53whSqZorjim2tSVnG4Bg4Mui3wcUFzwSKFKUczKHsZiVaVEfXdnfos9XYJ5hJgWTUmHCGWkx2Da2xVxAAaqB
│ │ ├── Bcd3cW1wGYuFZ52QBSLC9CVujQedXj1oY7Yv2KCCnaukt7QEFbWwu54t1Je1mJ95Ui2oa4WaYZUsgJiUhEEyHdbdzm
│ │ ├── Bcd8V9FLHVc9F3nFCR85RByGsGeBLvc11FsR44wpwKsAstFzUaEffRt7UH3dHH3BCeZRpxb8fy8mp8iGLCfwdpAYHH
│ │ └── Bcdxx1n9eqH7PYZTnHeBrBcHUMxKLrcLB3sD7XkhmTvSTDCt77Q3Ky3viXr9ptsp24YBPZozFi8mb2wraLohhguF42
├── bootstrap
│ ├── api.js
│ ├── app.css
│ ├── app.js
│ ├── chain-change-watcher.js
│ ├── dataloader.js
│ ├── drizzle.js
│ ├── sentry.js
│ ├── service-worker.js
│ ├── subgraph.js
│ ├── use-notifications.js
│ └── web3.js
├── components
│ ├── .gitignore
│ ├── .stylelintignore
│ ├── account-details-popup.js
│ ├── account-status.js
│ ├── attachment.js
│ ├── balance-table.js
│ ├── breadcrumbs.js
│ ├── buy-pnk-card.js
│ ├── case-card.js
│ ├── case-details-card.jsx
│ ├── case-round-history.js
│ ├── case-summary-card.js
│ ├── cases-list-card.js
│ ├── claim-modal.js
│ ├── collapsable-card.js
│ ├── court-card.js
│ ├── court-cascader-modal.js
│ ├── court-drawer.js
│ ├── court-list-item.js
│ ├── courts-list-card.js
│ ├── dispute-timeline.js
│ ├── error-boundary.js
│ ├── error-fallback
│ │ ├── index.js
│ │ └── switch-chain.js
│ ├── eth-address.js
│ ├── eth-amount.js
│ ├── evidence-card.js
│ ├── evidence-timeline.js
│ ├── footer
│ │ ├── footer.css
│ │ ├── footer.scss
│ │ └── index.js
│ ├── hint.js
│ ├── identicon.js
│ ├── list-item.js
│ ├── multi-balance.js
│ ├── multi-transaction-status.js
│ ├── network-status.js
│ ├── notification-settings.js
│ ├── notifications-card.js
│ ├── notifications.js
│ ├── ongoing-cases-card.js
│ ├── otc-card.js
│ ├── percentage-circle.js
│ ├── performance-card.js
│ ├── pie-chart.js
│ ├── pnk-balance-card.js
│ ├── pnk-exchanges-card.js
│ ├── pnk-stats-list-card.js
│ ├── pnk-xdai-exchanges-card.js
│ ├── required-chain-id-gateway.js
│ ├── required-chain-id-modal.js
│ ├── reward-card.js
│ ├── scroll-bar.js
│ ├── side-chain
│ │ ├── announcement-banner.js
│ │ ├── pnk-actions.js
│ │ └── switch-court-chain.js
│ ├── stake-modal.js
│ ├── stepped-content.js
│ ├── switch-network-message.js
│ ├── theme.less
│ ├── time-ago.js
│ ├── titled-list-card.js
│ ├── top-banner.js
│ └── welcome-card.js
├── containers
│ ├── 404.js
│ ├── case.js
│ ├── cases.js
│ ├── convert-pnk
│ │ ├── convert-pnk.js
│ │ ├── convert-stpnk-card.js
│ │ ├── index.js
│ │ └── switch-chain-button.js
│ ├── courts.js
│ ├── error-fallback.js
│ ├── home.js
│ └── tokens.js
├── helpers
│ ├── array.js
│ ├── block-explorer.js
│ ├── block-numbers.js
│ ├── create-error.js
│ ├── get-token-symbol.js
│ ├── networks.js
│ ├── rewards.js
│ └── transactions.js
├── hooks
│ ├── use-account.js
│ ├── use-chain-id.js
│ ├── use-force-update.js
│ ├── use-generators.js
│ ├── use-get-draws.js
│ ├── use-get-shifts.js
│ ├── use-interval.js
│ ├── use-previous.js
│ ├── use-promise.js
│ └── use-query-params.js
├── index.js
└── temp
│ ├── answer-string.js
│ ├── arbitrable-whitelist.js
│ ├── web3-derive-account.js
│ └── web3-salt.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "@babel/plugin-proposal-optional-chaining",
4 | "@babel/plugin-proposal-nullish-coalescing-operator"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | >0.2%
2 | not dead
3 | not ie <= 11
4 | not op_mini all
5 |
--------------------------------------------------------------------------------
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "@commitlint/config-conventional"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | REACT_APP_USER_SETTINGS_URL=https://hgyxlve79a.execute-api.us-east-2.amazonaws.com/production/user-settings
2 | REACT_APP_JUSTIFICATIONS_URL=https://kleros-api.netlify.app/.netlify/functions
3 | REACT_APP_METAEVIDENCE_URL=https://kleros-api.netlify.app/.netlify/functions/get-dispute-metaevidence
4 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | REACT_APP_USER_SETTINGS_URL=https://hgyxlve79a.execute-api.us-east-2.amazonaws.com/production/user-settings
2 | REACT_APP_JUSTIFICATIONS_URL=https://kleros-api.netlify.app/.netlify/functions
3 | REACT_APP_METAEVIDENCE_URL=https://kleros-api.netlify.app/.netlify/functions/get-dispute-metaevidence
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "react-app",
4 | "eslint:recommended",
5 | "plugin:react/recommended",
6 | "plugin:react-hooks/recommended",
7 | "plugin:import/recommended",
8 | "plugin:import/react",
9 | "plugin:prettier/recommended"
10 | ],
11 | "plugins": [
12 | "prettier",
13 | "react",
14 | "react-hooks",
15 | "import"
16 | ],
17 | "env": {
18 | "browser": true,
19 | "es6": true,
20 | "node": true,
21 | "jest": true
22 | },
23 | "globals": {
24 | "Atomics": "readonly",
25 | "SharedArrayBuffer": "readonly"
26 | },
27 | "parser": "babel-eslint",
28 | "parserOptions": {
29 | "ecmaFeatures": {
30 | "jsx": true
31 | },
32 | "ecmaVersion": 2020,
33 | "sourceType": "module"
34 | },
35 | "settings": {
36 | "react": {
37 | "version": "^16.7.0"
38 | }
39 | },
40 | "rules": {
41 | "no-unused-vars": [
42 | "error",
43 | {
44 | "varsIgnorePattern": "(^_+[0-9]*$)|([iI]gnored$)|(^ignored)",
45 | "argsIgnorePattern": "(^_+[0-9]*$)|([iI]gnored$)|(^ignored)"
46 | }
47 | ],
48 | "no-console": [
49 | "error",
50 | {
51 | "allow": [
52 | "warn",
53 | "error",
54 | "info",
55 | "debug"
56 | ]
57 | }
58 | ]
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | enable-beta-ecosystems: true
8 | updates:
9 | - package-ecosystem: "npm"
10 | directory: "/"
11 | schedule:
12 | interval: "weekly"
13 | open-pull-requests-limit: 10
14 | labels:
15 | - "dependencies"
16 | assignees:
17 | - alcercu
18 | - greenlucid
19 | - andreimvp
20 | - package-ecosystem: "github-actions"
21 | directory: "/"
22 | schedule:
23 | interval: "weekly"
24 | open-pull-requests-limit: 10
25 | labels:
26 | - "dependencies"
27 | assignees:
28 | - alcercu
29 | - greenlucid
30 | - andreimvp
31 | - package-ecosystem: "docker"
32 | directory: "/bots"
33 | schedule:
34 | interval: "weekly"
35 | labels:
36 | - "dependencies"
37 | assignees:
38 | - alcercu
39 | - greenlucid
40 | - andreimvp
41 |
42 |
--------------------------------------------------------------------------------
/.github/workflows/sentry-release.yml:
--------------------------------------------------------------------------------
1 | name: Sentry Release
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - master
8 |
9 | permissions: # added using https://github.com/step-security/secure-workflows
10 | contents: read
11 |
12 | jobs:
13 | release:
14 | runs-on: ubuntu-latest
15 | environment: production
16 | outputs:
17 | version: ${{ steps.set-version.outputs.version }}
18 | steps:
19 | - uses: actions/checkout@v2
20 |
21 | - name: Set up Node.js
22 | uses: actions/setup-node@v2
23 | with:
24 | node-version: 14
25 |
26 | - name: Install dependencies
27 | run: yarn install
28 |
29 | - name: Build and deploy subgraph
30 | run: yarn build
31 |
32 | - name: Set version
33 | id: set-version
34 | run: echo "version=v$(cat package.json | jq -r .version)-$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
35 |
36 | - name: Create Sentry release
37 | uses: getsentry/action-release@v1.2.1
38 | env:
39 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
40 | SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
41 | SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
42 | with:
43 | environment: production
44 | version: ${{ steps.set-version.outputs.version }}
45 | sourcemaps: ./dist
46 |
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Dependencies
4 | /node_modules/
5 |
6 | # Production
7 | /build/
8 |
9 | # Logs
10 | /yarn-debug.log*
11 | /yarn-error.log*
12 |
13 | # Editors
14 | /.vscode/
15 | /.idea/*
16 |
17 | # Misc
18 | .DS_Store
19 |
20 | .env
21 |
22 | .vim*
23 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit ""
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn lint-staged
5 |
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "**/*.{json,html,svg,md}": "prettier --write --ignore-unknown",
3 | "**/*.{less,css}": [
4 | "stylelint --fix",
5 | "prettier --write"
6 | ],
7 | "**/*.{js,jsx}": [
8 | "prettier --write",
9 | "eslint --fix"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | build
3 | coverage
4 |
5 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "overrides": [
4 | {
5 | "files": [
6 | "*.json"
7 | ],
8 | "options": {
9 | "parser": "json-stringify"
10 | }
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "stylelint-config-standard"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - node
4 | cache:
5 | directories:
6 | - node_modules
7 | yarn: true
8 | install: yarn install --pure-lockfile
9 | script:
10 | - yarn run lint
11 | - yarn run test
12 | - yarn run build
13 | notifications:
14 | slack: 'kleros:Ub8n81EgKJ3iRrMDyWyQIVJp'
15 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | See [contributing.kleros.io](https://contributing.kleros.io).
2 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Smart Contract Solutions, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included
14 | in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Court
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | The Kleros Court user interface.
15 |
16 | ## Get Started
17 |
18 | 1. Clone this repo.
19 | 2. Run `yarn` to install dependencies and then `yarn start` to start the dev server.
20 |
21 | > To allow view-only mode, you can the REACT_APP_WEB3_FALLBACK_URL variable to a provider of your choice. Example: REACT_APP_WEB3_FALLBACK_URL=wss://mainnet.infura.io/ws/v3/
22 |
23 | ## Other Scripts
24 |
25 | - `yarn run lint:js` - Lint the all .js/jsx files in the project.
26 | - `yarn run lint:css` - Lint the all .css/less files in the project.
27 | - `yarn run lint` - Lint the entire project.
28 | - `yarn run fix:js` - Fix fixable linting errors in .js/jsx files.
29 | - `yarn run fix:css` - Fix fixable linting errors in .css/less files.
30 | - `yarn run fix` - Fix fixable linting errors in all files in the project.
31 | - `yarn run build` - Create a production build.
32 | - `yarn run build:analyze` - Analyze the production build using source-map-explorer.
33 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | You can privately disclose vulnerabilities to us at any time, by sending an email to clement@kleros.io and contact@kleros.io.
6 | We can then discuss the best way to handle things on a case by case basis.
7 |
--------------------------------------------------------------------------------
/config-overrides.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/rules-of-hooks */
2 | const { override, useBabelRc } = require("customize-cra");
3 |
4 | module.exports = override(useBabelRc());
5 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/public/favicon.ico
--------------------------------------------------------------------------------
/public/icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/public/icon-192.png
--------------------------------------------------------------------------------
/public/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/public/icon-512.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | Kleros · Court
23 |
24 |
39 |
40 |
41 |
47 |
48 |
49 | You need to enable JavaScript to run this app.
50 |
51 |
52 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Kleros Court",
3 | "name": "Kleros · Court",
4 | "icons": [
5 | {
6 | "src": "icon-192.png",
7 | "type": "image/png",
8 | "sizes": "192x192"
9 | },
10 | {
11 | "src": "icon-512.png",
12 | "type": "image/png",
13 | "sizes": "512x512"
14 | }
15 | ],
16 | "start_url": ".",
17 | "display": "standalone",
18 | "theme_color": "#000000",
19 | "background_color": "#ffffff"
20 | }
21 |
--------------------------------------------------------------------------------
/public/stake-kleros-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/public/stake-kleros-logo.png
--------------------------------------------------------------------------------
/public/sw.js:
--------------------------------------------------------------------------------
1 | self.addEventListener('push', event => {
2 | const options = {
3 | body: 'Go to court',
4 | icon: 'stake-kleros-logo.png'
5 | };
6 | event.waitUntil(
7 | self.registration.showNotification(event.data.text(), options)
8 | );
9 | })
10 |
11 | self.addEventListener('notificationclick', function(event) {
12 | const clickedNotification = event.notification;
13 | clickedNotification.close();
14 | let url = 'https://court.kleros.io';
15 | event.waitUntil(
16 | clients.matchAll({type: 'window'}).then( windowClients => {
17 | // Check if there is already a window/tab open with the target URL
18 | for (var i = 0; i < windowClients.length; i++) {
19 | var client = windowClients[i];
20 | // If so, just focus it.
21 | if (client.url === url && 'focus' in client) {
22 | return client.focus();
23 | }
24 | }
25 | // If not, then open the target URL in a new window/tab.
26 | if (clients.openWindow) {
27 | return clients.openWindow(url);
28 | }
29 | })
30 | );
31 | });
32 |
--------------------------------------------------------------------------------
/scripts/sentry-release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
4 |
5 | cd $SCRIPT_DIR/..
6 |
7 | version="court@v$(cat package.json | jq -r .version)-$(git rev-parse --short HEAD)"
8 | sentry-cli releases new "$version"
9 | sentry-cli releases set-commits "$version" --auto
10 |
11 | rm -rf dist/
12 | yarn build
13 | sentry-cli releases files $version upload-sourcemaps dist/
14 |
15 | sentry-cli releases finalize "$version"
16 |
17 | cd -
18 |
--------------------------------------------------------------------------------
/src/adapters/antd/button.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import styled from "styled-components/macro";
4 | import { Button as AntdButton } from "antd";
5 |
6 | /**
7 | * Attempt to tackle props passing issue.
8 | * @see { @link https://github.com/ReactTraining/react-router/issues/6962 }
9 | */
10 |
11 | export function ButtonLink({ children, className, ...props }) {
12 | return (
13 |
14 | {children}
15 |
16 | );
17 | }
18 |
19 | ButtonLink.propTypes = {
20 | children: t.node,
21 | className: t.string,
22 | };
23 |
24 | export function Button({ children, className, ...props }) {
25 | return (
26 |
27 | {children}
28 |
29 | );
30 | }
31 |
32 | Button.propTypes = AntdButton.propTypes;
33 |
34 | const pick = (keys) => (obj) =>
35 | keys.reduce(
36 | (acc, key) => (Object.prototype.hasOwnProperty.call(obj, key) ? Object.assign(acc, { [key]: obj[key] }) : acc),
37 | {}
38 | );
39 |
40 | const getForwardedProps = pick([
41 | "disabled",
42 | "ghost",
43 | "href",
44 | "htmlType",
45 | "icon",
46 | "loading",
47 | "shape",
48 | "size",
49 | "target",
50 | "type",
51 | "onClick",
52 | "block",
53 | ]);
54 |
55 | const StyledButtonLink = styled(AntdButton).attrs((props) => ({ ...props, type: "link" }))`
56 | &.ant-btn-link {
57 | padding-left: 0;
58 | padding-right: 0;
59 | }
60 | `;
61 |
62 | const StyledButton = styled(AntdButton).attrs((props) => ({ ...props }))``;
63 |
--------------------------------------------------------------------------------
/src/adapters/antd/index.js:
--------------------------------------------------------------------------------
1 | export * from "./button";
2 |
--------------------------------------------------------------------------------
/src/api/side-chain/chain-params.js:
--------------------------------------------------------------------------------
1 | import logoXPNK from "../../assets/images/xPNK.png";
2 | import logoStPNK from "../../assets/images/stPNK.png";
3 |
4 | import { getBaseUrl } from "../../helpers/block-explorer";
5 |
6 | export const Tokens = {
7 | PNK: "PNK",
8 | stPNK: "stPNK",
9 | };
10 |
11 | const supportedSideChains = {
12 | // xDai
13 | 100: {
14 | chainId: 100,
15 | chainName: "Gnosis Chain",
16 | nativeCurrency: { name: "xDAI", symbol: "xDAI", decimals: 18 },
17 | rpcUrls: [ensureEnv("REACT_APP_WEB3_FALLBACK_XDAI_HTTPS_URL")],
18 | blockExplorerUrls: [getBaseUrl(100)],
19 | bridgeAppUrl: `https://omni.gnosischain.com/bridge?from=1&to=100&token=${ensureEnv("REACT_APP_PINAKION_ADDRESS")}`,
20 | bridgeAppHistoryUrl: "https://omni.gnosischain.com/history",
21 | mainChainId: 1,
22 | tokens: {
23 | [Tokens.PNK]: {
24 | address: ensureEnv("REACT_APP_RAW_PINAKION_XDAI_ADDRESS"),
25 | symbol: "PNK",
26 | decimals: 18,
27 | image: `${window.location.origin}${logoXPNK}`,
28 | },
29 | [Tokens.stPNK]: {
30 | address: ensureEnv("REACT_APP_PINAKION_XDAI_ADDRESS"),
31 | symbol: "stPNK",
32 | decimals: 18,
33 | image: `${window.location.origin}${logoStPNK}`,
34 | },
35 | },
36 | },
37 | };
38 |
39 | export function getSideChainId(chainId) {
40 | return getSideChainParamsFromMainChainId(chainId).chainId;
41 | }
42 |
43 | export function getSideChainParams(sideChainId) {
44 | const params = supportedSideChains[sideChainId];
45 | if (!params) {
46 | throw new Error(`Unsupported side-chain ID: ${sideChainId}`);
47 | }
48 | return params;
49 | }
50 |
51 | export function isSupportedSideChain(chainId) {
52 | return supportedSideChains[chainId] !== undefined;
53 | }
54 |
55 | export function getSideChainParamsFromMainChainId(mainChainId) {
56 | const params = mainChainIdToSideChainParams[mainChainId];
57 | if (!params) {
58 | throw new Error(`Unsupported chain ID: ${mainChainId}`);
59 | }
60 | return params;
61 | }
62 |
63 | export function getMainChainId(chainId) {
64 | return getSideChainParams(chainId).mainChainId;
65 | }
66 |
67 | export function isSupportedMainChain(chainId) {
68 | return mainChainIdToSideChainParams[chainId] !== undefined;
69 | }
70 |
71 | export function isSupportedChain(chainId) {
72 | return isSupportedSideChain(chainId) || isSupportedMainChain(chainId);
73 | }
74 |
75 | export function getCounterPartyChainId(chainId) {
76 | if (isSupportedMainChain(chainId)) {
77 | return getSideChainId(chainId);
78 | }
79 |
80 | if (isSupportedSideChain(chainId)) {
81 | return getMainChainId(chainId);
82 | }
83 |
84 | throw new Error(`Unsupported chain ID: ${chainId}`);
85 | }
86 |
87 | const mainChainIdToSideChainParams = Object.values(supportedSideChains).reduce(
88 | (acc, chainParams) => Object.assign(acc, { [chainParams.mainChainId]: chainParams }),
89 | {}
90 | );
91 |
92 | function ensureEnv(key, msg = `process.env.${key} is not defined`) {
93 | const value = process.env[key];
94 |
95 | if (value === "" || value === undefined || value === null) {
96 | throw new Error(msg);
97 | }
98 |
99 | return value;
100 | }
101 |
--------------------------------------------------------------------------------
/src/api/side-chain/create-side-chain-api.js:
--------------------------------------------------------------------------------
1 | import Web3 from "web3";
2 | import KlerosLiquidExtraViews from "../../assets/contracts/kleros-liquid-extra-views.json";
3 | import KlerosLiquid from "../../assets/contracts/kleros-liquid.json";
4 | import TokenBridgeXDai from "../../assets/contracts/token-bridge-xdai.json";
5 | import WrappedPinakion from "../../assets/contracts/wrapped-pinakion.json";
6 | import XPinakion from "../../assets/contracts/x-pinakion.json";
7 | import { getCounterPartyChainId, isSupportedSideChain } from "./chain-params";
8 | import * as xDai from "./xdai-api";
9 |
10 | export default async function createSideChainApi(provider) {
11 | const web3 = new Web3(provider);
12 | const chainId = await web3.eth.getChainId();
13 |
14 | if (!isSupportedSideChain(chainId)) {
15 | throw new Error(`Unsuported chain ID: ${chainId}`);
16 | }
17 |
18 | const api = xDai.createApi(xDaiParametersFactory(web3));
19 |
20 | return api;
21 | }
22 |
23 | const xDaiParametersFactory = (web3) => {
24 | const contracts = {
25 | tokenBridge: new web3.eth.Contract(TokenBridgeXDai.abi, ensureEnv("REACT_APP_TOKEN_BRIDGE_XDAI_ADDRESS")),
26 | wrappedPinakion: new web3.eth.Contract(WrappedPinakion.abi, ensureEnv("REACT_APP_PINAKION_XDAI_ADDRESS")),
27 | xPinakion: new web3.eth.Contract(XPinakion.abi, ensureEnv("REACT_APP_RAW_PINAKION_XDAI_ADDRESS")),
28 | klerosLiquidExtraViews: new web3.eth.Contract(
29 | KlerosLiquidExtraViews.abi,
30 | ensureEnv("REACT_APP_KLEROS_LIQUID_EXTRA_VIEWS_XDAI_ADDRESS")
31 | ),
32 | klerosLiquid: new web3.eth.Contract(KlerosLiquid.abi, ensureEnv("REACT_APP_KLEROS_LIQUID_XDAI_ADDRESS")),
33 | };
34 |
35 | contracts.tokenBridge.options.handleRevert = true;
36 | contracts.wrappedPinakion.options.handleRevert = true;
37 | contracts.klerosLiquidExtraViews.options.handleRevert = true;
38 | contracts.klerosLiquid.options.handleRevert = true;
39 |
40 | return {
41 | ...contracts,
42 | chainId: 100,
43 | destinationChainId: getCounterPartyChainId(100),
44 | };
45 | };
46 |
47 | function ensureEnv(key, msg = `process.env.${key} is not defined`) {
48 | const value = process.env[key];
49 |
50 | if (value === "" || value === undefined || value === null) {
51 | throw new Error(msg);
52 | }
53 |
54 | return value;
55 | }
56 |
--------------------------------------------------------------------------------
/src/api/side-chain/create-watch-token.js:
--------------------------------------------------------------------------------
1 | import { Tokens } from "./chain-params";
2 |
3 | export default function createWatchToken({ getChainParams }) {
4 | return async function requestWatchToken(provider, token) {
5 | if (![Tokens.stPNK, Tokens.PNK].includes(token)) {
6 | throw new Error(`Invalid token: ${token}`);
7 | }
8 |
9 | const chainId = Number.parseInt(
10 | await provider.request({
11 | method: "eth_chainId",
12 | }),
13 | 16
14 | );
15 |
16 | const tokenParams = getChainParams(chainId)?.tokens ?? {};
17 | const tokenData = tokenParams[token];
18 |
19 | if (tokenData && !isAssetWatched({ ...tokenData, chainId })) {
20 | try {
21 | await addToken(provider, tokenData);
22 | storeWatchedAsset({ ...tokenData, chainId });
23 | } catch (err) {
24 | console.warn(`Error when adding token ${token}:`, err);
25 | }
26 | }
27 | };
28 | }
29 |
30 | async function addToken(provider, { address, symbol, decimals = 18, image }) {
31 | /**
32 | * FIXME: Apparently this call is broken and the promise will resolve even if the user
33 | * rejects the request to watch the asset.
34 | *
35 | * @see { @link https://github.com/MetaMask/metamask-extension/issues/11377 }
36 | */
37 | return await provider.request({
38 | method: "wallet_watchAsset",
39 | params: {
40 | type: "ERC20",
41 | options: {
42 | address,
43 | symbol,
44 | decimals,
45 | image,
46 | },
47 | },
48 | });
49 | }
50 |
51 | const getStorageKey = ({ chainId, symbol, address }) => `@@kleros/court/tokens/${symbol}/${chainId}/${address}`;
52 |
53 | function isAssetWatched({ chainId, symbol, address }) {
54 | const key = getStorageKey({ chainId, symbol, address });
55 |
56 | try {
57 | return JSON.parse(window.localStorage.getItem(key)) === true;
58 | } catch (err) {
59 | console.warn("Error in isAssetWatched", err);
60 | return false;
61 | }
62 | }
63 |
64 | function storeWatchedAsset({ chainId, symbol, address }) {
65 | const key = getStorageKey({ chainId, symbol, address });
66 |
67 | try {
68 | window.localStorage.setItem(key, "true");
69 | } catch (err) {
70 | console.warn("Error in isAssetWatched", err);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/api/side-chain/index.js:
--------------------------------------------------------------------------------
1 | import createWatchToken from "./create-watch-token";
2 | import { getSideChainParams } from "./chain-params";
3 |
4 | export { SideChainApiProvider, useSideChainApi } from "./react-adapters";
5 | export * from "./chain-params";
6 |
7 | export { default as requestSwitchChain } from "./request-switch-chain";
8 |
9 | export const requestWatchToken = createWatchToken({
10 | getChainParams: getSideChainParams,
11 | });
12 |
--------------------------------------------------------------------------------
/src/api/side-chain/react-adapters.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useMemo } from "react";
2 | import t from "prop-types";
3 | import { Alert, Spin } from "antd";
4 | import usePromise from "../../hooks/use-promise";
5 | import createSideChainApi from "./create-side-chain-api";
6 |
7 | const SideChainApiContext = createContext({});
8 |
9 | export function useSideChainApi() {
10 | return useContext(SideChainApiContext);
11 | }
12 |
13 | export function SideChainApiProvider({ web3Provider, children, renderOnLoading, renderOnError }) {
14 | const p = useMemo(() => createSideChainApi(web3Provider), [web3Provider]);
15 | const sideChainApi = usePromise(p);
16 |
17 | const contentOnLoading = sideChainApi.isPending
18 | ? typeof renderOnLoading === "function"
19 | ? renderOnLoading()
20 | : renderOnLoading
21 | : null;
22 | const contentOnError = sideChainApi.isRejected
23 | ? typeof renderOnError === "function"
24 | ? renderOnError(sideChainApi.reason)
25 | : renderOnError
26 | : null;
27 |
28 | return (
29 | <>
30 | {sideChainApi.isPending ? (
31 | contentOnLoading
32 | ) : sideChainApi.isFulfilled ? (
33 | {children}
34 | ) : (
35 | contentOnError
36 | )}
37 | >
38 | );
39 | }
40 |
41 | SideChainApiProvider.propTypes = {
42 | web3Provider: t.object.isRequired,
43 | children: t.node.isRequired,
44 | renderOnLoading: t.oneOfType([t.node, t.func]),
45 | renderOnError: t.oneOfType([t.node, t.func]),
46 | };
47 |
48 | const defaultRenderOnLoading = (
49 |
50 |
51 |
52 | );
53 |
54 | const defaultRenderOnError = (error) => (
55 |
62 | );
63 |
64 | SideChainApiProvider.defaultProps = {
65 | renderOnLoading: defaultRenderOnLoading,
66 | renderOnError: defaultRenderOnError,
67 | };
68 |
--------------------------------------------------------------------------------
/src/api/side-chain/request-switch-chain.js:
--------------------------------------------------------------------------------
1 | import Web3 from "web3";
2 | import { getSideChainParams, isSupportedSideChain } from "./chain-params";
3 |
4 | const { toHex } = Web3.utils;
5 |
6 | export default async function requestSwitchChain(provider, destinationChainId) {
7 | try {
8 | return await switchChain(provider, { chainId: destinationChainId });
9 | } catch (err) {
10 | // This error code indicates that the chain has not been added to MetaMask
11 | // if it is not, then add it into the user MetaMask
12 | if (err.code === 4902 && isSupportedSideChain(destinationChainId)) {
13 | return await addChain(provider, getSideChainParams(destinationChainId));
14 | }
15 |
16 | throw err;
17 | }
18 | }
19 |
20 | async function addChain(provider, { chainId, chainName, nativeCurrency, rpcUrls, blockExplorerUrls }) {
21 | return await provider.request({
22 | method: "wallet_addEthereumChain",
23 | params: [
24 | {
25 | chainId: toHex(chainId),
26 | chainName: chainName,
27 | nativeCurrency: nativeCurrency,
28 | rpcUrls: rpcUrls,
29 | blockExplorerUrls: blockExplorerUrls,
30 | },
31 | ],
32 | });
33 | }
34 |
35 | async function switchChain(provider, { chainId }) {
36 | return await provider.request({
37 | method: "wallet_switchEthereumChain",
38 | params: [
39 | {
40 | chainId: toHex(chainId),
41 | },
42 | ],
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/src/assets/contracts/uniswap-v2-factory.json:
--------------------------------------------------------------------------------
1 | {
2 | "contractName": "UniswapV2Factory",
3 | "abi": [
4 | {
5 | "inputs": [
6 | { "internalType": "address", "name": "_feeToSetter", "type": "address" }
7 | ],
8 | "payable": false,
9 | "stateMutability": "nonpayable",
10 | "type": "constructor"
11 | },
12 | {
13 | "anonymous": false,
14 | "inputs": [
15 | {
16 | "indexed": true,
17 | "internalType": "address",
18 | "name": "token0",
19 | "type": "address"
20 | },
21 | {
22 | "indexed": true,
23 | "internalType": "address",
24 | "name": "token1",
25 | "type": "address"
26 | },
27 | {
28 | "indexed": false,
29 | "internalType": "address",
30 | "name": "pair",
31 | "type": "address"
32 | },
33 | {
34 | "indexed": false,
35 | "internalType": "uint256",
36 | "name": "",
37 | "type": "uint256"
38 | }
39 | ],
40 | "name": "PairCreated",
41 | "type": "event"
42 | },
43 | {
44 | "constant": true,
45 | "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
46 | "name": "allPairs",
47 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
48 | "payable": false,
49 | "stateMutability": "view",
50 | "type": "function"
51 | },
52 | {
53 | "constant": true,
54 | "inputs": [],
55 | "name": "allPairsLength",
56 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
57 | "payable": false,
58 | "stateMutability": "view",
59 | "type": "function"
60 | },
61 | {
62 | "constant": false,
63 | "inputs": [
64 | { "internalType": "address", "name": "tokenA", "type": "address" },
65 | { "internalType": "address", "name": "tokenB", "type": "address" }
66 | ],
67 | "name": "createPair",
68 | "outputs": [
69 | { "internalType": "address", "name": "pair", "type": "address" }
70 | ],
71 | "payable": false,
72 | "stateMutability": "nonpayable",
73 | "type": "function"
74 | },
75 | {
76 | "constant": true,
77 | "inputs": [],
78 | "name": "feeTo",
79 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
80 | "payable": false,
81 | "stateMutability": "view",
82 | "type": "function"
83 | },
84 | {
85 | "constant": true,
86 | "inputs": [],
87 | "name": "feeToSetter",
88 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
89 | "payable": false,
90 | "stateMutability": "view",
91 | "type": "function"
92 | },
93 | {
94 | "constant": true,
95 | "inputs": [
96 | { "internalType": "address", "name": "", "type": "address" },
97 | { "internalType": "address", "name": "", "type": "address" }
98 | ],
99 | "name": "getPair",
100 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
101 | "payable": false,
102 | "stateMutability": "view",
103 | "type": "function"
104 | },
105 | {
106 | "constant": false,
107 | "inputs": [
108 | { "internalType": "address", "name": "_feeTo", "type": "address" }
109 | ],
110 | "name": "setFeeTo",
111 | "outputs": [],
112 | "payable": false,
113 | "stateMutability": "nonpayable",
114 | "type": "function"
115 | },
116 | {
117 | "constant": false,
118 | "inputs": [
119 | { "internalType": "address", "name": "_feeToSetter", "type": "address" }
120 | ],
121 | "name": "setFeeToSetter",
122 | "outputs": [],
123 | "payable": false,
124 | "stateMutability": "nonpayable",
125 | "type": "function"
126 | }
127 | ]
128 | }
129 |
--------------------------------------------------------------------------------
/src/assets/images/alert.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/arrow-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/bell.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/bitfinex.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | logo
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/assets/images/breadcrumb.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/dark-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/dark-logo.png
--------------------------------------------------------------------------------
/src/assets/images/deversifi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/deversifi.png
--------------------------------------------------------------------------------
/src/assets/images/document.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/etherscan-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/etherscan-logo.png
--------------------------------------------------------------------------------
/src/assets/images/folder.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/gavel.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/ghost.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/assets/images/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/hexagon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/hourglass.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/image.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/info.png
--------------------------------------------------------------------------------
/src/assets/images/info.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/kleros-logo-flat-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/assets/images/kleros.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/assets/images/kyber.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/kyber.png
--------------------------------------------------------------------------------
/src/assets/images/light-purple-arrow.svg:
--------------------------------------------------------------------------------
1 |
9 |
15 |
16 |
--------------------------------------------------------------------------------
/src/assets/images/link.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/linkedin.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/loopring.svg:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 | logo-blue
12 | Created with Sketch.
13 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/assets/images/mail.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/omnibridge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/omnibridge.png
--------------------------------------------------------------------------------
/src/assets/images/paraswap.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/paraswap.jpg
--------------------------------------------------------------------------------
/src/assets/images/pdf.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/present.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/purple-arrow.svg:
--------------------------------------------------------------------------------
1 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/assets/images/question-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/reward.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/reward.png
--------------------------------------------------------------------------------
/src/assets/images/reward.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/assets/images/right-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/scales.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/section-arrow-background.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/section-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/skills.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/skills.png
--------------------------------------------------------------------------------
/src/assets/images/skills.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/assets/images/spinner.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/stPNK.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/stPNK.png
--------------------------------------------------------------------------------
/src/assets/images/stake-kleros-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/stake-kleros-logo.png
--------------------------------------------------------------------------------
/src/assets/images/sushiswap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/sushiswap.png
--------------------------------------------------------------------------------
/src/assets/images/telegram.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/transak.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/transak.png
--------------------------------------------------------------------------------
/src/assets/images/underline.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/video.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/x.svg:
--------------------------------------------------------------------------------
1 |
17 |
18 |
--------------------------------------------------------------------------------
/src/assets/images/xPNK.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kleros/court/a4528c27467499dd84b2c87bf3310939335647ce/src/assets/images/xPNK.png
--------------------------------------------------------------------------------
/src/assets/policies/9ffaRetGoVpJqrJi3wrqvEhADpPT1yZaKS7azcxEq1X7MJWGbCMBzjGNbcKeCnkneHvyBwmsfwF7QZAuuQLrh3dqc4:
--------------------------------------------------------------------------------
1 | {
2 | "name": "General Court",
3 | "description": "> “Whoever controls the courts, controls the state.”\n>\n> Aristotle\n\n#### General Court\n\nThis is a sample description for a Kleros policy. It supports markdown!\n\n| Look | Tables |\n| ---- | ------ |\n| too | ✔ |\n| how | ✔ |\n| cool | ✔ |\n",
4 | "summary": "You can also write code if you ever need to.\n\n```js\nimport React from 'react'\nimport ReactDOM from 'react-dom'\n\nReactDOM.render(\n Decentralize Justice!
,\n document.getElementById('root')\n)\n```\n",
5 | "policies": [
6 | {
7 | "title": "Sample Policy Title",
8 | "description": "Sample Policy description.",
9 | "clauses": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
10 | "justifications?": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like)."
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/assets/policies/9ffdfp3Hm9CsVpYA9FAxAEVV9iitYy9GRs7XfrMuBPNqF9dKsXPxUYNebDnQ6Z32Ycw7ZyjQ3Tp8KYSMzjg2Uskxgm:
--------------------------------------------------------------------------------
1 | {
2 | "name": "E-Commerce",
3 | "description": "> “Whoever controls the courts, controls the state.”\n>\n> Aristotle\n\n#### E-Commerce\n\nThis is a sample description for a Kleros policy. It supports markdown!\n\n| Look | Tables |\n| ---- | ------ |\n| too | ✔ |\n| how | ✔ |\n| cool | ✔ |\n",
4 | "summary": "You can also write code if you ever need to.\n\n```js\nimport React from 'react'\nimport ReactDOM from 'react-dom'\n\nReactDOM.render(\n Decentralize Justice!
,\n document.getElementById('root')\n)\n```\n",
5 | "policies": [
6 | {
7 | "title": "Sample Policy Title",
8 | "description": "Sample Policy description.",
9 | "clauses": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
10 | "justifications?": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like)."
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/assets/policies/9fferP2QNWuLxkaR79eUS7cFPbcmS3gGsog3ibWLAAyGTgfUrEYeDZSwkcGZvkTMFRpqvW2bxKDfNL2jHCnknrjGdW:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Transport",
3 | "description": "> “Whoever controls the courts, controls the state.”\n>\n> Aristotle\n\n#### Transport\n\nThis is a sample description for a Kleros policy. It supports markdown!\n\n| Look | Tables |\n| ---- | ------ |\n| too | ✔ |\n| how | ✔ |\n| cool | ✔ |\n",
4 | "summary": "You can also write code if you ever need to.\n\n```js\nimport React from 'react'\nimport ReactDOM from 'react-dom'\n\nReactDOM.render(\n Decentralize Justice!
,\n document.getElementById('root')\n)\n```\n",
5 | "policies": [
6 | {
7 | "title": "Sample Policy Title",
8 | "description": "Sample Policy description.",
9 | "clauses": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
10 | "justifications?": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like)."
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/assets/policies/9ffjVwGsqEWJ5uFbkuU7PgVARrRDhGoJAJp6FCeTkBD1juQChg9BvB6CbfQrrNsJKZJXsbvDGvmiN7Nb2piafdBMPW:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Insurance",
3 | "description": "> “Whoever controls the courts, controls the state.”\n>\n> Aristotle\n\n#### Insurance\n\nThis is a sample description for a Kleros policy. It supports markdown!\n\n| Look | Tables |\n| ---- | ------ |\n| too | ✔ |\n| how | ✔ |\n| cool | ✔ |\n",
4 | "summary": "You can also write code if you ever need to.\n\n```js\nimport React from 'react'\nimport ReactDOM from 'react-dom'\n\nReactDOM.render(\n Decentralize Justice!
,\n document.getElementById('root')\n)\n```\n",
5 | "policies": [
6 | {
7 | "title": "Sample Policy Title",
8 | "description": "Sample Policy description.",
9 | "clauses": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
10 | "justifications?": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like)."
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/assets/policies/Bccx3Zffpi6gfK38PpVQitC3emyRinSf6BE9PNcnyLfMPjVDZ1aChk7zPYYnR9RRcwJyLhko4ZxKZWVfCyaGTt3iZq:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Token Listing",
3 | "description": "This court serves as the final validation for token listing for verified projects listing on the Ethfinex Exchange using Kleros’ Token Curated List Dapp.\nThis is a high level, high stake court requiring deep blockchain knowledge, legal experience and / or a knowledge of exchange listings in general. Jurors are required to stake a large amount of PNK and should only do so if they are confident in the above capabilities.",
4 | "summary": "[Ethfinex Court Policy](https://cdn.kleros.link/ipfs/QmVzwEBpGsbFY3UgyjA3SxgGXx3r5gFGynNpaoXkp6jenu/Ethfinex%20Court%20Policy.pdf)"
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/policies/Bcd1WbSfBY5yYwnTrS5ddGemKuGt6WUyE2LptK1kBR4WhXudrM6Erd651VeGvtwmQaNmNC1C7JPVVy1bVomD8dn64i:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Blockchain",
3 | "description": "**Court Purpose:**\n\nThis is the blockchain community subcourt. Disputes in this subcourt should be those that require that jurors have an understanding of the broad blockchain ecosystem. Cases in this court may come from varying aspects of the ecosystem and could also be from lower courts that have been appealed. For example, a case in the Token Curated Registry could arrive here on appeal.\n",
4 | "summary": ""
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/policies/Bcd3cW1wGYuFZ52QBSLC9CVujQedXj1oY7Yv2KCCnaukt7QEFbWwu54t1Je1mJ95Ui2oa4WaYZUsgJiUhEEyHdbdzm:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Non-technical",
3 | "description": "**Court Purpose**\nThis court is for jurors ruling on challenged tokens from our TCR Dapp with Ethfinex. Challenged submission from the (Insert URL) Token Curated List will be resolved in this court unless appealed.",
4 | "summary": "**Guidelines:**\n- Jurors should carefully check all required fields in each submitted case\n- Contract addresses are an attack vector and should be checked carefully.\n- Attached Logos should be PNG format with a transparent background.\n- Project names and tickers should be carefully checked.\n- In case of duplicates, only the first submission should be accepted. The most recent submissions appear highest in the list."
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/policies/Bcd8V9FLHVc9F3nFCR85RByGsGeBLvc11FsR44wpwKsAstFzUaEffRt7UH3dHH3BCeZRpxb8fy8mp8iGLCfwdpAYHH:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Juror Training",
3 | "description": "This is the Kleros juror training court.",
4 | "summary": "Lots of general knowledge disputes suited for beginner arbitrators.",
5 | "policies": [
6 | {
7 | "title": "Sample Policy Title",
8 | "description": "Sample Policy description.",
9 | "clauses": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
10 | "justifications?": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like)."
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/assets/policies/Bcdxx1n9eqH7PYZTnHeBrBcHUMxKLrcLB3sD7XkhmTvSTDCt77Q3Ky3viXr9ptsp24YBPZozFi8mb2wraLohhguF42:
--------------------------------------------------------------------------------
1 | {
2 | "name": "General Court",
3 | "description": "**Court Purpose**\n\nThe General court exists as the top court in the hierarchy. All appeals made in subcourts will make their way to the General Court.",
4 | "summary": "**Guidelines:** \n- Jurors should cast their vote with a suitable verification.\n- Jurors should not rule in favor of a side who have engaged in immoral activities (example: rule reject on “revenge porn” images even if they would otherwise fit into the category).\n- “Refuse to arbitrate” should be used for disputes where both sides of the dispute have engaged in activities which are immoral (ex: refuse to rule on an assassination market dispute).\n- Immoral activities include: Murder, slavery, rape, violence, theft and perjury.\n- Jurors should attempt to interpret disputes according to the \"spirit of the dispute\" unless the arbitrable contract or the policies of the subcourt state otherwise.\n- Jurors should interpret disputes without assuming the existence of gods, spirits or other supernatural beings unless the arbitrable contract or the policies of the subcourt state otherwise."
5 | }
6 |
--------------------------------------------------------------------------------
/src/bootstrap/api.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import web3DeriveAccount from "../temp/web3-derive-account";
3 |
4 | export const accessSettings = async ({ patch, web3, address, settings }) => {
5 | const derived = await web3DeriveAccount(web3, address, patch);
6 | try {
7 | if (!derived) {
8 | if (!patch) throw new Error("No derived account stored.");
9 |
10 | return (
11 | await axios.patch(process.env.REACT_APP_USER_SETTINGS_URL, {
12 | payload: {
13 | address,
14 | settings,
15 | signature: await web3.eth.personal.sign(JSON.stringify(settings), address),
16 | },
17 | })
18 | ).data;
19 | }
20 |
21 | try {
22 | return (
23 | await axios[patch ? "patch" : "post"](process.env.REACT_APP_USER_SETTINGS_URL, {
24 | payload: {
25 | address,
26 | settings,
27 | signature: derived.sign(JSON.stringify(settings)).signature,
28 | },
29 | })
30 | ).data;
31 | } catch (err) {
32 | if (!patch) throw new Error("No derived account stored.");
33 |
34 | const settingsWithDerived = { ...settings, derivedAccountAddress: { S: derived.address } };
35 | return (
36 | await axios.patch(process.env.REACT_APP_USER_SETTINGS_URL, {
37 | payload: {
38 | address,
39 | settings: settingsWithDerived,
40 | signature: await web3.eth.personal.sign(JSON.stringify(settingsWithDerived), address),
41 | },
42 | })
43 | ).data;
44 | }
45 | } catch (err) {
46 | console.error(err.message);
47 | return { error: "An unexpected error occurred." };
48 | }
49 | };
50 |
51 | export const postJustification = async ({ web3, account, justification }) => {
52 | const derived = await web3DeriveAccount(web3, account, true);
53 | try {
54 | await axios.post(`${process.env.REACT_APP_JUSTIFICATIONS_URL}/put-justification`, {
55 | account,
56 | chainId: await web3.eth.getChainId(),
57 | justification,
58 | signature: derived
59 | ? derived.sign(JSON.stringify(justification)).signature
60 | : await web3.eth.personal.sign(JSON.stringify(justification), account),
61 | });
62 | } catch (err) {
63 | if (derived) {
64 | await axios.post(`${process.env.REACT_APP_JUSTIFICATIONS_URL}/put-justification`, {
65 | account,
66 | chainId: await web3.eth.getChainId(),
67 | derived: derived.address,
68 | derivedSignature: await web3.eth.personal.sign(
69 | `Sign this to confirm derived account address ${derived.address}. This will be used to provide justifications.`,
70 | account
71 | ),
72 | justification,
73 | signature: derived.sign(JSON.stringify(justification)).signature,
74 | });
75 | return;
76 | }
77 |
78 | console.error(err.message);
79 | }
80 | };
81 |
--------------------------------------------------------------------------------
/src/bootstrap/app.css:
--------------------------------------------------------------------------------
1 | img {
2 | max-width: 100%;
3 | }
4 |
--------------------------------------------------------------------------------
/src/bootstrap/chain-change-watcher.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import t from "prop-types";
3 | import useChainId from "../hooks/use-chain-id";
4 | import usePrevious from "../hooks/use-previous";
5 |
6 | export default function ChainChangeWatcher({ children }) {
7 | useReloadOnChainChanged();
8 |
9 | return children;
10 | }
11 |
12 | ChainChangeWatcher.propTypes = {
13 | children: t.node.isRequired,
14 | };
15 |
16 | function useReloadOnChainChanged() {
17 | const chainId = useChainId();
18 | const previousChainId = usePrevious(chainId);
19 |
20 | useEffect(() => {
21 | if (chainId !== undefined && previousChainId !== undefined && chainId !== previousChainId) {
22 | window.location.reload();
23 | }
24 | }, [previousChainId, chainId]);
25 | }
26 |
--------------------------------------------------------------------------------
/src/bootstrap/sentry.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import * as Sentry from "@sentry/react";
3 | import { BrowserTracing } from "@sentry/tracing";
4 | import { version } from "../../package.json";
5 | import App from "./app";
6 | import DefaultFallback from "../components/error-fallback";
7 |
8 | Sentry.init({
9 | dsn: process.env.REACT_APP_SENTRY_ENDPOINT,
10 | environment: process.env.REACT_APP_CONTEXT,
11 | release: `court@${version}`,
12 | integrations: [new BrowserTracing()],
13 | });
14 |
15 | Sentry.withErrorBoundary(App, { fallback: });
16 |
--------------------------------------------------------------------------------
/src/bootstrap/subgraph.js:
--------------------------------------------------------------------------------
1 | export const displaySubgraph = {
2 | 1: process.env.REACT_APP_SUBGRAPH_MAINNET_DISPLAY,
3 | 100: process.env.REACT_APP_SUBGRAPH_GNOSIS_DISPLAY,
4 | 10200: "https://api.studio.thegraph.com/query/61738/kleros-display-chiado/version/latest",
5 | 11155111: "https://api.studio.thegraph.com/query/61738/kleros-display-sepolia/version/latest",
6 | };
7 |
--------------------------------------------------------------------------------
/src/bootstrap/web3.js:
--------------------------------------------------------------------------------
1 | import Web3 from "web3";
2 |
3 | let web3;
4 |
5 | if (typeof window !== "undefined" && typeof window.ethereum !== "undefined") {
6 | web3 = new Web3(window.ethereum.currentProvider);
7 | } else if (process.env.REACT_APP_WEB3_FALLBACK_HTTPS_URL || process.env.REACT_APP_WEB3_FALLBACK_URL) {
8 | // Fallback provider.
9 | web3 = new Web3(process.env.REACT_APP_WEB3_FALLBACK_HTTPS_URL ?? process.env.REACT_APP_WEB3_FALLBACK_URL);
10 | } else {
11 | throw new Error("No fallback Web3 URL provided!");
12 | }
13 |
14 | export default web3;
15 |
16 | const chainIdToRpcEndpoint = {
17 | 1: process.env.REACT_APP_WEB3_FALLBACK_HTTPS_URL,
18 | 10: process.env.REACT_APP_WEB3_FALLBACK_OPTIMISM_HTTPS_URL,
19 | 100: process.env.REACT_APP_WEB3_FALLBACK_XDAI_HTTPS_URL,
20 | 130: process.env.REACT_APP_WEB3_FALLBACK_UNICHAIN_HTTPS_URL,
21 | 137: process.env.REACT_APP_WEB3_FALLBACK_POLYGON_HTTPS_URL,
22 | 300: process.env.REACT_APP_WEB3_FALLBACK_ZKSYNCSEPOLIA_HTTPS_URL,
23 | 324: process.env.REACT_APP_WEB3_FALLBACK_ZKSYNC_HTTPS_URL,
24 | 690: process.env.REACT_APP_WEB3_FALLBACK_REDSTONE_HTTPS_URL,
25 | 1301: process.env.REACT_APP_WEB3_FALLBACK_UNICHAINSEPOLIA_HTTPS_URL,
26 | 10200: process.env.REACT_APP_WEB3_FALLBACK_CHIADO_HTTPS_URL,
27 | 80001: process.env.REACT_APP_WEB3_FALLBACK_MUMBAI_HTTPS_URL,
28 | 42161: process.env.REACT_APP_WEB3_FALLBACK_ARBITRUM_HTTPS_URL,
29 | 421614: process.env.REACT_APP_WEB3_FALLBACK_ARBITRUMSEPOLIA_HTTPS_URL,
30 | 11155111: process.env.REACT_APP_WEB3_FALLBACK_SEPOLIA_HTTPS_URL,
31 | 11155420: process.env.REACT_APP_WEB3_FALLBACK_OPTIMISMSEPOLIA_HTTPS_URL,
32 | };
33 |
34 | export function getReadOnlyRpcUrl(chainId) {
35 | const url = chainIdToRpcEndpoint[chainId];
36 | if (!url) {
37 | throw new Error(`Unsupported chain ID: ${chainId}`);
38 | }
39 |
40 | return url;
41 | }
42 |
43 | export function getReadOnlyWeb3({ chainId }) {
44 | return new Web3(getReadOnlyRpcUrl(chainId));
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/.gitignore:
--------------------------------------------------------------------------------
1 | theme.css
2 |
--------------------------------------------------------------------------------
/src/components/.stylelintignore:
--------------------------------------------------------------------------------
1 | theme.css
2 |
--------------------------------------------------------------------------------
/src/components/account-details-popup.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import t from "prop-types";
3 | import styled from "styled-components/macro";
4 | import { List, Popover, Spin, Divider } from "antd";
5 | import { drizzleReactHooks } from "@drizzle/react-plugin";
6 | import { VIEW_ONLY_ADDRESS } from "../bootstrap/dataloader";
7 | import ETHAddress from "./eth-address";
8 | import ETHAmount from "./eth-amount";
9 | import Identicon from "./identicon";
10 | import { getTokenSymbol } from "../helpers/get-token-symbol";
11 |
12 | const { useDrizzle, useDrizzleState } = drizzleReactHooks;
13 |
14 | export default function AccountDetailsPopup({ trigger, pinakion, className }) {
15 | const { useCacheCall } = useDrizzle();
16 | const chainId = useDrizzleState((ds) => ds.web3.networkId);
17 | const pnkTokenSymbol = useMemo(() => getTokenSymbol(chainId, "PNK"), [chainId]);
18 |
19 | const drizzleState = useDrizzleState((drizzleState) => ({
20 | account: drizzleState.accounts[0] || VIEW_ONLY_ADDRESS,
21 | balance: drizzleState.accounts[0] ? drizzleState.accountBalances[drizzleState.accounts[0]] : null,
22 | }));
23 |
24 | const PNK = useCacheCall("MiniMeTokenERC20", "balanceOf", drizzleState.account);
25 |
26 | return (
27 |
32 |
33 |
36 |
37 |
38 |
39 | }
40 | title="Address"
41 | />
42 |
43 | {drizzleState.account && (
44 |
45 | }
47 | title="Balance"
48 | />
49 |
50 | )}
51 | {pinakion && (
52 |
53 |
54 | }
56 | title={<>{pnkTokenSymbol} Balance>}
57 | />
58 |
59 |
60 | )}
61 |
62 | ) : (
63 |
64 | No Wallet Detected
65 |
66 | To view account details, a web3 wallet such as{" "}
67 |
68 | Metamask
69 | {" "}
70 | is required.
71 |
72 |
73 | )
74 | }
75 | placement="bottomRight"
76 | title="Account"
77 | trigger="click"
78 | className={className}
79 | >
80 | {trigger}
81 |
82 | );
83 | }
84 |
85 | AccountDetailsPopup.propTypes = {
86 | trigger: t.node.isRequired,
87 | className: t.string,
88 | pinakion: t.bool,
89 | };
90 |
91 | AccountDetailsPopup.defaultProps = {
92 | className: "",
93 | pinakion: false,
94 | };
95 |
96 | const StyledIdentity = styled.div`
97 | display: flex;
98 | gap: 8px;
99 | align-items: center;
100 | `;
101 |
102 | const StyledViewOnlyDiv = styled.div`
103 | max-width: 360px;
104 | `;
105 |
--------------------------------------------------------------------------------
/src/components/account-status.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components/macro";
3 | import { Divider } from "antd";
4 | import { drizzleReactHooks } from "@drizzle/react-plugin";
5 | import { VIEW_ONLY_ADDRESS } from "../bootstrap/dataloader";
6 | import AccountDetailsPopup from "./account-details-popup";
7 | import NetworkStatus from "./network-status";
8 | import ETHAddress from "./eth-address";
9 |
10 | const { useDrizzleState } = drizzleReactHooks;
11 |
12 | export default function AccountStatus() {
13 | const { account = VIEW_ONLY_ADDRESS } = useDrizzleState((drizzleState) => ({
14 | account: drizzleState.accounts[0],
15 | }));
16 |
17 | return (
18 |
22 |
23 | {account !== VIEW_ONLY_ADDRESS ? (
24 | <>
25 |
26 |
27 |
28 |
29 | >
30 | ) : null}
31 |
32 | }
33 | />
34 | );
35 | }
36 |
37 | const StyledAccountStatus = styled.button`
38 | display: flex;
39 | gap: 8px;
40 | align-items: center;
41 | cursor: pointer;
42 | color: white;
43 | border: none;
44 | background-color: rgba(255, 255, 255, 0.15);
45 | border-radius: 24px;
46 | padding: 8px 12px;
47 | font-weight: 400;
48 | transition: all 0.2s ease-in;
49 |
50 | :hover,
51 | :focus {
52 | background-color: rgba(255, 255, 255, 0.25);
53 | }
54 |
55 | .address {
56 | font-family: "Roboto Mono", monospace;
57 | font-size: 12px;
58 | }
59 | `;
60 |
61 | const StyledDivider = styled(Divider)`
62 | position: static;
63 | top: initial;
64 | height: 14px;
65 | background-color: rgba(255, 255, 255, 0.25);
66 | margin: 0;
67 | `;
68 |
--------------------------------------------------------------------------------
/src/components/attachment.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import styled from "styled-components/macro";
4 | import { Divider, Popover } from "antd";
5 | import isImage from "is-image";
6 | import isTextPath from "is-text-path";
7 | import isVideo from "is-video";
8 | import { ReactComponent as Document } from "../assets/images/document.svg";
9 | import { ReactComponent as Image } from "../assets/images/image.svg";
10 | import { ReactComponent as Link } from "../assets/images/link.svg";
11 | import { ReactComponent as PDF } from "../assets/images/pdf.svg";
12 | import { ReactComponent as Video } from "../assets/images/video.svg";
13 |
14 | const StyledPopover = styled(({ className, ...rest }) => (
15 |
16 | ))`
17 | .ant-popover {
18 | &-inner {
19 | border: 1px solid;
20 | }
21 |
22 | &-title {
23 | color: inherit;
24 | }
25 | }
26 | `;
27 | const StyledIFrame = styled.iframe`
28 | height: 400px;
29 | margin-top: -8px;
30 | width: 300px;
31 | `;
32 |
33 | const isPDF = (extension) => extension.toLowerCase() === ".pdf";
34 |
35 | const Attachment = ({ URI, description, extension: _extension, previewURI, title }) => {
36 | let extension;
37 | if (!_extension && URI) extension = `.${URI.split(".").pop()}`;
38 | else extension = `.${_extension}`;
39 | let Component;
40 | if (!URI) Component = Document;
41 | else if (isPDF(extension)) Component = PDF;
42 | else if (isTextPath(extension)) Component = Document;
43 | else if (isImage(extension)) Component = Image;
44 | else if (isVideo(extension)) Component = Video;
45 | else Component = Link;
46 | Component = ;
47 | // No popover
48 | if (!title && !description) {
49 | if (URI)
50 | return (
51 |
52 | {Component}
53 |
54 | );
55 | return Component;
56 | }
57 |
58 | return (
59 |
65 | {description}
66 |
67 |
68 | >
69 | ) : (
70 | description
71 | )
72 | }
73 | title={title}
74 | >
75 | {URI ? (
76 |
77 | {Component}
78 |
79 | ) : (
80 | Component
81 | )}
82 |
83 | );
84 | };
85 |
86 | Attachment.propTypes = {
87 | URI: t.string,
88 | description: t.string,
89 | extension: t.string,
90 | previewURI: t.string,
91 | title: t.string,
92 | };
93 |
94 | Attachment.defaultProps = {
95 | URI: null,
96 | extension: null,
97 | previewURI: null,
98 | };
99 |
100 | export default Attachment;
101 |
--------------------------------------------------------------------------------
/src/components/balance-table.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import clsx from "clsx";
4 | import { Typography } from "antd";
5 | import styled from "styled-components/macro";
6 | import EthAmount from "./eth-amount";
7 |
8 | export default function BalanceTable({ title, children }) {
9 | return (
10 |
11 | {title}
12 |
13 |
16 |
17 |
18 | );
19 | }
20 |
21 | BalanceTable.propTypes = {
22 | title: t.node.isRequired,
23 | children: t.oneOfType([t.node, t.arrayOf(t.node)]).isRequired,
24 | };
25 |
26 | BalanceTable.Row = Row;
27 | BalanceTable.EmptyRow = EmptyRow;
28 |
29 | function Row({ description, error, value, tokenSymbol, variant, level }) {
30 | return (
31 |
32 | {description}
33 | {error ? (
34 |
35 | {error.message}
36 |
37 | ) : (
38 | <>
39 |
40 |
41 |
42 | {tokenSymbol}
43 | >
44 | )}
45 |
46 | );
47 | }
48 |
49 | Row.propTypes = {
50 | description: t.node.isRequired,
51 | error: t.instanceOf(Error),
52 | value: t.oneOfType([t.string, t.number, t.object]),
53 | tokenSymbol: t.node.isRequired,
54 | variant: t.oneOf(["default", "primary", "warning"]),
55 | level: t.number,
56 | };
57 |
58 | Row.defaultProps = {
59 | variant: "default",
60 | level: 0,
61 | };
62 |
63 | function EmptyRow() {
64 | return (
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | const StyledBalanceTableWrapper = styled.section``;
72 |
73 | const StyledTitle = styled(Typography.Title)`
74 | text-align: center;
75 | `;
76 |
77 | const StyledContent = styled.main`
78 | background: #fff;
79 | border: 1px solid rgba(0, 0, 0, 0.1);
80 | box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.06);
81 | border-radius: 18px;
82 | padding: 24px 30px;
83 | color: rgba(0, 0, 0, 0.45);
84 |
85 | table {
86 | width: 100%;
87 | border-collapse: collapse;
88 | }
89 |
90 | thead {
91 | th {
92 | color: rgba(0, 0, 0, 0.45);
93 | text-align: center;
94 | }
95 | }
96 |
97 | tbody {
98 | th,
99 | td {
100 | padding: 0 4px;
101 | }
102 |
103 | tr > :first-child {
104 | padding-left: 0;
105 | }
106 |
107 | tr > :last-child {
108 | padding-right: 0;
109 | }
110 |
111 | th {
112 | font-weight: normal;
113 | }
114 |
115 | td {
116 | font-weight: bold;
117 | }
118 |
119 | td.amount {
120 | width: 100%;
121 | text-align: right;
122 | }
123 |
124 | th,
125 | td.token {
126 | white-space: nowrap;
127 | }
128 |
129 | tr.spacer > td {
130 | text-indent: -9999px;
131 | }
132 |
133 | tr.level-1 > :first-child {
134 | padding-left: 16px;
135 | }
136 |
137 | tr.level-2 > :first-child {
138 | padding-left: 32px;
139 | }
140 |
141 | tr.level-3 > :first-child {
142 | padding-left: 48px;
143 | }
144 |
145 | tr.primary {
146 | color: rgba(0, 0, 0, 0.85);
147 |
148 | > td.amount,
149 | > td.token {
150 | color: #009aff;
151 | }
152 | }
153 |
154 | tr.warning {
155 | > td.amount,
156 | > td.token {
157 | color: #faad14;
158 | }
159 | }
160 |
161 | td.error {
162 | color: #f60c36;
163 | }
164 |
165 | td.empty::before {
166 | content: "Empty cell";
167 | visibility: hidden;
168 | speak: none;
169 | }
170 | }
171 | `;
172 |
--------------------------------------------------------------------------------
/src/components/breadcrumbs.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 | import { ReactComponent as Breadcrumb } from '../assets/images/breadcrumb.svg'
3 | import PropTypes from 'prop-types'
4 | import styled from 'styled-components/macro'
5 |
6 | const StyledDiv = styled.div`
7 | height: ${props => (props.large ? 38 : 20)}px;
8 | overflow-x: auto;
9 | overflow-y: hidden;
10 | position: relative;
11 | width: 100%;
12 | `
13 | const StyledBreadcrumbDiv = styled.div`
14 | cursor: pointer;
15 | height: ${props => (props.large ? 38 : 20)}px;
16 | left: ${props => props.id * 100}px;
17 | position: absolute;
18 | top: 0;
19 | z-index: ${props => props.length - props.id};
20 |
21 | .first-fill {
22 | path {
23 | fill: #1e075f;
24 | }
25 | }
26 | .second-fill {
27 | path {
28 | fill: #4d00b4;
29 | }
30 | }
31 | .third-fill {
32 | path {
33 | fill: #009aff;
34 | }
35 | }
36 | `
37 | const StyledBreadcrumb = styled(Breadcrumb)`
38 | height: ${props => (props.large === 'true' ? 38 : 20)}px;
39 | width: ${props => (props.large === 'true' ? 255 : 114)}px;
40 | `
41 | const StyledTitleDiv = styled.div`
42 | color: white;
43 | font-size: ${props => (props.large ? 14 : 10)}px;
44 | left: ${props => (props.large ? 28 : 20)}px;
45 | line-height: ${props => (props.large ? 38 : 20)}px;
46 | ${props => props.active && 'font-weight: bold;'}
47 | overflow: hidden;
48 | position: absolute;
49 | text-overflow: ellipsis;
50 | top: 0;
51 | user-select: none;
52 | width: ${props => (props.large === 'true' ? 215 : 90)}px;
53 | `
54 |
55 | const Breadcrumbs = ({
56 | activeIndex,
57 | breadcrumbs,
58 | className,
59 | colorIndex,
60 | large,
61 | onClick
62 | }) => (
63 |
64 | {(Array.isArray(breadcrumbs) ? breadcrumbs : [breadcrumbs]).map((b, i) => (
65 | onClick(Number(e.currentTarget.id)), [onClick])
73 | }
74 | >
75 |
83 |
84 | {b}
85 |
86 |
87 | ))}
88 |
89 | )
90 |
91 | Breadcrumbs.propTypes = {
92 | activeIndex: PropTypes.number,
93 | breadcrumbs: PropTypes.oneOfType([
94 | PropTypes.arrayOf(PropTypes.node.isRequired).isRequired,
95 | PropTypes.node.isRequired
96 | ]).isRequired,
97 | className: PropTypes.string,
98 | colorIndex: PropTypes.number,
99 | large: PropTypes.bool,
100 | onClick: PropTypes.func
101 | }
102 |
103 | Breadcrumbs.defaultProps = {
104 | activeIndex: null,
105 | className: null,
106 | colorIndex: null,
107 | large: false,
108 | onClick: null
109 | }
110 |
111 | export default Breadcrumbs
112 |
--------------------------------------------------------------------------------
/src/components/case-summary-card.js:
--------------------------------------------------------------------------------
1 | import { Col, Row } from "antd";
2 | import React from "react";
3 | import { Link, withRouter } from "react-router-dom";
4 | import styled from "styled-components/macro";
5 | import t from "prop-types";
6 |
7 | const StyledCaseSummary = styled.div`
8 | background: white;
9 | border-radius: 12px;
10 | display: inline-block;
11 | margin: 0px 2%;
12 | width: 96%;
13 | `;
14 |
15 | const CaseNumber = styled.div`
16 | color: #4d00b4;
17 | font-size: 14px;
18 | font-weight: 500;
19 | line-height: 16px;
20 | `;
21 | const CaseStatus = styled.div`
22 | float: right;
23 | font-size: 14px;
24 | font-weight: 500;
25 | line-height: 16px;
26 | `;
27 | const CaseSummaryBody = styled.div`
28 | border-radius: 12px;
29 | box-shadow: 0px 6px 36px #bc9cff;
30 | padding: 13px 21px;
31 | `;
32 | const CaseSummaryText = styled.div`
33 | color: black;
34 | font-size: 20px;
35 | font-weight: 500;
36 | line-height: 23px;
37 | margin: 26px 0px;
38 | `;
39 |
40 | const CaseSummaryCard = ({ dispute }) => (
41 |
42 |
43 |
44 |
45 |
46 | {`Case #${dispute.ID}`}
47 |
48 |
49 | {dispute.statusDiv}
50 |
51 |
52 | {dispute.metaEvidence ? dispute.metaEvidence.title : ""}
53 |
54 |
55 |
56 | );
57 |
58 | export default withRouter(CaseSummaryCard);
59 |
60 | CaseSummaryCard.propTypes = {
61 | dispute: t.any.isRequired,
62 | };
63 |
--------------------------------------------------------------------------------
/src/components/cases-list-card.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { drizzleReactHooks } from "@drizzle/react-plugin";
3 | import { ReactComponent as Hourglass } from "../assets/images/hourglass.svg";
4 | import ListItem from "./list-item";
5 | import TimeAgo from "./time-ago";
6 | import TitledListCard from "./titled-list-card";
7 | import styled from "styled-components/macro";
8 | import { VIEW_ONLY_ADDRESS } from "../bootstrap/dataloader";
9 | import useChainId from "../hooks/use-chain-id";
10 | import useGetDraws from "../hooks/use-get-draws";
11 |
12 | const { useDrizzle, useDrizzleState } = drizzleReactHooks;
13 |
14 | const StyledDiv = styled.div`
15 | background: whitesmoke;
16 | padding: 30px 22px;
17 | position: relative;
18 | text-align: center;
19 | `;
20 | const StyledDeadlineDiv = styled.div`
21 | font-weight: medium;
22 | `;
23 | const StyledTimeAgo = styled(TimeAgo)`
24 | font-size: 24px;
25 | font-weight: bold;
26 | `;
27 | const StyledHourglass = styled(Hourglass)`
28 | position: absolute;
29 | right: 13px;
30 | top: 13px;
31 | `;
32 | const CasesListCard = () => {
33 | const { useCacheCall } = useDrizzle();
34 | const drizzleState = useDrizzleState((drizzleState) => ({
35 | account: drizzleState.accounts[0] || VIEW_ONLY_ADDRESS,
36 | }));
37 | const chainId = useChainId();
38 | const draws = useGetDraws(chainId, `address: "${drizzleState.account}"`);
39 |
40 | const disputes = useCacheCall(["KlerosLiquid"], (call) =>
41 | draws
42 | ? Object.values(
43 | draws.reduce((acc, d) => {
44 | acc[d.disputeID] = d;
45 | return acc;
46 | }, {})
47 | ).reduce(
48 | (acc, d) => {
49 | acc.total++;
50 | const dispute = call("KlerosLiquid", "disputes", d.disputeID);
51 | if (dispute)
52 | if (dispute.period === "1" || dispute.period === "2") {
53 | const dispute2 = call("KlerosLiquid", "getDispute", d.disputeID);
54 | if (dispute2)
55 | if (Number(d.appeal) === dispute2.votesLengths.length - 1) {
56 | const vote = call("KlerosLiquid", "getVote", d.disputeID, d.appeal, d.voteID);
57 | if (vote)
58 | if (vote.voted) acc.active++;
59 | else {
60 | acc.votePending++;
61 | const subcourt = call("KlerosLiquid", "getSubcourt", dispute.subcourtID);
62 | if (subcourt) {
63 | const deadline = new Date(
64 | (Number(dispute.lastPeriodChange) + Number(subcourt.timesPerPeriod[dispute.period])) * 1000
65 | );
66 | if (!acc.deadline || deadline < acc.deadline) acc.deadline = deadline;
67 | } else acc.loading = true;
68 | }
69 | else acc.loading = true;
70 | } else acc.active++;
71 | else acc.loading = true;
72 | } else acc[dispute.period === "4" ? "executed" : "active"]++;
73 | else acc.loading = true;
74 | return acc;
75 | },
76 | { active: 0, executed: 0, loading: false, total: 0, votePending: 0 }
77 | )
78 | : { loading: true }
79 | );
80 |
81 | return (
82 |
83 | Vote Pending
84 | Active
85 | Closed
86 | {disputes.deadline && (
87 |
88 | Next voting deadline
89 | {disputes.deadline}
90 |
91 |
92 | )}
93 |
94 | );
95 | };
96 |
97 | export default CasesListCard;
98 |
--------------------------------------------------------------------------------
/src/components/collapsable-card.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import styled from 'styled-components'
3 | import { ReactComponent as ArrowUp } from '../assets/images/arrow-up.svg'
4 | import { ReactComponent as ArrowDown } from '../assets/images/arrow-down.svg'
5 |
6 | const CollapsableCard = styled.div`
7 | background: #fbf9fe;
8 | border: 1px solid #d09cff;
9 | border-radius: 3px;
10 | box-sizing: border-box;
11 | margin-bottom: 28px;
12 | padding: 0;
13 | `
14 | const StyledHeader = styled.div`
15 | background: #4d00b4;
16 | border-radius: 3px;
17 | color: white;
18 | display: flex;
19 | justify-content: space-between;
20 | padding: 15px 30px;
21 | `
22 |
23 | const DetailsArea = ({ title, children, headerSpacing = false }) => {
24 | const [showInputs, setShowInputs] = useState(false)
25 |
26 | return (
27 |
28 | setShowInputs(!showInputs)}
30 | style={{ cursor: 'pointer' }}
31 | >
32 | {title}
33 | {showInputs ? : }
34 |
35 | {showInputs ? (
36 | {children}
37 | ) : (
38 | ''
39 | )}
40 |
41 | )
42 | }
43 |
44 | export default DetailsArea
45 |
--------------------------------------------------------------------------------
/src/components/court-list-item.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import styled from "styled-components/macro";
4 | import { drizzleReactHooks } from "@drizzle/react-plugin";
5 | import { VIEW_ONLY_ADDRESS } from "../bootstrap/dataloader";
6 | import ETHAmount from "./eth-amount";
7 | import ListItem from "./list-item";
8 |
9 | const { useDrizzle, useDrizzleState } = drizzleReactHooks;
10 |
11 | const CourtListItem = ({ ID, name, onClick }) => {
12 | const { useCacheCall } = useDrizzle();
13 | const drizzleState = useDrizzleState((drizzleState) => ({
14 | account: drizzleState.accounts[0] || VIEW_ONLY_ADDRESS,
15 | }));
16 |
17 | const stake = useCacheCall("KlerosLiquidExtraViews", "stakeOf", drizzleState.account, ID);
18 |
19 | return (
20 |
23 | PNK
24 | >
25 | }
26 | onClick={onClick}
27 | >
28 | {name}
29 |
30 | );
31 | };
32 |
33 | CourtListItem.propTypes = {
34 | ID: t.number.isRequired,
35 | name: t.string.isRequired,
36 | onClick: t.func.isRequired,
37 | };
38 |
39 | export default CourtListItem;
40 |
41 | const StyledListItem = styled(ListItem)`
42 | .ant-list-item-extra-wrap {
43 | width: 100%;
44 | display: flex;
45 | justify-content: space-between;
46 | align-items: center;
47 | gap: 16px;
48 | }
49 |
50 | .ant-list-item-extra {
51 | position: initial;
52 | right: initial;
53 | top: initial;
54 | transform: initial;
55 | margin-left: auto;
56 | text-align: right;
57 | }
58 | `;
59 |
--------------------------------------------------------------------------------
/src/components/courts-list-card.js:
--------------------------------------------------------------------------------
1 | import { drizzleReactHooks } from "@drizzle/react-plugin";
2 | import React from "react";
3 | import t from "prop-types";
4 | import ListItem from "./list-item";
5 | import TitledListCard from "./titled-list-card";
6 | import CourtListItem from "./court-list-item";
7 | import { useDataloader, VIEW_ONLY_ADDRESS } from "../bootstrap/dataloader";
8 |
9 | const { useDrizzle, useDrizzleState } = drizzleReactHooks;
10 |
11 | const CourtsListCard = ({ apy, setActiveSubcourtID }) => {
12 | const { useCacheCall } = useDrizzle();
13 | const drizzleState = useDrizzleState((drizzleState) => ({
14 | account: drizzleState.accounts[0] || VIEW_ONLY_ADDRESS,
15 | }));
16 | const loadPolicy = useDataloader.loadPolicy();
17 | const juror = useCacheCall("KlerosLiquidExtraViews", "getJuror", drizzleState.account);
18 | let names = useCacheCall(
19 | ["PolicyRegistry"],
20 | (call) =>
21 | juror &&
22 | [...new Set(juror.subcourtIDs)]
23 | .filter((ID) => ID !== "0")
24 | .map((ID) => String(ID - 1))
25 | .map((ID) => {
26 | const policy = call("PolicyRegistry", "policies", ID);
27 | if (policy !== undefined) {
28 | const policyJSON = loadPolicy(policy);
29 | if (policyJSON)
30 | return {
31 | name: policyJSON.name,
32 | ID,
33 | };
34 | }
35 | return undefined;
36 | })
37 | );
38 |
39 | const loading = !names || names.some((n) => n === undefined);
40 | return (
41 |
42 | {!loading &&
43 | (names.length > 0 ? (
44 | names.map((n) => (
45 | setActiveSubcourtID(Number(n.ID))}
50 | />
51 | ))
52 | ) : (
53 | <>
54 | You are not staked in any courts.
55 | >
56 | ))}
57 |
58 | );
59 | };
60 |
61 | CourtsListCard.propTypes = {
62 | apy: t.number.isRequired,
63 | setActiveSubcourtID: t.func.isRequired,
64 | onClick: t.func.isRequired,
65 | };
66 |
67 | export default CourtsListCard;
68 |
--------------------------------------------------------------------------------
/src/components/error-boundary.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { withScope, captureException } from "@sentry/browser";
5 |
6 | export default class ErrorBoundary extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = { hasError: false, error: null, errorInfo: null };
10 | }
11 |
12 | static getDerivedStateFromError = () => {
13 | return { hasError: true };
14 | };
15 | componentDidCatch(error, errorInfo) {
16 | withScope((scope) => {
17 | Object.keys(errorInfo).forEach((key) => {
18 | scope.setExtra(key, errorInfo[key]);
19 | });
20 | captureException(error);
21 | });
22 | this.setState({
23 | error: error,
24 | errorInfo: errorInfo,
25 | });
26 | }
27 | render() {
28 | const { hasError } = this.state;
29 | const { fallback: Fallback, children } = this.props;
30 | return hasError ? : children;
31 | }
32 | }
33 |
34 | ErrorBoundary.propTypes = {
35 | children: PropTypes.node,
36 | fallback: PropTypes.any,
37 | };
38 |
--------------------------------------------------------------------------------
/src/components/error-fallback/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Alert, Button } from "antd";
3 | import PropTypes from "prop-types";
4 |
5 | import ErrorFallbackLayout from "../../containers/error-fallback";
6 | import styled from "styled-components/macro";
7 |
8 | const DefaultFallback = ({ onClick }) => {
9 | return (
10 |
11 |
15 | Report Feedback
16 |
17 | }
18 | message="An unexpected error occurred in Athens."
19 | type="error"
20 | />
21 |
22 | );
23 | };
24 | export default DefaultFallback;
25 |
26 | DefaultFallback.propTypes = {
27 | onClick: PropTypes.func,
28 | };
29 |
30 | const StyledAlert = styled(Alert)`
31 | left: 50%;
32 | position: fixed;
33 | top: 50%;
34 | transform: translate(-50%, -50%);
35 | .ant-alert-message {
36 | margin-bottom: 20px;
37 | }
38 | `;
39 |
40 | const StyledButton = styled(Button)`
41 | width: 100%;
42 | height: 50px;
43 | font-size: 20px;
44 | `;
45 |
--------------------------------------------------------------------------------
/src/components/error-fallback/switch-chain.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button, Modal } from "antd";
3 | import styled from "styled-components/macro";
4 | import { chainIdToNetworkShortName } from "../../helpers/networks";
5 | import ErrorFallbackLayout from "../../containers/error-fallback";
6 | import requestSwitchChain from "../../api/side-chain/request-switch-chain";
7 |
8 | export default function SwitchChainFallback() {
9 | const [selectedId, setSelectedId] = useState(null);
10 | const [isLoading, setIsLoading] = useState(false);
11 |
12 | const handleSwitchNetwork = async () => {
13 | setIsLoading(true);
14 | try {
15 | await requestSwitchChain(window.ethereum, selectedId);
16 | setIsLoading(false);
17 | } catch (error) {
18 | setIsLoading(false);
19 | }
20 | };
21 | return (
22 |
23 |
24 | Unsupported Network
25 | Unsupported network detected. Please select a network to connect
26 |
27 | {Object.entries(chainIdToNetworkShortName).map(([key, value]) => (
28 | {
32 | setSelectedId(key);
33 | }}
34 | >
35 | {value}
36 |
37 | ))}
38 |
39 |
40 | Switch Network
41 |
42 |
43 |
44 | );
45 | }
46 |
47 | const StyledModal = styled(Modal)`
48 | .ant-modal-content {
49 | border-radius: 0.5rem;
50 | width: 80%;
51 | left: 50%;
52 | transform: translateX(-50%);
53 | }
54 | .ant-modal-body {
55 | border-radius: 0.5rem;
56 | }
57 |
58 | h2 {
59 | margin: 0 0 5px 0;
60 | font-size: 14px;
61 | }
62 |
63 | p {
64 | color: #3c424299;
65 | font-size: 11px;
66 | }
67 | @media only screen and (min-width: 768px) {
68 | .ant-modal-content {
69 | width: 30rem;
70 | left: 50%;
71 | transform: translateX(-50%);
72 | }
73 |
74 | h2 {
75 | margin: 0 0 5px 0;
76 | font-size: 20px;
77 | }
78 |
79 | p {
80 | color: #3c424299;
81 | font-size: 14px;
82 | }
83 | }
84 | `;
85 |
86 | const StyledButton = styled(Button)`
87 | width: 100%;
88 | height: 50px;
89 | font-size: 14px;
90 | @media only screen and (min-width: 768px) {
91 | font-size: 20px;
92 | }
93 | `;
94 |
95 | const StyledWrapper = styled.ul`
96 | height: 15rem;
97 | padding: 0;
98 | margin: 0 0 20px 0;
99 | overflow: auto;
100 | list-style-type: none;
101 | border: 1px solid #ededed;
102 | background: #e9dfed73;
103 | border-radius: 10px;
104 | ::-webkit-scrollbar {
105 | display: none;
106 | }
107 | `;
108 |
109 | const StyledLine = styled.li`
110 | padding: 10px 14px;
111 | margin: 0 0 8px 0;
112 | font-size: 14px;
113 | border-radius: 10px;
114 | cursor: pointer;
115 | background: ${(props) => props.isActive && "#999cff"};
116 | color: ${(props) => props.isActive && "white"};
117 |
118 | &:hover {
119 | background: ${(props) => !props.isActive && "#e3cfee"};
120 | color: ${(props) => !props.isActive && "#4d50b4"};
121 | }
122 |
123 | @media only screen and (min-width: 768px) {
124 | padding: 10px 14px;
125 | margin: 0 0 8px 0;
126 | font-size: 18px;
127 | border-radius: 10px;
128 | cursor: pointer;
129 | background: ${(props) => props.isActive && "#999cff"};
130 | color: ${(props) => props.isActive && "white"};
131 |
132 | &:hover {
133 | background: ${(props) => !props.isActive && "#e3cfee"};
134 | color: ${(props) => !props.isActive && "#4d50b4"};
135 | }
136 | }
137 | `;
138 |
--------------------------------------------------------------------------------
/src/components/eth-address.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import styled from "styled-components/macro";
4 | import useChainId from "../hooks/use-chain-id";
5 | import { getAddressUrl } from "../helpers/block-explorer";
6 | import Identicon from "./identicon";
7 |
8 | export default function ETHAddress({ address, showIdenticon, withLink }) {
9 | const addressDisplay = `${address.slice(0, 6)}...${address.slice(address.length - 4)}`;
10 |
11 | const content = showIdenticon ? (
12 |
13 |
14 | {addressDisplay}
15 |
16 | ) : (
17 | addressDisplay
18 | );
19 |
20 | return withLink ? {content} : content;
21 | }
22 |
23 | ETHAddress.propTypes = {
24 | address: t.string.isRequired,
25 | showIdenticon: t.bool,
26 | withLink: t.bool,
27 | };
28 |
29 | ETHAddress.defaultProps = {
30 | showIdenticon: false,
31 | withLink: true,
32 | };
33 |
34 | function ETHAddressLink({ address, children }) {
35 | const chainId = useChainId();
36 | const url = getAddressUrl(chainId, address);
37 |
38 | return (
39 |
40 | {children}
41 |
42 | );
43 | }
44 |
45 | ETHAddressLink.propTypes = {
46 | address: t.string.isRequired,
47 | children: t.node.isRequired,
48 | };
49 |
50 | const StyledIdentity = styled.div`
51 | display: flex;
52 | gap: 8px;
53 | align-items: center;
54 | `;
55 |
--------------------------------------------------------------------------------
/src/components/eth-amount.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import t from "prop-types";
3 | import { Skeleton } from "antd";
4 | import styled from "styled-components/macro";
5 | import Web3 from "web3";
6 | import { getTokenSymbol } from "../helpers/get-token-symbol";
7 | import { drizzleReactHooks } from "@drizzle/react-plugin";
8 |
9 | const { useDrizzleState } = drizzleReactHooks;
10 | const { fromWei } = Web3.utils;
11 |
12 | export default function ETHAmount({ amount, decimals, tokenSymbol }) {
13 | const chainId = useDrizzleState((ds) => ds.web3.networkId);
14 |
15 | let finalDecimals = decimals;
16 | const chainTokenSymbol = useMemo(() => getTokenSymbol(chainId), [chainId]);
17 | const calculatedTokenSymbol = useMemo(() => getTokenSymbol(chainId, tokenSymbol), [chainId, tokenSymbol]);
18 |
19 | if (chainTokenSymbol === "xDAI" && tokenSymbol === true) {
20 | finalDecimals = 2;
21 | }
22 |
23 | if (amount === null) {
24 | return ;
25 | }
26 |
27 | const numericValue = Number(
28 | fromWei(typeof amount === "number" ? formatNumber(amount, { decimals: 0 }) : String(amount))
29 | );
30 | const value = formatNumber(finalDecimals === 0 ? Math.trunc(numericValue) : numericValue, {
31 | decimals: finalDecimals,
32 | useGrouping: true,
33 | });
34 |
35 | return tokenSymbol === true ? (
36 | <>
37 | {value} {chainTokenSymbol}
38 | >
39 | ) : tokenSymbol === false ? (
40 | value
41 | ) : React.isValidElement(tokenSymbol) ? (
42 | <>
43 | {value} {tokenSymbol}
44 | >
45 | ) : (
46 | <>
47 | {value} {calculatedTokenSymbol}
48 | >
49 | );
50 | }
51 |
52 | ETHAmount.propTypes = {
53 | amount: t.oneOfType([t.string.isRequired, t.number.isRequired, t.object.isRequired]),
54 | decimals: t.number,
55 | tokenSymbol: t.oneOfType([t.bool, t.string.isRequired, t.element.isRequired]),
56 | };
57 |
58 | ETHAmount.defaultProps = {
59 | amount: null,
60 | decimals: 0,
61 | tokenSymbol: false,
62 | };
63 |
64 | const formatNumber = (number, { locale = [], useGrouping = false, decimals = 2 } = {}) =>
65 | new Intl.NumberFormat(locale, {
66 | useGrouping,
67 | minimumFractionDigits: decimals,
68 | maximumFractionDigits: decimals,
69 | }).format(number);
70 |
71 | const SkeletonTitleProps = { width: 30 };
72 |
73 | const StyledSkeleton = styled(Skeleton)`
74 | display: inline;
75 |
76 | .ant-skeleton-title {
77 | margin: -3px 0;
78 | }
79 | `;
80 |
--------------------------------------------------------------------------------
/src/components/evidence-timeline.js:
--------------------------------------------------------------------------------
1 | import { Col, Icon, Row } from "antd";
2 | import React from "react";
3 | // eslint-disable-next-line no-restricted-imports
4 | import styled from "styled-components";
5 | import EvidenceCard from "./evidence-card";
6 |
7 | const StyledHeaderCol = styled(Col)`
8 | color: #4d00b4;
9 | font-size: 18px;
10 | font-weight: 500;
11 | line-height: 21px;
12 | `;
13 | const StyledDividerCol = styled(Col)`
14 | border-right: 1px solid #4d00b4;
15 | height: 30px;
16 | width: 50%;
17 | `;
18 | const EventDiv = styled.div`
19 | background: #4d00b4;
20 | border-radius: 300px;
21 | color: white;
22 | font-size: 12px;
23 | font-weight: 500;
24 | line-height: 14px;
25 | margin-left: auto;
26 | margin-right: auto;
27 | padding: 10px 0;
28 | text-align: center;
29 | width: 135px;
30 | `;
31 | const ScrollText = styled.div`
32 | color: #009aff;
33 | cursor: pointer;
34 | font-size: 14px;
35 | font-weight: 500;
36 | line-height: 16px;
37 | text-align: right;
38 |
39 | @media (max-width: 500px) {
40 | text-align: center;
41 | }
42 | `;
43 | const StyledEvidenceTimelineArea = styled.div`
44 | padding: 35px 10%;
45 | `;
46 |
47 | const getRulingText = (ruling, metaEvidence) => {
48 | if (!ruling) {
49 | return "Jurors refused to make a ruling";
50 | }
51 | const rulingIndex = Number(ruling) - 1;
52 | const rulingTitle = metaEvidence.rulingOptions?.titles?.[rulingIndex];
53 | return `Jurors ruled: ${rulingTitle || ruling}`;
54 | };
55 |
56 | // eslint-disable-next-line react/prop-types
57 | const EvidenceTimeline = ({ evidence = [], metaEvidence = {}, ruling = null, chainId = 1 }) => {
58 | // Sort so most recent is first
59 | const sortedEvidence = evidence.sort((a, b) => {
60 | if (a.submittedAt > b.submittedAt) return -1;
61 | else if (a.submittedAt < b.submittedAt) return 1;
62 |
63 | return 0;
64 | });
65 |
66 | if (sortedEvidence.length === 0) return null;
67 |
68 | return (
69 |
70 |
71 | Latest
72 |
73 | {ruling && {getRulingText(ruling, metaEvidence)} }
74 |
75 | {
78 | const _bottomRow = document.getElementById("scroll-bottom");
79 | _bottomRow.scrollIntoView();
80 | }}
81 | >
82 | Scroll to Bottom
83 |
84 |
85 | {sortedEvidence.map((_evidence, i) => (
86 |
87 |
88 |
89 |
90 |
91 |
92 | ))}
93 |
94 |
95 |
96 |
97 | Start
98 |
99 | Dispute Created
100 |
101 | {
104 | const _bottomRow = document.getElementById("scroll-top");
105 | _bottomRow.scrollIntoView();
106 | }}
107 | >
108 | Scroll to Top
109 |
110 |
111 |
112 | );
113 | };
114 |
115 | export default EvidenceTimeline;
116 |
--------------------------------------------------------------------------------
/src/components/footer/footer.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | display: flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | background: #4d00b4;
6 | color: white;
7 | height: 60px;
8 | font-size: 14px;
9 | padding: 18px 50px;
10 | width: 100%;
11 | margin-top: -60px;
12 | }
13 |
14 | @media (max-width: 768px) {
15 | .footer {
16 | display: block;
17 | }
18 | }
19 |
20 | .footer a {
21 | color: white;
22 | text-decoration: none;
23 | }
24 |
25 | .footer-left {
26 | display: flex;
27 | text-align: center;
28 | align-items: center;
29 | justify-content: flex-start;
30 | width: 33%;
31 | }
32 |
33 | .footer-center {
34 | display: inline-block;
35 | text-align: center;
36 | width: 33%;
37 | }
38 |
39 | .footer-right {
40 | display: flex;
41 | justify-content: flex-end;
42 | align-items: center;
43 | width: 33%;
44 | }
45 |
46 | .footer-right-help {
47 | width: 40%;
48 | display: inline-block;
49 | }
50 |
51 | .footer-right-help-text {
52 | display: inline-block;
53 | padding-right: 10px;
54 | }
55 |
56 | .footer-right-help-icon {
57 | width: 18px;
58 | position: relative;
59 | }
60 |
61 | @media (max-width: 1130px) {
62 | .footer-right-help {
63 | width: 100%;
64 | }
65 | }
66 |
67 | .footer-right-icons {
68 | display: flex;
69 | justify-content: center;
70 | align-items: center;
71 | gap: 10px;
72 | }
73 |
74 | @media (max-width: 1130px) {
75 | .footer-right-icons {
76 | display: flex;
77 | }
78 | }
79 |
80 | @media (max-width: 858px) {
81 | .footer {
82 | display: flex;
83 | justify-content: center;
84 | align-items: center;
85 | text-align: center;
86 | flex-direction: column;
87 | height: auto;
88 | }
89 |
90 | .footer-left,
91 | .footer-center,
92 | .footer-right {
93 | justify-content: center;
94 | width: 100%;
95 | padding-bottom: 10px;
96 | }
97 |
98 | .footer-right-help {
99 | padding-bottom: 10px;
100 | }
101 |
102 | .footer-right {
103 | flex-wrap: wrap;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/components/footer/footer.scss:
--------------------------------------------------------------------------------
1 | .footer {
2 | background: #4D00B4;
3 | color: white;
4 | height: 60px;
5 | font-size: 14px;
6 | padding: 18px 25px;
7 | width: 100%;
8 | margin-top: -60px;
9 |
10 | @media (max-width: 768px) {
11 | display: none;
12 | }
13 |
14 | a {
15 | color: white;
16 | text-decoration: none;
17 | }
18 |
19 | &-left {
20 | display: inline-block;
21 | text-align: left;
22 | width: 33%;
23 | }
24 | &-center {
25 | display: inline-block;
26 | text-align: center;
27 | width: 33%;
28 | }
29 | &-right {
30 | display: inline-block;
31 | width: 33%;
32 | padding-left: 10%;
33 |
34 | &-help {
35 | width: 40%;
36 | display: inline-block;
37 |
38 | &-text {
39 | display: inline-block;
40 | padding-right: 10px;
41 | }
42 |
43 | &-icon {
44 | width: 18px;
45 | position: relative;
46 | top: 4px;
47 | }
48 |
49 | @media (max-width: 1130px) {
50 | width: 100%;
51 | }
52 | }
53 |
54 | &-icons {
55 | display: inline-block;
56 | width: 60%;
57 | position: relative;
58 | top: 3px;
59 | a {
60 | padding-right: 10px;
61 | }
62 |
63 | @media (max-width: 1130px) {
64 | display: none;
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/footer/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ReactComponent as Question } from "../../assets/images/question-circle.svg";
3 | import { ReactComponent as Twitter } from "../../assets/images/x.svg";
4 | import { ReactComponent as Github } from "../../assets/images/github.svg";
5 | import { ReactComponent as Ghost } from "../../assets/images/ghost.svg";
6 | import { ReactComponent as LinkedIn } from "../../assets/images/linkedin.svg";
7 | import { ReactComponent as Telegram } from "../../assets/images/telegram.svg";
8 | import "./footer.css";
9 |
10 | const Footer = () => (
11 |
12 |
15 |
Kleros Court
16 |
41 |
42 | );
43 |
44 | export default Footer;
45 |
--------------------------------------------------------------------------------
/src/components/hint.js:
--------------------------------------------------------------------------------
1 | import { ReactComponent as Info } from '../assets/images/info.svg'
2 | import PropTypes from 'prop-types'
3 | import React from 'react'
4 | import { Tooltip } from 'antd'
5 | import styled from 'styled-components/macro'
6 |
7 | const StyledDiv = styled.div`
8 | font-style: italic;
9 | text-align: center;
10 | `
11 | const StyledTitleDiv = styled(StyledDiv)`
12 | font-weight: bold;
13 | `
14 | const Hint = ({ description, title }) => (
15 |
20 | {title}
21 | {description}
22 | >
23 | }
24 | >
25 |
26 |
27 | )
28 |
29 | Hint.propTypes = {
30 | description: PropTypes.node.isRequired,
31 | title: PropTypes.node.isRequired
32 | }
33 |
34 | export default Hint
35 |
--------------------------------------------------------------------------------
/src/components/identicon.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import { Avatar } from "antd";
4 | import ReactBlockies from "react-blockies";
5 | import styled from "styled-components/macro";
6 | import { VIEW_ONLY_ADDRESS } from "../bootstrap/dataloader";
7 |
8 | export default function Identicon({ address, large, className }) {
9 | return (
10 |
11 | {address !== VIEW_ONLY_ADDRESS ? (
12 |
13 | ) : (
14 | U
15 | )}
16 |
17 | );
18 | }
19 |
20 | Identicon.propTypes = {
21 | address: t.string,
22 | large: t.bool,
23 | className: t.string,
24 | };
25 |
26 | Identicon.defaultProps = {
27 | address: VIEW_ONLY_ADDRESS,
28 | className: "",
29 | large: false,
30 | };
31 |
32 | const StyledDiv = styled.div`
33 | height: 24px;
34 | width: 24px;
35 | line-height: 100%;
36 | `;
37 |
38 | const StyledAvatar = styled(Avatar)`
39 | && {
40 | width: 24px;
41 | height: 24px;
42 | line-height: 24px;
43 | }
44 | `;
45 |
46 | const StyledReactBlockies = styled(ReactBlockies)`
47 | border-radius: ${({ large }) => (large ? "4" : "16")}px;
48 | `;
49 |
--------------------------------------------------------------------------------
/src/components/list-item.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import styled from "styled-components/macro";
4 | import { List } from "antd";
5 |
6 | const StyledListItem = styled(List.Item)`
7 | color: #4004a3;
8 | font-weight: bold;
9 | padding-left: 19px;
10 | padding-right: 19px;
11 | position: relative;
12 |
13 | display: flex;
14 | gap: 16px;
15 |
16 | ::last-child {
17 | margin-left: auto;
18 | }
19 | `;
20 |
21 | const ListItem = ({ children, extra, ...rest }) => (
22 | {extra}} {...rest}>
23 | {children}
24 |
25 | );
26 |
27 | ListItem.propTypes = {
28 | children: t.node,
29 | extra: t.node,
30 | };
31 |
32 | export default ListItem;
33 |
--------------------------------------------------------------------------------
/src/components/multi-transaction-status.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import clsx from "clsx";
4 | import styled from "styled-components/macro";
5 | import { Icon, Steps } from "antd";
6 | import { TxState } from "../helpers/transactions";
7 | import { getTransactionUrl } from "../helpers/block-explorer";
8 |
9 | export default function MultiTransactionStatus({ chainId, error, transactions }) {
10 | const currentPendingTx = transactions.findIndex((tx) => !isTxComplete(tx));
11 | const allComplete = transactions.every(isTxComplete);
12 |
13 | const currentStep = currentPendingTx !== -1 ? currentPendingTx : allComplete ? transactions.length : 0;
14 | const status = error ? "error" : allComplete ? "finish" : "process";
15 |
16 | return (
17 |
18 | {transactions.map((tx, index) => {
19 | return (
20 |
27 | {tx.txHash ? (
28 |
29 | View details
30 |
31 | ) : null}
32 | {currentStep === index && !error ? : null}
33 |
34 | ) : null
35 | }
36 | description={
37 | error && currentStep === index ? (
38 |
39 | {error.message}
40 | {error.cause ? ({error.cause.message}) : null}
41 |
42 | ) : null
43 | }
44 | />
45 | );
46 | })}
47 |
48 | );
49 | }
50 |
51 | MultiTransactionStatus.propTypes = {
52 | chainId: t.number.isRequired,
53 | error: t.instanceOf(Error),
54 | transactions: t.arrayOf(
55 | t.shape({
56 | state: t.oneOf([...Object.values(TxState), "skipped"]).isRequired,
57 | txHash: t.string,
58 | title: t.node.isRequired,
59 | })
60 | ).isRequired,
61 | };
62 |
63 | const isTxComplete = (tx) => tx.state === TxState.Mined || tx.state === "skipped";
64 |
65 | const StyledSteps = styled(Steps)``;
66 |
67 | const StyledStep = styled(Steps.Step)`
68 | &.skipped {
69 | opacity: 0.5;
70 |
71 | .ant-steps-item-title {
72 | text-decoration: line-through;
73 | }
74 | }
75 | `;
76 |
77 | const StyledSubTitle = styled.span`
78 | display: inline-flex;
79 | align-items: center;
80 | gap: 8px;
81 | `;
82 |
83 | const StyledErrorDescription = styled.div`
84 | > span {
85 | display: block;
86 | }
87 |
88 | .cause-message {
89 | font-size: 12px;
90 | }
91 | `;
92 |
--------------------------------------------------------------------------------
/src/components/network-status.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import clsx from "clsx";
4 | import styled from "styled-components/macro";
5 | import { Badge, Skeleton } from "antd";
6 | import { drizzleReactHooks } from "@drizzle/react-plugin";
7 | import useChainId from "../hooks/use-chain-id";
8 | import { chainIdToNetworkShortName } from "../helpers/networks";
9 |
10 | const { useDrizzleState } = drizzleReactHooks;
11 |
12 | export default function NetworkStatus({ className }) {
13 | const { status } = useDrizzleState((drizzleState) => ({
14 | status: drizzleState.web3.status,
15 | }));
16 | const chainId = useChainId();
17 |
18 | return chainId ? (
19 |
24 | ) : (
25 |
26 | );
27 | }
28 |
29 | NetworkStatus.propTypes = {
30 | className: t.string,
31 | };
32 |
33 | const networkStatusToBadgeStatus = {
34 | initialized: "success",
35 | initializing: "warning",
36 | failed: "danger",
37 | };
38 |
39 | const StyledBadge = styled(Badge)`
40 | white-space: nowrap;
41 |
42 | &.initialized {
43 | color: #52c41a;
44 | }
45 |
46 | &.initializing {
47 | color: #faad14;
48 | }
49 |
50 | &.failed {
51 | color: #f5222d;
52 | }
53 |
54 | .ant-badge-status-dot {
55 | width: 8px;
56 | height: 8px;
57 | }
58 |
59 | .ant-badge-status-text {
60 | color: #fff;
61 | }
62 | `;
63 |
64 | const StyledSkeleton = styled(Skeleton)`
65 | && {
66 | display: block;
67 | width: 80px;
68 | margin: 0;
69 |
70 | .ant-skeleton-content {
71 | display: block;
72 | }
73 |
74 | .ant-skeleton-title {
75 | margin: 0;
76 | border-radius: 4px;
77 | background-image: linear-gradient(
78 | 90deg,
79 | rgba(242, 242, 242, 0.25) 25%,
80 | rgba(230, 230, 230, 0.25) 37%,
81 | rgba(242, 242, 242, 0.25) 63%
82 | ) !important;
83 | }
84 | }
85 | `;
86 |
--------------------------------------------------------------------------------
/src/components/otc-card.js:
--------------------------------------------------------------------------------
1 | import { Button, Col, Row } from 'antd'
2 | import React from 'react'
3 | import styled from 'styled-components'
4 | import stakeImg from '../assets/images/stake-kleros-logo.png'
5 | import { ReactComponent as Hexagon } from '../assets/images/hexagon.svg'
6 |
7 | const StyledOTCCard = styled.div`
8 | background: linear-gradient(111.31deg, #4d00b4 19.55%, #6500b4 40.51%);
9 | border-radius: 12px;
10 | color: #ffffff;
11 | min-height: 100px;
12 | padding: 24px;
13 | width: 100%;
14 | `
15 | const StyledPrefixDiv = styled.div`
16 | left: 29px;
17 | position: absolute;
18 | top: 48px;
19 | transform: translate(-50%, -50%);
20 |
21 | @media (max-width: 991px) {
22 | top: 33px;
23 | }
24 | `
25 | const IconDiv = styled.div`
26 | margin-top: 15px;
27 | `
28 | const ButtonDiv = styled.div`
29 | margin-top: 30px;
30 | @media (max-width: 991px) {
31 | margin-bottom: 30px;
32 | }
33 | `
34 | const StyledTextSmall = styled.div`
35 | font-size: 14px;
36 | font-weight: 500;
37 | margin: 0px;
38 | text-align: left;
39 | `
40 | const StyledTextLarge = styled.div`
41 | font-size: 24px;
42 | margin: 0px;
43 | text-align: left;
44 | `
45 | const StyledButton = styled(Button)`
46 | margin-top: '12px';
47 | max-width: '150px';
48 | `
49 |
50 | const OTCCard = () => (
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | If you're interested in acquiring PNK tokens OTC, get in touch
65 |
66 |
67 | In order to ensure fair token distribution, tokens are sold to
68 | buyers at prices reflected by the market.
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Get in touch here
77 |
78 |
79 |
80 |
81 |
82 |
83 | )
84 |
85 | export default OTCCard
86 |
--------------------------------------------------------------------------------
/src/components/percentage-circle.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { CircularProgressbar, buildStyles } from 'react-circular-progressbar'
3 | import 'react-circular-progressbar/dist/styles.css'
4 |
5 | const nf = new Intl.NumberFormat('en-US', { style: 'percent' })
6 |
7 | const PercentageCircle = ({ percent }) => (
8 |
18 | )
19 |
20 | export default PercentageCircle
21 |
--------------------------------------------------------------------------------
/src/components/performance-card.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { drizzleReactHooks } from "@drizzle/react-plugin";
3 | import PercentageCircle from "./percentage-circle";
4 | import { Spin } from "antd";
5 | import TitledListCard from "./titled-list-card";
6 | import styled from "styled-components/macro";
7 | import { VIEW_ONLY_ADDRESS } from "../bootstrap/dataloader";
8 | import useChainId from "../hooks/use-chain-id";
9 | import useGetShifts from "../hooks/use-get-shifts";
10 |
11 | const { useDrizzleState } = drizzleReactHooks;
12 |
13 | const StyledDiv = styled.div`
14 | align-items: center;
15 | display: flex;
16 | justify-content: center;
17 | padding: 30px;
18 |
19 | & > div {
20 | flex: 1;
21 | }
22 | `;
23 | const StyledText = styled.div`
24 | color: #4004a3;
25 | font-size: 18px;
26 | margin-top: 15px;
27 | text-align: center;
28 | `;
29 | const StyledGraphContainer = styled.div`
30 | margin: auto;
31 | margin-bottom: 15px;
32 | width: 40%;
33 | `;
34 |
35 | const PNKStatsListCard = () => {
36 | const drizzleState = useDrizzleState((drizzleState) => ({
37 | account: drizzleState.accounts[0] || VIEW_ONLY_ADDRESS,
38 | }));
39 |
40 | let loadingData = true;
41 | const chainId = useChainId();
42 | const rewards = useGetShifts(chainId, `address: "${drizzleState.account}"`);
43 |
44 | if (rewards) loadingData = false;
45 |
46 | let totalCases = 0;
47 | let coherentVote = 0;
48 | let lastSeenDispute = -1;
49 | if (!loadingData)
50 | for (const reward of rewards)
51 | if (Number(reward.disputeID) !== lastSeenDispute) {
52 | totalCases++;
53 | lastSeenDispute = Number(reward.disputeID);
54 | if (Number(reward.ETHAmount) > 0) coherentVote++;
55 | }
56 |
57 | const percent = !loadingData && rewards.length ? (coherentVote / totalCases).toFixed(2) * 100 : 0;
58 |
59 | return (
60 |
61 |
62 |
63 |
64 |
65 |
66 | Cases Coherent
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | export default PNKStatsListCard;
74 |
--------------------------------------------------------------------------------
/src/components/pie-chart.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react'
2 | import PropTypes from 'prop-types'
3 | import ReactMinimalPieChart from 'react-minimal-pie-chart'
4 | import styled from 'styled-components/macro'
5 |
6 | const StyledDiv = styled.div`
7 | padding: 65px 45px 45px;
8 | position: relative;
9 | `
10 | const StyledTitleDiv = styled.div`
11 | font-weight: medium;
12 | left: 50%;
13 | position: absolute;
14 | top: 20px;
15 | transform: translateX(-50%);
16 | `
17 | const StyledTooltipDiv = styled.div.attrs(({ x, y }) => ({
18 | style: { left: `${x}px`, top: `${y - 60}px` }
19 | }))`
20 | background: white;
21 | border: 1px solid black;
22 | border-radius: 3px;
23 | padding: 10px 8px;
24 | position: absolute;
25 | white-space: nowrap;
26 | z-index: 1;
27 | `
28 | const PieChart = ({ data, title }) => {
29 | const [state, setState] = useState({ dataIndex: null, x: null, y: null })
30 | const onMouseMove = useCallback(e => {
31 | const bounds = e.currentTarget.getBoundingClientRect()
32 | const x = e.clientX - bounds.left
33 | const y = e.clientY - bounds.top
34 | setState(state => ({ ...state, x, y }))
35 | }, [])
36 | const onMouseOut = useCallback(
37 | () => setState(state => ({ ...state, dataIndex: null })),
38 | []
39 | )
40 | const onMouseOver = useCallback(
41 | (_, __, dataIndex) => setState(state => ({ ...state, dataIndex })),
42 | []
43 | )
44 | const inPie = state.dataIndex !== null
45 | return (
46 |
47 | {title}
48 |
54 | {inPie && (
55 |
60 | {data[state.dataIndex].tooltip}
61 |
62 | )}
63 |
64 |
65 | )
66 | }
67 |
68 | PieChart.propTypes = {
69 | data: PropTypes.arrayOf(
70 | PropTypes.shape({
71 | tooltip: PropTypes.node.isRequired,
72 | value: PropTypes.number.isRequired
73 | }).isRequired
74 | ).isRequired,
75 | title: PropTypes.node.isRequired
76 | }
77 |
78 | export default PieChart
79 |
--------------------------------------------------------------------------------
/src/components/pnk-exchanges-card.js:
--------------------------------------------------------------------------------
1 | import { ReactComponent as Bitfinex } from "../assets/images/bitfinex.svg";
2 | import DeversiFi from "../assets/images/deversifi.png";
3 | import { ReactComponent as Uniswap } from "../assets/images/uniswap.svg";
4 | import { ReactComponent as Loopring } from "../assets/images/loopring.svg";
5 | import KyberSwap from "../assets/images/kyber.png";
6 | import { ReactComponent as Guardarian } from "../assets/images/guardarian.svg";
7 | import { ReactComponent as OneInch } from "../assets/images/OneInch.svg";
8 | import Paraswap from "../assets/images/paraswap.jpg";
9 | import { ReactComponent as Balancer } from "../assets/images/balancer.svg";
10 | import Sushiswap from "../assets/images/sushiswap.png";
11 | import { ReactComponent as GateIO } from "../assets/images/gateio.svg";
12 | import { ReactComponent as OKEX } from "../assets/images/okex.svg";
13 | import React from "react";
14 | import styled from "styled-components/macro";
15 |
16 | const StyledExchangeCard = styled.a`
17 | align-items: center;
18 | justify-content: center;
19 | background: white;
20 | border-radius: 12px;
21 | box-shadow: 0px 6px 36px #bc9cff;
22 | display: flex;
23 | height: 40px;
24 | padding: 40px 26px;
25 |
26 | svg,
27 | img {
28 | vertical-align: middle;
29 | max-width: 80%;
30 | max-height: 70px;
31 | }
32 | `;
33 |
34 | const StyledExchangeSection = styled.div`
35 | display: grid;
36 | grid-gap: 20px;
37 | grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
38 | `;
39 |
40 | const Exchanges = [
41 | {
42 | logo: ,
43 | link: "https://app.uniswap.org/#/swap?inputCurrency=ETH&outputCurrency=0x93ed3fbe21207ec2e8f2d3c3de6e058cb73bc04d",
44 | },
45 | {
46 | logo: ,
47 | link: "https://app.sushi.com/swap?inputCurrency=ETH&outputCurrency=0x93ed3fbe21207ec2e8f2d3c3de6e058cb73bc04d",
48 | },
49 | {
50 | logo: ,
51 | link: "https://balancer.exchange/",
52 | },
53 | {
54 | logo: ,
55 | link: "https://kyberswap.com/#/swap",
56 | },
57 | {
58 | logo: ,
59 | link: "https://app.deversifi.com/",
60 | },
61 | {
62 | logo: ,
63 | link: "https://loopring.org/",
64 | },
65 | {
66 | logo: ,
67 | link: "https://paraswap.io/",
68 | },
69 | {
70 | logo: ,
71 | link: "https://1inch.exchange/",
72 | },
73 | {
74 | logo: ,
75 | link: "https://www.bitfinex.com/t/PNKETH",
76 | },
77 | {
78 | logo: ,
79 | link: "https://www.gate.io/trade/PNK_USDT",
80 | },
81 | {
82 | logo: ,
83 | link: "https://www.okex.com/markets/spot-info/pnk-usdt",
84 | },
85 | {
86 | logo: ,
87 | link: "https://guardarian.com/",
88 | },
89 | ];
90 |
91 | // eslint-disable-next-line react/display-name
92 | export default () => (
93 |
94 | {Exchanges.map((exchange, i) => (
95 |
96 | {exchange.logo}
97 |
98 | ))}
99 |
100 | );
101 |
--------------------------------------------------------------------------------
/src/components/pnk-xdai-exchanges-card.js:
--------------------------------------------------------------------------------
1 | import { ReactComponent as Swapr } from "../assets/images/swapr.svg";
2 | import Omnibridge from "../assets/images/omnibridge.png";
3 | import React from "react";
4 | import styled from "styled-components/macro";
5 |
6 | const StyledExchangeCard = styled.a`
7 | align-items: center;
8 | justify-content: center;
9 | background: white;
10 | border-radius: 12px;
11 | box-shadow: 0px 6px 36px #bc9cff;
12 | display: flex;
13 | height: 40px;
14 | padding: 40px 26px;
15 |
16 | svg,
17 | img {
18 | vertical-align: middle;
19 | max-width: 80%;
20 | max-height: 70px;
21 | }
22 | `;
23 |
24 | const StyledExchangeSection = styled.div`
25 | display: grid;
26 | grid-gap: 20px;
27 | grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
28 | `;
29 |
30 | const Exchanges = [
31 | {
32 | logo: ,
33 | link: "https://omni.gnosischain.com/bridge?from=1&to=100&token=0x93ed3fbe21207ec2e8f2d3c3de6e058cb73bc04d",
34 | },
35 | {
36 | logo: ,
37 | link: "https://swapr.eth.limo/#/swap?inputCurrency=0x37b60f4e9a31a64ccc0024dce7d0fd07eaa0f7b3&chainId=100",
38 | },
39 | ];
40 |
41 | // eslint-disable-next-line react/display-name
42 | export default () => (
43 |
44 | {Exchanges.map((exchange, i) => (
45 |
46 | {exchange.logo}
47 |
48 | ))}
49 |
50 | );
51 |
--------------------------------------------------------------------------------
/src/components/required-chain-id-modal.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import styled from "styled-components/macro";
4 | import { drizzleReactHooks } from "@drizzle/react-plugin";
5 | import { Button, Icon, Modal, Typography } from "antd";
6 | import { chainIdToNetworkName } from "../helpers/networks";
7 | import { isSupportedSideChain, requestSwitchChain } from "../api/side-chain";
8 | import { useClearRequiredChainId, useSetRequiredChainId } from "./required-chain-id-gateway";
9 | import useAccount from "../hooks/use-account";
10 |
11 | const { useDrizzle } = drizzleReactHooks;
12 |
13 | export default function RequiredChainIdModal({ requiredChainId }) {
14 | const account = useAccount();
15 | const hasAccount = !!account;
16 |
17 | const networkName = chainIdToNetworkName[requiredChainId];
18 | const cleanRequiredChainId = useClearRequiredChainId();
19 |
20 | return (
21 |
28 | ) : null
29 | }
30 | title={`Switch to ${networkName}`}
31 | onCancel={() => cleanRequiredChainId()}
32 | >
33 | {hasAccount ? (
34 | isSupportedSideChain(requiredChainId) ? (
35 |
36 | To go to the Kleros Side-Chain Court, please click the button bellow or switch to {networkName} on MetaMask.
37 |
38 | ) : (
39 |
40 | To go back to the main Kleros Court, please switch to {networkName} on MetaMask.
41 |
42 | )
43 | ) : (
44 |
45 | You need an{" "}
46 |
47 | Ethereum Wallet
48 | {" "}
49 | to be able to switch to {networkName}.
50 |
51 | )}
52 |
53 | );
54 | }
55 |
56 | RequiredChainIdModal.propTypes = {
57 | requiredChainId: t.number,
58 | };
59 |
60 | function SwitchNetworkButton({ requiredChainId }) {
61 | const { drizzle } = useDrizzle();
62 |
63 | const setRequiredChainId = useSetRequiredChainId();
64 |
65 | const switchChain = async () => {
66 | if (isSupportedSideChain(requiredChainId)) {
67 | try {
68 | await requestSwitchChain(drizzle.web3.currentProvider, requiredChainId);
69 | } catch (err) {
70 | console.warn("Failed to request the switch to the side-chain:", err);
71 | /**
72 | * If the call fails, it means that it's not supported.
73 | * This happens for the native Ethereum Mainnet and well-known testnets,
74 | * such as Ropsten and Kovan. Apparently this is due to security reasons.
75 | * @see { @link https://docs.metamask.io/guide/rpc-api.html#wallet-addethereumchain }
76 | */
77 | setRequiredChainId(requiredChainId);
78 | }
79 | } else if (requiredChainId) {
80 | setRequiredChainId(requiredChainId);
81 | }
82 | };
83 |
84 | return (
85 |
86 | Go to Court on {chainIdToNetworkName[requiredChainId]}
87 |
88 |
89 | );
90 | }
91 |
92 | SwitchNetworkButton.propTypes = {
93 | requiredChainId: t.number.isRequired,
94 | };
95 |
96 | const StyledModal = styled(Modal)`
97 | .ant-modal-header {
98 | border: none;
99 | }
100 |
101 | .ant-modal-title {
102 | font-size: 36px;
103 | line-height: 1.33;
104 | text-align: center;
105 | color: #4d00b4;
106 | }
107 |
108 | .ant-modal-footer {
109 | padding: 16px 24px;
110 | border: none;
111 | text-align: center;
112 | }
113 | `;
114 |
115 | const StyledExplainer = styled(Typography.Paragraph)`
116 | text-align: center;
117 | `;
118 |
--------------------------------------------------------------------------------
/src/components/scroll-bar.js:
--------------------------------------------------------------------------------
1 | import { Col, Icon, Row } from 'antd'
2 | import React from 'react'
3 | import styled from 'styled-components'
4 |
5 | const CenteredScrollText = styled(Col)`
6 | color: #4d00b4;
7 | font-size: 18px;
8 | font-weight: 500;
9 | line-height: 21px;
10 | text-align: center;
11 | `
12 |
13 | const ScrollBar = ({ currentOption, numberOfOptions, setOption }) => (
14 |
15 |
16 |
18 | setOption(currentOption === 0 ? numberOfOptions : currentOption - 1)
19 | }
20 | style={{ color: '#4D00B4' }}
21 | type="left"
22 | />
23 |
24 |
25 | {currentOption + 1} / {numberOfOptions + 1}
26 |
27 |
28 |
30 | setOption(currentOption === numberOfOptions ? 0 : currentOption + 1)
31 | }
32 | style={{ color: '#4D00B4' }}
33 | type="right"
34 | />
35 |
36 |
37 | )
38 |
39 | export default ScrollBar
40 |
--------------------------------------------------------------------------------
/src/components/side-chain/announcement-banner.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createPortal } from "react-dom";
3 | import styled from "styled-components/macro";
4 | import { Alert } from "antd";
5 | import createPersistedState from "use-persisted-state";
6 |
7 | const useXDaiCourtAlert = createPersistedState("@kleros/court/alert/xdai-court");
8 |
9 | const bannerRoot = document.querySelector("#banner-root");
10 |
11 | export default function AnnouncementBanner({ message = "Kleros Court is now available on Gnosis Chain." }) {
12 | const [isAlertVisible, setAlertVisible] = useXDaiCourtAlert(true);
13 |
14 | return isAlertVisible
15 | ? createPortal(
16 | setAlertVisible(false)} message={message} />,
17 | bannerRoot
18 | )
19 | : null;
20 | }
21 |
22 | const StyledAlert = styled(Alert)`
23 | background-color: #9013fe;
24 |
25 | .ant-alert-message {
26 | display: block;
27 | color: white;
28 | text-align: center;
29 | }
30 |
31 | .anticon-close {
32 | color: rgba(255, 255, 255, 0.85);
33 |
34 | :focus,
35 | :hover {
36 | color: rgba(255, 255, 255, 1);
37 | filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.5));
38 | }
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/src/components/switch-network-message.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import styled from "styled-components/macro";
4 | import { Button, Icon, Typography } from "antd";
5 | import { drizzleReactHooks } from "@drizzle/react-plugin";
6 | import { isSupportedSideChain, SideChainApiProvider, useSideChainApi } from "../api/side-chain";
7 | import { chainIdToNetworkName } from "../helpers/networks";
8 |
9 | const { useDrizzle } = drizzleReactHooks;
10 |
11 | export default function SwitchNetworkMessage({ title, wantedChainId, showSwitchButton }) {
12 | const isSupported = isSupportedSideChain(wantedChainId);
13 | const { drizzle } = useDrizzle();
14 |
15 | return (
16 |
17 | {title}
18 | Please switch to {chainIdToNetworkName[wantedChainId]} to proceed.
19 | {showSwitchButton && isSupported ? (
20 |
21 |
22 |
23 | ) : null}
24 |
25 | );
26 | }
27 |
28 | SwitchNetworkMessage.propTypes = {
29 | title: t.node.isRequired,
30 | wantedChainId: t.node.isRequired,
31 | showSwitchButton: t.bool,
32 | };
33 |
34 | SwitchNetworkMessage.defaultProps = {
35 | showSwitchButton: false,
36 | };
37 |
38 | function SwitchNetworkButton() {
39 | const tokenBridgeApi = useSideChainApi();
40 |
41 | return (
42 | {
45 | try {
46 | tokenBridgeApi.destination.switchChain();
47 | } catch {
48 | // do nothing..
49 | }
50 | }}
51 | >
52 | Switch Network
53 |
54 |
55 | );
56 | }
57 |
58 | const StyledTitle = styled(Typography.Title)``;
59 | const StyledParagraph = styled(Typography.Paragraph)``;
60 |
61 | const StyledSwitchNetworkMessage = styled.div`
62 | text-align: center;
63 |
64 | ${StyledParagraph} {
65 | color: rgba(0, 0, 0, 0.45);
66 | }
67 | `;
68 |
--------------------------------------------------------------------------------
/src/components/time-ago.js:
--------------------------------------------------------------------------------
1 | import 'react-time-ago/Tooltip.css'
2 | import JavascriptTimeAgo from 'javascript-time-ago'
3 | import en from 'javascript-time-ago/locale/en'
4 |
5 | JavascriptTimeAgo.locale(en)
6 |
7 | export { default } from 'react-time-ago'
8 |
--------------------------------------------------------------------------------
/src/components/top-banner.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import styled from "styled-components/macro";
4 | import { Card, Col, Row } from "antd";
5 |
6 | export default function TopBanner({ description, extra, title, extraLong }) {
7 | return extraLong ? (
8 |
9 |
10 |
11 | {title}
12 |
13 |
14 | {description}
15 |
16 |
17 | {extra}
18 |
19 |
20 |
21 | ) : (
22 |
23 |
24 |
25 | {title}
26 | {description}
27 |
28 | {extra}
29 |
30 |
31 | );
32 | }
33 |
34 | TopBanner.propTypes = {
35 | description: t.node,
36 | extra: t.node,
37 | title: t.string.isRequired,
38 | extraLong: t.bool,
39 | };
40 |
41 | TopBanner.defaultProps = {
42 | description: null,
43 | extra: null,
44 | extraLong: false,
45 | };
46 |
47 | const StyledCard = styled(Card)`
48 | background: linear-gradient(270deg, #f2e3ff 22.92%, #ffffff 76.25%);
49 | box-shadow: 0px 18px 24px -6px rgba(188, 156, 255, 0.4);
50 | color: #4d00b4;
51 | margin: 0 -9.375vw 28px -9.375vw;
52 | min-height: 88px;
53 | padding: 0px 9.375vw;
54 |
55 | .ant-card-body {
56 | padding: 24px 0;
57 | }
58 | `;
59 |
60 | const StyledDescriptionCol = styled(Col)`
61 | margin-bottom: 8px;
62 |
63 | @media (max-width: 640px) {
64 | display: none;
65 | }
66 | `;
67 |
68 | const StyledTitleCol = styled(Col)`
69 | font-size: 24px;
70 | font-weight: bold;
71 | `;
72 | const StyledExtraCol = styled(Col)`
73 | align-items: center;
74 | display: flex;
75 | flex-direction: row;
76 | grid-gap: 8px 0;
77 |
78 | @media (max-width: 500px) {
79 | flex-direction: column;
80 | }
81 | `;
82 |
83 | const StyledTitleRow = styled(Row)`
84 | display: flex;
85 | gap: 24px;
86 | justify-content: space-between;
87 | margin: 0;
88 | `;
89 |
--------------------------------------------------------------------------------
/src/components/welcome-card.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { Card } from 'antd'
3 | import PropTypes from 'prop-types'
4 | import { fluidRange } from 'polished'
5 | import styled from 'styled-components/macro'
6 |
7 | const StyledCard = styled(Card)`
8 | z-index: 1;
9 |
10 | .ant-card-body {
11 | align-items: center;
12 | display: flex;
13 | flex-wrap: wrap;
14 | justify-content: center;
15 | }
16 | `
17 | const StyledCardGrid = styled(Card.Grid)`
18 | align-items: center;
19 | display: flex;
20 | flex: 1 1 250px;
21 | justify-content: center;
22 | ${fluidRange([
23 | {
24 | fromSize: '190px',
25 | prop: 'height',
26 | toSize: '320px'
27 | },
28 | {
29 | fromSize: '20px',
30 | prop: 'paddingRight',
31 | toSize: '50px'
32 | },
33 | {
34 | fromSize: '20px',
35 | prop: 'paddingLeft',
36 | toSize: '50px'
37 | }
38 | ])}
39 | position: relative;
40 | `
41 | const StyledIconCardGrid = styled(StyledCardGrid)`
42 | background: white;
43 | border-radius: 12px;
44 | max-width: 393px;
45 | `
46 | const StyledDiv = styled.div`
47 | bottom: 25px;
48 | position: absolute;
49 | `
50 | const StyledTextCardGrid = styled(StyledCardGrid)`
51 | border-radius: 0 12px 12px 0;
52 | color: white;
53 | font-weight: medium;
54 | margin: 0 0 0 -20px;
55 | max-width: 413px;
56 | ${fluidRange({
57 | fromSize: '24px',
58 | prop: 'fontSize',
59 | toSize: '48px'
60 | })}
61 | z-index: -1;
62 |
63 | @media (max-width: 649px) {
64 | border-radius: 0 0 12px 12px;
65 | height: auto;
66 | margin: -20px 0 0;
67 | max-width: 393px;
68 | padding: calc(10% + 20px) 0 10%;
69 | }
70 | `
71 | const WelcomeCard = ({ icon, text, version }) => {
72 | const [show, setShow] = useState(!localStorage.getItem('shownWelcome'))
73 | useEffect(() => {
74 | if (show) {
75 | localStorage.setItem('shownWelcome', true)
76 | const timeout = setTimeout(() => setShow(false), 5000)
77 | return () => clearTimeout(timeout)
78 | }
79 | }, [])
80 | return (
81 | show && (
82 |
83 |
84 | {icon}
85 | {version}
86 |
87 |
88 | {text}
89 |
90 |
91 | )
92 | )
93 | }
94 |
95 | WelcomeCard.propTypes = {
96 | icon: PropTypes.node.isRequired,
97 | text: PropTypes.string.isRequired,
98 | version: PropTypes.string.isRequired
99 | }
100 |
101 | export default WelcomeCard
102 |
--------------------------------------------------------------------------------
/src/containers/404.js:
--------------------------------------------------------------------------------
1 | import { Spin } from "antd";
2 | import { ReactComponent as Acropolis } from "../assets/images/acropolis.svg";
3 | import PropTypes from "prop-types";
4 | import React from "react";
5 | import styled from "styled-components/macro";
6 |
7 | export default function C404({ Web3 }) {
8 | return (
9 |
10 |
11 |
12 | {Web3 ? "Loading Court" : "404"}
13 |
14 | {Web3 ? "Fetching information about the Court, please wait." : "Something went wrong in Athens!"}
15 |
16 | {Web3 && }
17 |
18 | {Web3
19 | ? "Please make sure you have your wallet unlocked on Mainnet, Gnosis Chain, Sepolia or Chiado. If you don't have a wallet, we recommend you install MetaMask on desktop and Trust on mobile."
20 | : "The greek gods are not available at the moment."}
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | C404.propTypes = {
28 | Web3: PropTypes.bool,
29 | };
30 |
31 | C404.defaultProps = {
32 | Web3: false,
33 | };
34 |
35 | const StyledDiv = styled.div`
36 | display: flex;
37 | flex-direction: column;
38 | min-height: ${(props) => (props.Web3 ? "100vh" : "calc(100vh - 64px - 56px)")};
39 | ${(props) => !props.Web3 && "margin: 0 -9.375vw -62px;"}
40 | `;
41 |
42 | const StyledAcropolis = styled(Acropolis)`
43 | height: auto;
44 | width: 100%;
45 | `;
46 |
47 | const StyledInfoDiv = styled.div`
48 | flex: 1;
49 | padding: 0 9.375vw 62px;
50 | text-align: center;
51 | `;
52 |
53 | const Styled404Div = styled.div`
54 | font-size: 88px;
55 | font-weight: bold;
56 | line-height: 112px;
57 | `;
58 |
59 | const StyledMessageLine2 = styled.div`
60 | font-size: 24px;
61 | `;
62 |
63 | const StyledMessageLine3 = styled.div`
64 | font-size: 16px;
65 | margin-top: 25px;
66 | `;
67 |
--------------------------------------------------------------------------------
/src/containers/convert-pnk/convert-pnk.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components/macro";
3 | import { drizzleReactHooks } from "@drizzle/react-plugin";
4 | import { Divider, Spin } from "antd";
5 | import { getCounterPartyChainId, isSupportedMainChain, SideChainApiProvider } from "../../api/side-chain";
6 | import { getReadOnlyWeb3 } from "../../bootstrap/web3";
7 | import TopBanner from "../../components/top-banner";
8 | import useChainId from "../../hooks/use-chain-id";
9 | import C404 from "../404";
10 | import ConvertStPnk from "./convert-stpnk-card";
11 |
12 | const { useDrizzle } = drizzleReactHooks;
13 |
14 | export default function ConvertPnkWrapper() {
15 | const chainId = useChainId();
16 | const { drizzle } = useDrizzle();
17 |
18 | const web3Provider = React.useMemo(() => {
19 | try {
20 | return isSupportedMainChain(chainId)
21 | ? getReadOnlyWeb3({ chainId: getCounterPartyChainId(chainId) })
22 | : drizzle.web3.currentProvider;
23 | } catch (err) {
24 | console.warn("Failed to get a provider for the side-chain API:", err);
25 | return null;
26 | }
27 | }, [drizzle.web3.currentProvider, chainId]);
28 |
29 | return web3Provider ? (
30 | (
33 |
43 | )}
44 | >
45 |
46 |
47 | ) : (
48 |
49 | );
50 | }
51 |
52 | function ConvertPnk() {
53 | return (
54 | <>
55 |
56 |
57 |
58 | >
59 | );
60 | }
61 |
62 | const StyledDivider = styled(Divider)`
63 | border: none !important;
64 | background: none !important;
65 | `;
66 |
--------------------------------------------------------------------------------
/src/containers/convert-pnk/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./convert-pnk";
2 |
--------------------------------------------------------------------------------
/src/containers/convert-pnk/switch-chain-button.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import t from "prop-types";
3 | import styled from "styled-components/macro";
4 | import { Button } from "antd";
5 | import { drizzleReactHooks } from "@drizzle/react-plugin";
6 | import { requestSwitchChain } from "../../api/side-chain";
7 | import { useSetRequiredChainId } from "../../components/required-chain-id-gateway";
8 | import { chainIdToNetworkShortName } from "../../helpers/networks";
9 |
10 | const { useDrizzle } = drizzleReactHooks;
11 |
12 | export default function SwitchChainButton({ destinationChainId }) {
13 | const switchChain = useSwitchChain(destinationChainId);
14 |
15 | return (
16 |
17 |
18 | Switch to {chainIdToNetworkShortName[destinationChainId]}
19 |
20 |
21 | );
22 | }
23 |
24 | SwitchChainButton.propTypes = {
25 | destinationChainId: t.number.isRequired,
26 | };
27 |
28 | function useSwitchChain(destinationChainId) {
29 | const { drizzle } = useDrizzle();
30 | const setRequiredChainId = useSetRequiredChainId();
31 |
32 | return React.useCallback(async () => {
33 | try {
34 | await requestSwitchChain(drizzle.web3.currentProvider, destinationChainId);
35 | } catch (err) {
36 | // 4001 is the MetaMask error code when users deny permission.
37 | if (err.code !== 4001) {
38 | /**
39 | * If the call fails with any other reason, then set the global required chain ID.
40 | */
41 | console.warn("Failed to request the switch to the side-chain:", err);
42 | setRequiredChainId(destinationChainId);
43 | }
44 | }
45 | }, [destinationChainId, setRequiredChainId, drizzle.web3.currentProvider]);
46 | }
47 |
48 | const StyledWrapper = styled.div`
49 | display: flex;
50 | justify-content: center;
51 | padding: 1rem 0;
52 | `;
53 |
--------------------------------------------------------------------------------
/src/containers/error-fallback.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import styled from "styled-components/macro";
5 | import { ReactComponent as Acropolis } from "../assets/images/acropolis.svg";
6 |
7 | const ErrorFallbackLayout = ({ children }) => {
8 | return (
9 |
10 |
11 | {children}
12 |
13 | );
14 | };
15 | export default ErrorFallbackLayout;
16 |
17 | ErrorFallbackLayout.propTypes = {
18 | children: PropTypes.node,
19 | };
20 |
21 | const StyledDiv = styled.div`
22 | height: 100vh;
23 | width: 100vw;
24 | `;
25 |
26 | const StyledAcropolis = styled(Acropolis)`
27 | height: auto;
28 | width: 100%;
29 | `;
30 |
--------------------------------------------------------------------------------
/src/helpers/array.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns an array containing a range of numbers. O(n).
3 | *
4 | * It can be called with 1 or 2 parameters like:
5 | *
6 | * `range(length)` - Will produce an array of size `length` with items from `0` to `length - 1`.
7 | * `range(start, end)` - Will produce an array of size `end - start` with items from `start` to `end - 1`.
8 | *
9 | * Examples:
10 | *
11 | * - `range(10) // --> [0, 1, ..., 9]`
12 | * - `range(0, 10) // --> [0, 1, ..., 9] (same as range(10))`
13 | * - `range(10, 20) // --> [10, 11, ..., 19]`
14 | *
15 | * @param {number} lengthOrStart The length of the array or its starting value (inclusive).
16 | * @param {number|undefined} end [OPTIONAL] If ommited, `lengthOrStart` is treated like it's the length of the array. If provided it will determine its ending value (exclusive).
17 | * @return {number[]} The range array.
18 | */
19 | export const range = (lengthOrStart, end) => {
20 | const length = end === undefined ? lengthOrStart : end - lengthOrStart;
21 | const start = end === undefined ? 0 : lengthOrStart;
22 |
23 | return Array(length)
24 | .fill()
25 | .map((_, i) => start + i);
26 | };
27 |
28 | /**
29 | * Returns an array containing arrays for the binary permutation of `length` bits. O(2^n).
30 | *
31 | * Each item in the nested array is the value of the bit in the n-th position.
32 | *
33 | * Example:
34 | *
35 | * - `binaryPermutation(3)` will produce:
36 | *
37 | * [ [ 0, 0, 0 ],
38 | * [ 0, 0, 1 ],
39 | * [ 0, 1, 0 ],
40 | * [ 0, 1, 1 ],
41 | * [ 1, 0, 0 ],
42 | * [ 1, 0, 1 ],
43 | * [ 1, 1, 0 ],
44 | * [ 1, 1, 1 ] ]
45 | *
46 | * @param {number} length The number of bits to generate the permutation.
47 | * @return {(0|1)[][]} The binary permutation of `length` bits.
48 | */
49 | export const binaryPermutations = (length) => {
50 | const permutations = [];
51 | for (let i = 0; i < Math.pow(2, length); i++) {
52 | permutations.push(i.toString(2).padStart(length, "0").split("").map(Number));
53 | }
54 | return permutations;
55 | };
56 |
--------------------------------------------------------------------------------
/src/helpers/block-explorer.js:
--------------------------------------------------------------------------------
1 | const chainIdToBaseUrl = {
2 | 1: "https://etherscan.io",
3 | 100: "https://gnosisscan.io",
4 | 10200: "https://blockscout.chiadochain.net",
5 | 11155111: "https://sepolia.etherscan.io",
6 | };
7 |
8 | export const getBaseUrl = (chainId) => chainIdToBaseUrl[chainId] ?? chainIdToBaseUrl[1];
9 |
10 | export const getAddressUrl = (chainId, address) => `${getBaseUrl(chainId)}/address/${address}`;
11 |
12 | export const getTransactionUrl = (chainId, txHash) => `${getBaseUrl(chainId)}/tx/${txHash}`;
13 |
--------------------------------------------------------------------------------
/src/helpers/block-numbers.js:
--------------------------------------------------------------------------------
1 | const KlerosLiquidBlockNumbers = {
2 | 1: process.env.REACT_APP_KLEROS_LIQUID_BLOCK_NUMBER,
3 | 100: process.env.REACT_APP_KLEROS_LIQUID_XDAI_BLOCK_NUMBER,
4 | 10200: process.env.REACT_APP_KLEROS_LIQUID_CHIADO_BLOCK_NUMBER,
5 | 11155111: process.env.REACT_APP_KLEROS_LIQUID_SEPOLIA_BLOCK_NUMBER,
6 | };
7 |
8 | export const getKlerosLiquidBlockNumber = (chainId) => KlerosLiquidBlockNumbers[chainId];
9 |
--------------------------------------------------------------------------------
/src/helpers/create-error.js:
--------------------------------------------------------------------------------
1 | export default function createError(message, cause, context) {
2 | const causeMixin = cause
3 | ? {
4 | cause: {
5 | value: cause,
6 | enumerable: true,
7 | },
8 | }
9 | : undefined;
10 |
11 | const contextMixin = context
12 | ? {
13 | context: {
14 | value: context,
15 | enumerable: true,
16 | },
17 | }
18 | : undefined;
19 |
20 | if (!causeMixin && !contextMixin) {
21 | throw new Error(message);
22 | }
23 |
24 | return Object.create(new Error(message), { ...causeMixin, ...contextMixin });
25 | }
26 |
--------------------------------------------------------------------------------
/src/helpers/get-token-symbol.js:
--------------------------------------------------------------------------------
1 | import t, { PropTypes } from "prop-types";
2 |
3 | export function getTokenSymbol(chainId, token) {
4 | if (token) {
5 | return chainIdToTokenSuffix[chainId] && chainIdToTokenSuffix[chainId][token]
6 | ? chainIdToTokenSuffix[chainId][token]
7 | : token;
8 | }
9 |
10 | const suffix = chainIdToSuffix[chainId] || "ETH";
11 | return suffix;
12 | }
13 |
14 | getTokenSymbol.propTypes = {
15 | chainId: PropTypes.number.isRequired,
16 | token: t.string,
17 | };
18 |
19 | const chainIdToSuffix = {
20 | 1: "ETH",
21 | 100: "xDAI",
22 | 10200: "xDAI",
23 | 11155111: "sETH",
24 | };
25 |
26 | const chainIdToTokenSuffix = {
27 | 1: { PNK: "PNK", xPNK: "<>" },
28 | 100: { PNK: "stPNK", xPNK: "PNK" },
29 | 10200: { PNK: "stPNK", xPNK: "PNK" },
30 | 11155111: { PNK: "PNK", xPNK: "<>" },
31 | };
32 |
--------------------------------------------------------------------------------
/src/helpers/networks.js:
--------------------------------------------------------------------------------
1 | export const chainIdToNetworkName = {
2 | 1: "Ethereum Mainnet",
3 | 100: "Gnosis Chain",
4 | 10200: "Gnosis Testnet Chiado",
5 | 11155111: "Ethereum Testnet Sepolia",
6 | };
7 |
8 | export const chainIdToNetworkShortName = {
9 | 1: "Mainnet",
10 | 100: "Gnosis Chain",
11 | 10200: "Chiado",
12 | 11155111: "Sepolia",
13 | };
14 |
--------------------------------------------------------------------------------
/src/helpers/rewards.js:
--------------------------------------------------------------------------------
1 | import { BigNumber, ethers } from "ethers";
2 | import PNKAbi from "../assets/contracts/pinakion.json";
3 | import Web3 from "web3";
4 |
5 | function getTarget() {
6 | let months;
7 | const start = new Date(2023, 11, 1); // When KIP-66 started
8 | const initialTarget = 0.28; // initial staking target for KIP-66
9 | const now = new Date();
10 | // add 1% per month since start date of kip66 with max 50%
11 | months = (now.getFullYear() - start.getFullYear()) * 12;
12 | months -= start.getMonth();
13 | months += now.getMonth();
14 | months = months <= 0 ? 0 : months;
15 | const target = initialTarget + months * 0.01;
16 | return target > 0.5 ? 0.5 : target;
17 | }
18 |
19 | function getPreviousMonthAndYear(date = new Date()) {
20 | const currentMonth = date.getMonth();
21 | const currentYear = date.getFullYear();
22 | let month, year;
23 | // month starts with 0
24 | if (currentMonth === 0) {
25 | // month is 12 and the previous year
26 | month = 12;
27 | year = currentYear - 1;
28 | } else {
29 | // no need to do month -1 because starts in zero.
30 | month = currentMonth;
31 | year = currentYear;
32 | }
33 | return {
34 | month: month < 10 ? `0${month}` : month.toString(),
35 | year: year.toString(),
36 | };
37 | }
38 |
39 | export async function getLastMonthReward() {
40 | let { month, year } = getPreviousMonthAndYear();
41 | // fetch the script where the court get the rewads. There is a list of IPFS files with the rewards there.
42 | const res = await fetch("https://raw.githubusercontent.com/kleros/court/master/src/components/claim-modal.js");
43 | const test = await res.text();
44 | // extract the ipfs files from the court code of the last month (for gnosis and mainnet)
45 | let reg = new RegExp(`"(?[a-zA-Z0-9]*)/snapshot-(${year}-${month}|xdai-snapshot-${year}-${month}).json"`, "g");
46 | let matches = Array.from(test.matchAll(reg));
47 | let urls = matches.map((r) => `https://cdn.kleros.link/ipfs/${r.groups.cid}/snapshot-${year}-${month}.json`);
48 | if (urls.length === 0) {
49 | // try with previous month if no urls where found.
50 | let { month: prevMonth, year: prevYear } = getPreviousMonthAndYear(new Date(Number(year), Number(month) - 1, 1));
51 | reg = new RegExp(
52 | `"(?[a-zA-Z0-9]*)/snapshot-(${prevYear}-${prevMonth}|xdai-snapshot-${prevYear}-${prevMonth}).json"`,
53 | "g"
54 | );
55 | matches = Array.from(test.matchAll(reg));
56 | urls = matches.map((r) => `https://cdn.kleros.link/ipfs/${r.groups.cid}/snapshot-${prevYear}-${prevMonth}.json`);
57 | }
58 | let lastMonthReward = BigNumber.from(0);
59 | // read the reward from the ipfs file and add it.
60 | for (const url of urls) {
61 | const res = await fetch(url);
62 | const json = await res.json();
63 | lastMonthReward = lastMonthReward.add(ethers.BigNumber.from(json.totalClaimable.hex));
64 | }
65 | return Number(ethers.utils.formatEther(lastMonthReward.toString()));
66 | }
67 |
68 | export async function getStakingReward(chainId, totalStaked) {
69 | const web3 = new Web3("https://eth.llamarpc.com");
70 | if (!totalStaked) return 0;
71 | // This address is outdated, was copied from klerosboard
72 | const COOP_MULTISIG = "0x67a57535b11445506a9e340662cd0c9755e5b1b4";
73 | const pnkContract = new web3.eth.Contract(PNKAbi.abi, process.env.REACT_APP_PINAKION_ADDRESS);
74 | const totalSupply = await pnkContract.methods.totalSupply().call();
75 | const balanceOfCoop = await pnkContract.methods.balanceOf(COOP_MULTISIG).call();
76 |
77 | // Comment below prevents build-time error, related to eslint
78 | /* global BigInt */
79 | const actualSupply = Number(ethers.utils.formatUnits(String(BigInt(totalSupply) - BigInt(balanceOfCoop)), "ether"));
80 | const chainRewardPercentage = chainId === "100" ? 0.1 : 0.9; // Reward splitted by court
81 | const lastMonthReward = await getLastMonthReward();
82 | const target = getTarget();
83 | const currentStakedRate = totalStaked / actualSupply;
84 | const chainReward = chainRewardPercentage * lastMonthReward * (1 + target - currentStakedRate);
85 | return (chainReward / totalStaked) * 12 * 100;
86 | }
87 |
--------------------------------------------------------------------------------
/src/helpers/transactions.js:
--------------------------------------------------------------------------------
1 | import createError from "./create-error";
2 |
3 | export async function* promiEventToAsyncGenerator(promiEvent) {
4 | const buffer = {
5 | state: TxState.None,
6 | };
7 |
8 | const txHashPromise = new Promise((resolve) => {
9 | promiEvent.once("transactionHash", (txHash) => {
10 | buffer.txHash = txHash;
11 | resolve({
12 | ...buffer,
13 | state: TxState.Pending,
14 | });
15 | });
16 | });
17 |
18 | const errorPromise = new Promise((_, reject) => {
19 | promiEvent.once("error", (err) => {
20 | reject(createError(err.message, null, { txHash: buffer.txHash }));
21 | });
22 | });
23 |
24 | const receiptPromise = new Promise((resolve) => {
25 | promiEvent.once("receipt", (receipt) => {
26 | buffer.receipt = receipt;
27 | resolve({
28 | ...buffer,
29 | state: TxState.Mined,
30 | });
31 | });
32 | });
33 |
34 | yield await Promise.race([txHashPromise, errorPromise]);
35 | yield await Promise.race([errorPromise, receiptPromise]);
36 | }
37 |
38 | export const TxState = {
39 | None: "none",
40 | Pending: "pending",
41 | Mined: "mined",
42 | };
43 |
--------------------------------------------------------------------------------
/src/hooks/use-account.js:
--------------------------------------------------------------------------------
1 | import { drizzleReactHooks } from "@drizzle/react-plugin";
2 |
3 | const { useDrizzleState } = drizzleReactHooks;
4 |
5 | export default function useAccount() {
6 | return useDrizzleState((ds) => ds.accounts[0]);
7 | }
8 |
--------------------------------------------------------------------------------
/src/hooks/use-chain-id.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, createContext, useContext } from "react";
2 | import { Alert, Spin } from "antd";
3 | import t from "prop-types";
4 |
5 | const ChainIdContext = createContext();
6 |
7 | export default function useChainId() {
8 | return useContext(ChainIdContext);
9 | }
10 |
11 | export function ChainIdProvider({ web3, children, renderOnError, renderOnLoading }) {
12 | const [chainId, setChainId] = useState();
13 | const [error, setError] = useState();
14 |
15 | useEffect(() => {
16 | if (!web3) {
17 | return;
18 | }
19 |
20 | let isMounted = true;
21 |
22 | async function getChainId() {
23 | try {
24 | const chainIdFromProvider = await web3.eth.getChainId();
25 |
26 | if (isMounted) {
27 | setChainId(chainIdFromProvider);
28 | }
29 | } catch (err) {
30 | if (isMounted) {
31 | setError(err);
32 | }
33 | }
34 | }
35 |
36 | getChainId();
37 |
38 | return () => {
39 | isMounted = false;
40 | };
41 | }, [web3]);
42 |
43 | useEffect(() => {
44 | if (!web3) {
45 | return;
46 | }
47 |
48 | const addListener = (web3.currentProvider.on ?? web3.currentProvider.addListener ?? (() => {})).bind(
49 | web3.currentProvider
50 | );
51 | const removeListener = (web3.currentProvider.off ?? web3.currentProvider.removeListener ?? (() => {})).bind(
52 | web3.currentProvider
53 | );
54 |
55 | let isMounted = true;
56 |
57 | const handleNetworkChanged = (chainIdFromEvent) => {
58 | // chainChanged payload is the chain ID in hex format.
59 | const normalizedChainId = hexStringToNumber(chainIdFromEvent);
60 |
61 | if (isMounted) {
62 | setChainId(normalizedChainId);
63 | }
64 | };
65 |
66 | addListener("chainChanged", handleNetworkChanged);
67 |
68 | return () => {
69 | removeListener("chainChanged", handleNetworkChanged);
70 | isMounted = false;
71 | };
72 | }, [web3]);
73 |
74 | const errorContent = error ? (typeof renderOnError === "function" ? renderOnError(error) : renderOnError) : null;
75 | const loadingContent =
76 | chainId === undefined ? (typeof renderOnLoading === "function" ? renderOnLoading() : renderOnLoading) : null;
77 |
78 | return chainId ? (
79 | {errorContent ?? loadingContent ?? children}
80 | ) : null;
81 | }
82 |
83 | const defaultRenderOnLoading = (
84 |
85 |
86 |
87 | );
88 |
89 | const defaultRenderOnError = (error) => ;
90 |
91 | ChainIdProvider.propTypes = {
92 | web3: t.object.isRequired,
93 | renderOnLoading: t.oneOfType([t.node, t.func]),
94 | renderOnError: t.oneOfType([t.node, t.func]),
95 | children: t.node,
96 | };
97 |
98 | ChainIdProvider.defaultProps = {
99 | renderOnLoading: defaultRenderOnLoading,
100 | renderOnError: defaultRenderOnError,
101 | };
102 |
103 | const hexStringToNumber = (chainId) => Number.parseInt(chainId, 16);
104 |
--------------------------------------------------------------------------------
/src/hooks/use-force-update.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 |
3 | export default function useForceUpdate() {
4 | const [token, setToken] = useState(Math.random());
5 | const forceUpdate = useCallback(() => {
6 | setToken(Math.random());
7 | }, []);
8 |
9 | return [token, forceUpdate];
10 | }
11 |
--------------------------------------------------------------------------------
/src/hooks/use-get-draws.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { useEffect, useState } from "react";
3 | import { displaySubgraph } from "../bootstrap/subgraph";
4 |
5 | const fetchDraws = async (chainId, where, lastId, first) => {
6 | const res = await axios.post(displaySubgraph[chainId], {
7 | query: `
8 | {
9 | draws(where: {${where}, id_gt: "${lastId}"}, first: ${first}) {
10 | id
11 | address
12 | disputeID
13 | appeal
14 | voteID
15 | }
16 | }
17 | `,
18 | });
19 |
20 | return res.data.data.draws;
21 | };
22 |
23 | const getBatch = async (chainId, where) => {
24 | const batches = [];
25 | let lastId = "";
26 | const BATCH_SIZE = 1000;
27 | // eslint-disable-next-line no-constant-condition
28 | while (true) {
29 | const entities = await fetchDraws(chainId, where, lastId, BATCH_SIZE);
30 | batches.push(entities);
31 | if (entities.length < BATCH_SIZE) break;
32 | lastId = entities[BATCH_SIZE - 1].id;
33 | }
34 | return batches.flat(1);
35 | };
36 |
37 | const useGetDraws = (chainId, where) => {
38 | const [draws, setDraws] = useState();
39 |
40 | useEffect(() => {
41 | getBatch(chainId, where).then((d) => setDraws(d));
42 | }, [chainId, where]);
43 |
44 | return draws;
45 | };
46 |
47 | export default useGetDraws;
48 |
--------------------------------------------------------------------------------
/src/hooks/use-get-shifts.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { useEffect, useState } from "react";
3 | import { displaySubgraph } from "../bootstrap/subgraph";
4 |
5 | const fetchShifts = async (chainId, where, lastId, first) => {
6 | const res = await axios.post(displaySubgraph[chainId], {
7 | query: `
8 | {
9 | tokenAndETHShifts(where: {${where}, id_gt: "${lastId}"}, first: ${first}) {
10 | id
11 | ETHAmount
12 | address
13 | disputeID
14 | tokenAmount
15 | }
16 | }
17 | `,
18 | });
19 |
20 | return res.data.data.tokenAndETHShifts;
21 | };
22 |
23 | const getBatch = async (chainId, where) => {
24 | const batches = [];
25 | let lastId = "";
26 | const BATCH_SIZE = 1000;
27 | // eslint-disable-next-line no-constant-condition
28 | while (true) {
29 | const entities = await fetchShifts(chainId, where, lastId, BATCH_SIZE);
30 | batches.push(entities);
31 | if (entities.length < BATCH_SIZE) break;
32 | lastId = entities[BATCH_SIZE - 1].id;
33 | }
34 | return batches.flat(1);
35 | };
36 |
37 | const useGetShifts = (chainId, where) => {
38 | const [shifts, setShifts] = useState();
39 |
40 | useEffect(() => {
41 | getBatch(chainId, where).then((s) => setShifts(s));
42 | }, [chainId, where]);
43 |
44 | return shifts;
45 | };
46 |
47 | export default useGetShifts;
48 |
--------------------------------------------------------------------------------
/src/hooks/use-interval.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | export default function useInterval(callback, delay) {
4 | const savedCallback = useRef(callback);
5 |
6 | useEffect(() => {
7 | savedCallback.current = callback;
8 | }, [callback]);
9 |
10 | useEffect(() => {
11 | function tick() {
12 | savedCallback.current();
13 | }
14 |
15 | if (delay !== null) {
16 | const id = setInterval(tick, delay);
17 | return () => {
18 | clearInterval(id);
19 | };
20 | }
21 | }, [callback, delay]);
22 | }
23 |
--------------------------------------------------------------------------------
/src/hooks/use-previous.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | export default function usePrevious(value) {
4 | const ref = useRef();
5 |
6 | useEffect(() => {
7 | ref.current = value;
8 | }, [value]);
9 |
10 | return ref.current;
11 | }
12 |
--------------------------------------------------------------------------------
/src/hooks/use-promise.js:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect, useReducer } from "react";
2 |
3 | export default function usePromise(promise) {
4 | const [state, dispatch] = useReducer(promiseReducer, INITIAL_STATE);
5 | const promiseRef = useRef();
6 |
7 | useEffect(() => {
8 | promiseRef.current = resolvePromise(promise);
9 |
10 | if (!promiseRef.current) {
11 | return;
12 | }
13 |
14 | let canceled = false;
15 |
16 | promiseRef.current.then(
17 | (value) => !canceled && dispatch({ type: State.Fulfilled, payload: value }),
18 | (reason) => !canceled && dispatch({ type: State.Rejected, payload: reason })
19 | );
20 |
21 | return () => {
22 | dispatch({ type: "RESET" });
23 | canceled = true;
24 | };
25 | }, [promise]);
26 |
27 | return {
28 | state: state.tag,
29 | isPending: state.tag === State.Pending,
30 | isSettled: state.tag !== State.Pending,
31 | isFulfilled: state.tag === State.Fulfilled,
32 | isRejected: state.tag === State.Rejected,
33 | value: state.value,
34 | reason: state.reason,
35 | };
36 | }
37 |
38 | const State = {
39 | Pending: "pending",
40 | Fulfilled: "fulfilled",
41 | Rejected: "rejected",
42 | };
43 |
44 | const INITIAL_STATE = {
45 | reason: undefined,
46 | value: undefined,
47 | tag: State.Pending,
48 | };
49 |
50 | function promiseReducer(state = INITIAL_STATE, action) {
51 | if (action.type === "RESET") {
52 | return INITIAL_STATE;
53 | }
54 |
55 | switch (state.tag) {
56 | case State.Pending: {
57 | switch (action.type) {
58 | case State.Fulfilled:
59 | return {
60 | reason: undefined,
61 | value: action.payload,
62 | tag: State.Fulfilled,
63 | };
64 | case State.Rejected:
65 | return {
66 | reason: action.payload,
67 | value: undefined,
68 | tag: State.Rejected,
69 | };
70 | default:
71 | return state;
72 | }
73 | }
74 | default:
75 | return state;
76 | }
77 | }
78 |
79 | async function resolvePromise(promise) {
80 | if (typeof promise === "function") {
81 | return await promise();
82 | }
83 |
84 | return await promise;
85 | }
86 |
--------------------------------------------------------------------------------
/src/hooks/use-query-params.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { useLocation } from "react-router-dom";
3 |
4 | export default function useQueryParams() {
5 | const { search } = useLocation();
6 |
7 | return useMemo(() => Object.fromEntries(new URLSearchParams(search).entries()), [search]);
8 | }
9 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { register } from "./bootstrap/service-worker";
4 | import "./bootstrap/sentry";
5 | import App from "./bootstrap/app";
6 | import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
7 |
8 | register();
9 |
10 | ReactDOM.render( , document.getElementById("root"));
11 |
--------------------------------------------------------------------------------
/src/temp/answer-string.js:
--------------------------------------------------------------------------------
1 | import BigNumber from "bignumber.js";
2 | import * as realitioLibQuestionFormatter from "@reality.eth/reality-eth-lib/formatters/question";
3 | import Web3 from "web3";
4 | const { toBN } = Web3.utils;
5 |
6 | export const getAnswerString = (rulingOptions, vote, uintDisplayMode = "dec") => {
7 | const questionJson = {
8 | decimals: rulingOptions.precision,
9 | outcomes: rulingOptions.titles,
10 | type: rulingOptions.type,
11 | };
12 |
13 | const returnString = realitioLibQuestionFormatter.getAnswerString(
14 | questionJson,
15 | realitioLibQuestionFormatter.padToBytes32(toBN(vote).sub(toBN("1")).toString(16))
16 | );
17 |
18 | const isNumericAnswer =
19 | /^\d+[.,]?\d*(e[-+]?\d+)?$/.test(returnString) && ["uint", "int"].includes(rulingOptions.type);
20 |
21 | if (isNumericAnswer) {
22 | if (uintDisplayMode === "hex") {
23 | return realitioLibQuestionFormatter.padToBytes32(toBN(vote).sub(toBN("1")).toString(16));
24 | }
25 | if (uintDisplayMode === "dec") {
26 | BigNumber.config({ EXPONENTIAL_AT: 1e9 });
27 | } else {
28 | BigNumber.config({ EXPONENTIAL_AT: [-3, 1e9] });
29 | }
30 | const returnStringFormated = new BigNumber(returnString);
31 | return returnStringFormated.toString();
32 | }
33 |
34 | return returnString;
35 | };
36 |
--------------------------------------------------------------------------------
/src/temp/web3-derive-account.js:
--------------------------------------------------------------------------------
1 | export const derivedAccountKey =
2 | "To keep your data safe and to use certain features of Kleros, we ask that you sign these messages to create a secret key for your account. This key is unrelated from your main Ethereum account and will not be able to send any transactions.";
3 |
4 | export default async (web3, account, create = false) => {
5 | const storageKey = `${account}-${derivedAccountKey}`;
6 | let secret = localStorage.getItem(storageKey);
7 |
8 | if (secret === null) {
9 | if (!create) return null;
10 |
11 | secret = await web3.eth.personal.sign(derivedAccountKey, account);
12 | localStorage.setItem(storageKey, secret);
13 | }
14 |
15 | return web3.eth.accounts.privateKeyToAccount(web3.utils.keccak256(secret));
16 | };
17 |
--------------------------------------------------------------------------------
/src/temp/web3-salt.js:
--------------------------------------------------------------------------------
1 | export default async function web3Salt(web3, account, key, ...args) {
2 | const storageKey = `${account}-${key}`;
3 | let secret = localStorage.getItem(storageKey);
4 |
5 | if (secret === null) {
6 | secret = await cachedSignRequest(web3, key, account);
7 | localStorage.setItem(storageKey, secret);
8 | }
9 |
10 | return web3.utils.soliditySha3(secret, ...args);
11 | }
12 |
13 | const signatureRequestCache = {};
14 |
15 | /**
16 | * Components that call the function above are usually big and will re-render for various reasons.
17 | * Since we don't have the time to properly refactor them, this is a workaround so ensure that we
18 | * won't spam jurors with multiple signature requests for the same message.
19 | */
20 | const cachedSignRequest = async (web3, key, account) => {
21 | if (!signatureRequestCache[key]) {
22 | signatureRequestCache[key] = web3.eth.personal.sign(key, account);
23 | }
24 |
25 | const result = await signatureRequestCache[key];
26 | delete signatureRequestCache[key];
27 |
28 | return result;
29 | };
30 |
--------------------------------------------------------------------------------