├── src ├── react-app-env.d.ts ├── assets │ ├── images │ │ ├── frame.png │ │ ├── opera.webp │ │ ├── menu-icon.png │ │ ├── fortmatic.svg │ │ ├── trust.svg │ │ └── walletconnect.svg │ └── abis │ │ ├── PermanentGTCRFactory.json │ │ ├── GTCRFactory.json │ │ └── LightGTCRFactory.json ├── types │ ├── index.d.ts │ └── web3-context.ts ├── index.js ├── prop-types │ ├── bn.js │ └── item.js ├── pages │ ├── layout-content.js │ ├── no-web3.jsx │ ├── items-router.tsx │ ├── item-details-router.tsx │ ├── light-item-details │ │ ├── tour-steps.js │ │ ├── item-action-modal.js │ │ └── modals │ │ │ └── evidence.js │ ├── error-page │ │ └── index.js │ ├── permanent-item-details │ │ ├── item-action-modal.js │ │ └── modals │ │ │ ├── evidence.js │ │ │ └── withdraw.tsx │ ├── items │ │ ├── item-card-title.js │ │ └── tour-steps.js │ ├── permanent-items │ │ └── item-card-title.js │ ├── item-details │ │ ├── item-action-modal.js │ │ └── modals │ │ │ └── evidence.js │ └── light-items │ │ ├── tour-steps.js │ │ └── item-card-title.js ├── styles │ ├── small-screen-style.js │ └── responsive-size.ts ├── components │ ├── custom-registries │ │ └── seer │ │ │ ├── is-seer-registry.tsx │ │ │ ├── use-seer-markets-data.tsx │ │ │ └── seer-card-content.tsx │ ├── loading.js │ ├── long-text.js │ ├── eth-address.tsx │ ├── eth-amount.tsx │ ├── pgtcr-deposit-input.js │ ├── address-input.tsx │ ├── gtcr-address.js │ ├── file-display.js │ ├── rich-address.tsx │ ├── permanent-item-card-content.js │ ├── permanent-item-action-button.js │ ├── item-action-button.js │ ├── item-card-content.js │ ├── tour.js │ ├── custom-input.js │ ├── beta-warning.js │ ├── tcr-metadata-display.js │ ├── footer.tsx │ ├── contract-explorer-url.tsx │ ├── layout │ │ └── app-menu.js │ ├── display-selector.js │ ├── permanent-item-status-badge.js │ ├── light-item-card-content.js │ ├── smart-contract-wallet-warning.tsx │ ├── tcr-card-content.js │ ├── rich-address-input.tsx │ ├── light-tcr-card-content.js │ ├── permanent-tcr-card-content.js │ └── base-deposit-input.js ├── utils │ ├── object-without-key.js │ ├── truncate-at-word.js │ ├── graphql │ │ ├── tcr-existence-test.js │ │ ├── light-registry.js │ │ ├── index.js │ │ ├── item-search.js │ │ ├── permanent-registry.js │ │ ├── classic-registry-items.js │ │ ├── item-details.js │ │ ├── classic-item-details.js │ │ ├── light-items.js │ │ ├── permanent-items.js │ │ └── permanent-item-details.js │ ├── get-ipfs-path.ts │ ├── network-utils.js │ ├── lower-limit.js │ ├── ipfs-parse.ts │ ├── async-file-reader.js │ ├── upload-form-data-to-ipfs.ts │ ├── network-env.js │ ├── ipfs-publish.js │ ├── fast-signer.js │ ├── string.js │ └── notifications.js ├── hooks │ ├── navigate-and-scroll-top.js │ ├── native-currency.js │ ├── window-dimensions.js │ ├── use-graphql-client.ts │ ├── use-check-light-curate.ts │ ├── countdown.js │ ├── appeal-time.js │ ├── token-symbol.js │ ├── get-logs.js │ ├── arbitration-cost.js │ ├── meta-evidence.js │ ├── factory.js │ ├── required-fees.js │ ├── use-tcr-network.js │ └── use-path-validation.ts ├── contexts │ ├── wallet-context.js │ ├── tcr-view-context.js │ ├── light-tcr-view-context.js │ └── tour-context.js ├── bootstrap │ ├── fontawesome.js │ ├── app.js │ └── app-router.tsx └── config │ ├── connectors.js │ └── networks.js ├── .prettierrc.js ├── public ├── favicon.ico ├── icon-192.png ├── icon-512.png ├── manifest.json └── index.html ├── .gitignore ├── tsconfig.json ├── netlify.toml ├── LICENSE ├── README.md ├── .env.example ├── CONTRIBUTING.md └── .eslintrc.js /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true 4 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kleros/gtcr/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kleros/gtcr/HEAD/public/icon-192.png -------------------------------------------------------------------------------- /public/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kleros/gtcr/HEAD/public/icon-512.png -------------------------------------------------------------------------------- /src/assets/images/frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kleros/gtcr/HEAD/src/assets/images/frame.png -------------------------------------------------------------------------------- /src/assets/images/opera.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kleros/gtcr/HEAD/src/assets/images/opera.webp -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | ethereum: any 3 | } 4 | 5 | type Empty = null | undefined 6 | -------------------------------------------------------------------------------- /src/assets/images/menu-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kleros/gtcr/HEAD/src/assets/images/menu-icon.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import App from './bootstrap/app' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | 5 | ReactDOM.render(, document.querySelector('#root')) 6 | -------------------------------------------------------------------------------- /src/prop-types/bn.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | 3 | const BNPropType = PropTypes.shape({ 4 | add: PropTypes.func.isRequired, 5 | mul: PropTypes.func.isRequired 6 | }) 7 | 8 | export default BNPropType 9 | -------------------------------------------------------------------------------- /src/pages/layout-content.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Layout } from 'antd' 3 | 4 | const StyledLayoutContent = styled(Layout.Content)` 5 | padding: 42px 9.375vw 42px; 6 | ` 7 | export default StyledLayoutContent 8 | -------------------------------------------------------------------------------- /src/styles/small-screen-style.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components' 2 | 3 | export const BREAKPOINT_LANDSCAPE = 900 4 | 5 | export const smallScreenStyle = styleFn => css` 6 | @media (max-width: ${BREAKPOINT_LANDSCAPE}px) { 7 | ${() => styleFn()} 8 | } 9 | ` 10 | -------------------------------------------------------------------------------- /src/components/custom-registries/seer/is-seer-registry.tsx: -------------------------------------------------------------------------------- 1 | import { seerAddresses } from 'config/tcr-addresses' 2 | 3 | export const isSeerRegistry = (tcrAddress: string, chainId: string) => 4 | seerAddresses[chainId as keyof typeof seerAddresses] === 5 | tcrAddress.toLowerCase() 6 | -------------------------------------------------------------------------------- /src/types/web3-context.ts: -------------------------------------------------------------------------------- 1 | import { Web3Context } from 'web3-react/dist/context' 2 | 3 | export interface ErrorWithCode extends Error { 4 | code?: string | number | undefined 5 | } 6 | 7 | export interface Web3ContextCurate extends Web3Context { 8 | error: ErrorWithCode | null 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/object-without-key.js: -------------------------------------------------------------------------------- 1 | // i don't want to have to import lodash just for this 2 | 3 | const objectWithoutKey = (object, key) => { 4 | // eslint-disable-next-line no-unused-vars 5 | const { [key]: nothing, ...otherKeys } = object 6 | return otherKeys 7 | } 8 | 9 | export default objectWithoutKey 10 | -------------------------------------------------------------------------------- /src/utils/truncate-at-word.js: -------------------------------------------------------------------------------- 1 | export const truncateAtWord = (text, maxLength) => { 2 | if (text.length <= maxLength) return text 3 | const truncated = text.substring(0, maxLength) 4 | const lastSpace = truncated.lastIndexOf(' ') 5 | return lastSpace > 0 6 | ? `${truncated.substring(0, lastSpace)}...` 7 | : `${truncated}...` 8 | } 9 | -------------------------------------------------------------------------------- /src/hooks/navigate-and-scroll-top.js: -------------------------------------------------------------------------------- 1 | import { useHistory } from 'react-router-dom' 2 | 3 | const useNavigateAndScrollTop = () => { 4 | const history = useHistory() 5 | 6 | const navigateAndScrollTop = path => { 7 | history.push(path) 8 | window.scrollTo(0, 0) 9 | } 10 | 11 | return navigateAndScrollTop 12 | } 13 | 14 | export default useNavigateAndScrollTop 15 | -------------------------------------------------------------------------------- /src/utils/graphql/tcr-existence-test.js: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | const TCR_EXISTENCE_TEST = gql` 4 | query TcrExistenceTest($tcrAddress: String!) { 5 | lregistry: LRegistry_by_pk(id: $tcrAddress) { 6 | id 7 | } 8 | registry: Registry_by_pk(id: $tcrAddress) { 9 | id 10 | } 11 | } 12 | ` 13 | 14 | export default TCR_EXISTENCE_TEST 15 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Kleros · GTCR", 3 | "name": "Kleros · Generalized Tokenc Curated List", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Spin } from 'antd' 4 | 5 | const LoadingWrapper = styled.div` 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | height: 100%; 10 | ` 11 | 12 | const Loading = () => ( 13 | 14 | 15 | 16 | ) 17 | 18 | export default Loading 19 | -------------------------------------------------------------------------------- /src/components/long-text.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Typography } from 'antd' 4 | 5 | const StyledSpan = styled.span` 6 | color: gray; 7 | ` 8 | 9 | const LongText = ({ value }) => { 10 | if (!value) return empty 11 | 12 | return {value} 13 | } 14 | 15 | export default LongText 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # theme build 15 | /src/bootstrap/theme.css 16 | 17 | # misc 18 | .DS_Store 19 | .env 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Local Netlify folder 26 | .netlify 27 | -------------------------------------------------------------------------------- /src/hooks/native-currency.js: -------------------------------------------------------------------------------- 1 | import { useWeb3Context } from 'web3-react' 2 | import { NETWORKS } from '../config/networks' 3 | 4 | /** 5 | * Get the ticker for the chain's native currency. 6 | * @returns {string} The ticker for the chain's native currency. 7 | */ 8 | export default function useNativeCurrency() { 9 | const { networkId } = useWeb3Context() 10 | if (!networkId) return 'ETH' 11 | if (networkId === NETWORKS.gnosis) return 'xDAI' 12 | 13 | return 'ETH' 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/get-ipfs-path.ts: -------------------------------------------------------------------------------- 1 | export interface IPFSResultObject { 2 | cids: string[] 3 | } 4 | 5 | export const getIPFSPath = (ipfsResultObject: IPFSResultObject): string => 6 | getFormattedPath(ipfsResultObject.cids[0]) 7 | 8 | /** 9 | * 10 | * @param url an ipfs cid 11 | * @returns formats an ipfs cid to be in /ipfs/hash format, reolaces ipfs://, ipfs/, with /ipfs/ 12 | */ 13 | export const getFormattedPath = (url: string) => 14 | url.replace(/^(ipfs:\/\/|ipfs\/?)/, '/ipfs/') 15 | -------------------------------------------------------------------------------- /src/utils/network-utils.js: -------------------------------------------------------------------------------- 1 | import { NETWORKS_INFO } from 'config/networks' 2 | 3 | export const getAddressPage = ({ networkId, address }) => { 4 | const page = NETWORKS_INFO[networkId].explorers[0].url 5 | const pageWithAddress = `${page}/address/${address}` 6 | return pageWithAddress 7 | } 8 | 9 | export const getTxPage = ({ networkId, txHash }) => { 10 | const page = NETWORKS_INFO[networkId].explorers[0].url 11 | const pageWithTx = `${page}/tx/${txHash}` 12 | return pageWithTx 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/graphql/light-registry.js: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | const LIGHT_REGISTRY_QUERY = gql` 4 | query lightRegistryQuery($lowerCaseTCRAddress: String!) { 5 | lregistry: LRegistry_by_pk(id: $lowerCaseTCRAddress) { 6 | numberOfAbsent 7 | numberOfRegistered 8 | numberOfRegistrationRequested 9 | numberOfClearingRequested 10 | numberOfChallengedRegistrations 11 | numberOfChallengedClearing 12 | } 13 | } 14 | ` 15 | 16 | export default LIGHT_REGISTRY_QUERY 17 | -------------------------------------------------------------------------------- /src/assets/images/fortmatic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/contexts/wallet-context.js: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react' 2 | import PropTypes from 'prop-types' 3 | import useNotificationWeb3 from '../hooks/notifications-web3' 4 | 5 | const WalletContext = createContext() 6 | const WalletProvider = ({ children }) => ( 7 | 8 | {children} 9 | 10 | ) 11 | 12 | WalletProvider.propTypes = { 13 | children: PropTypes.node.isRequired 14 | } 15 | 16 | export { WalletContext, WalletProvider } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "preserve" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /src/contexts/tcr-view-context.js: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react' 2 | import PropTypes from 'prop-types' 3 | import useTcrView from '../hooks/tcr-view' 4 | 5 | const TCRViewContext = createContext() 6 | const TCRViewProvider = ({ children, tcrAddress }) => ( 7 | 8 | {children} 9 | 10 | ) 11 | 12 | TCRViewProvider.propTypes = { 13 | children: PropTypes.node.isRequired, 14 | tcrAddress: PropTypes.string.isRequired 15 | } 16 | 17 | export { TCRViewContext, TCRViewProvider } 18 | -------------------------------------------------------------------------------- /src/contexts/light-tcr-view-context.js: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react' 2 | import PropTypes from 'prop-types' 3 | import useLightTcrView from 'hooks/light-tcr-view' 4 | 5 | const LightTCRViewContext = createContext() 6 | const LightTCRViewProvider = ({ children, tcrAddress }) => ( 7 | 8 | {children} 9 | 10 | ) 11 | 12 | LightTCRViewProvider.propTypes = { 13 | children: PropTypes.node.isRequired, 14 | tcrAddress: PropTypes.string.isRequired 15 | } 16 | 17 | export { LightTCRViewContext, LightTCRViewProvider } 18 | -------------------------------------------------------------------------------- /src/utils/lower-limit.js: -------------------------------------------------------------------------------- 1 | // Takes the lower limit from an array of integers compared to some input. 2 | // This is used to select which metadata to use for an item based on 3 | // the its blocknumber or timestamp and the block number or timestamp 4 | // of the metaevidence used on when it was first submitted. 5 | const takeLower = (list, limit) => { 6 | list = list.map(item => Number(item)) 7 | limit = Number(limit) 8 | let result = list[0] 9 | 10 | for (let i = 0; i < list.length; i++) 11 | if (list[i] > limit) { 12 | result = list[i - 1] 13 | break 14 | } 15 | 16 | return result 17 | } 18 | 19 | export default takeLower 20 | -------------------------------------------------------------------------------- /src/utils/ipfs-parse.ts: -------------------------------------------------------------------------------- 1 | export const getFormattedPath = (url: string) => { 2 | // Handle already formatted or prefixed URLs 3 | if (url.startsWith('/ipfs/')) return url 4 | if (url.startsWith('ipfs/')) return `/${url}` 5 | if (url.startsWith('ipfs://')) return url.replace('ipfs://', '/ipfs/') 6 | 7 | // Handle raw IPFS hashes (CIDv0 or CIDv1) 8 | const ipfsHashPattern = /^[a-zA-Z0-9]{46,59}$/ 9 | if (ipfsHashPattern.test(url)) return `/ipfs/${url}` 10 | 11 | return url 12 | } 13 | 14 | export const parseIpfs = (path: string) => { 15 | const ipfsResourceLink = 16 | process.env.REACT_APP_IPFS_GATEWAY + getFormattedPath(path) 17 | 18 | return ipfsResourceLink 19 | } 20 | -------------------------------------------------------------------------------- /src/components/eth-address.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { useWeb3Context } from 'web3-react' 4 | import { getAddressPage } from '../utils/network-utils' 5 | 6 | const StyledA = styled.a` 7 | text-decoration: underline; 8 | ` 9 | 10 | const ETHAddress: React.FC<{ address: string }> = ({ address }) => { 11 | const { networkId } = useWeb3Context() 12 | const fullPage = getAddressPage({ networkId, address }) 13 | return ( 14 | 15 | {address.slice(0, 6)}...{address.slice(address.length - 4)} 16 | 17 | ) 18 | } 19 | 20 | export default ETHAddress 21 | -------------------------------------------------------------------------------- /src/utils/graphql/index.js: -------------------------------------------------------------------------------- 1 | export { default as LIGHT_ITEM_DETAILS_QUERY } from './item-details' 2 | export { default as LIGHT_ITEMS_QUERY } from './light-items' 3 | export { default as CLASSIC_REGISTRY_ITEMS_QUERY } from './classic-registry-items' 4 | export { default as CLASSIC_ITEM_DETAILS_QUERY } from './classic-item-details' 5 | export { default as LIGHT_REGISTRY_QUERY } from './light-registry' 6 | export { default as TCR_EXISTENCE_TEST } from './tcr-existence-test' 7 | export { default as PERMANENT_ITEM_DETAILS_QUERY } from './permanent-item-details' 8 | export { default as PERMANENT_REGISTRY_QUERY } from './permanent-registry' 9 | export { default as PERMANENT_ITEMS_QUERY } from './permanent-items' 10 | -------------------------------------------------------------------------------- /src/utils/async-file-reader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wraps FileReader in a promise. 3 | * @param {string} file The path to the blob we want to read. 4 | * @returns {object} A promise version of FileReader. 5 | */ 6 | export default function asyncReadFile(file) { 7 | return new Promise((resolve, reject) => { 8 | let content = '' 9 | const reader = new FileReader() 10 | // Wait till complete 11 | reader.onloadend = e => { 12 | content = e.target.result 13 | const result = content.split(/\r\n|\n/) 14 | resolve(result) 15 | } 16 | // Make sure to handle error states 17 | reader.addEventListener('error', e => { 18 | reject(e) 19 | }) 20 | reader.readAsDataURL(file) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/styles/responsive-size.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description this func applies repsonsiveness to a css property, the value will range from minSize to maxSize 3 | * @param minSize the minimum value of the property 4 | * @param maxSize max value of the property 5 | * @param minScreen the min screen width at which the property will be at minSize 6 | * @param maxScreen the max screen width at which the property will be at maxSize 7 | * 8 | */ 9 | export const responsiveSize = ( 10 | minSize: number, 11 | maxSize: number, 12 | minScreen: number = 375, 13 | maxScreen: number = 1250 14 | ) => 15 | `calc(${minSize}px + (${maxSize} - ${minSize}) * (min(max(100vw, ${minScreen}px), ${maxScreen}px) - ${minScreen}px) / (${maxScreen - 16 | minScreen}))` 17 | -------------------------------------------------------------------------------- /src/utils/upload-form-data-to-ipfs.ts: -------------------------------------------------------------------------------- 1 | import { fetch } from 'cross-fetch' 2 | 3 | export async function uploadFormDataToIPFS( 4 | formData: FormData, 5 | operation: string = 'evidence', 6 | pinToGraph = false 7 | ): Promise { 8 | const url = `${process.env.REACT_APP_COURT_FUNCTIONS_URL}/.netlify/functions/upload-to-ipfs?operation=${operation}&pinToGraph=${pinToGraph}` 9 | 10 | const response = await fetch(url, { 11 | method: 'POST', 12 | body: formData 13 | }) 14 | 15 | if (response.status !== 200) { 16 | const error = await response 17 | .json() 18 | .catch(() => ({ message: 'Error uploading to IPFS' })) 19 | throw new Error(error.message) 20 | } 21 | const data = await response.json() 22 | return data 23 | } 24 | -------------------------------------------------------------------------------- /src/bootstrap/fontawesome.js: -------------------------------------------------------------------------------- 1 | import { library } from '@fortawesome/fontawesome-svg-core' 2 | import { fab } from '@fortawesome/free-brands-svg-icons' 3 | import { 4 | faBullhorn, 5 | faGavel, 6 | faHourglassHalf, 7 | faClock, 8 | faBalanceScale, 9 | faCoins, 10 | faInfoCircle, 11 | faExternalLinkAlt, 12 | faSearch, 13 | faBell, 14 | faFileAlt, 15 | faCheck, 16 | faExclamationTriangle, 17 | faShieldAlt 18 | } from '@fortawesome/free-solid-svg-icons' 19 | 20 | library.add( 21 | faGavel, 22 | faHourglassHalf, 23 | faClock, 24 | faBalanceScale, 25 | faCoins, 26 | faBullhorn, 27 | faInfoCircle, 28 | fab, 29 | faExternalLinkAlt, 30 | faSearch, 31 | faBell, 32 | faFileAlt, 33 | faCheck, 34 | faExclamationTriangle, 35 | faShieldAlt 36 | ) 37 | -------------------------------------------------------------------------------- /src/prop-types/item.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import BNPropType from './bn' 3 | 4 | const ItemPropTypes = PropTypes.shape({ 5 | ID: PropTypes.string.isRequired, 6 | status: PropTypes.number.isRequired, 7 | disputed: PropTypes.bool.isRequired, 8 | disputeStatus: PropTypes.number.isRequired, 9 | hasPaid: PropTypes.arrayOf(PropTypes.bool).isRequired, 10 | data: PropTypes.string.isRequired, 11 | // eslint-disable-next-line react/forbid-prop-types 12 | decodedData: PropTypes.array, 13 | currentRuling: PropTypes.number.isRequired, 14 | appealStart: BNPropType.isRequired, 15 | appealEnd: BNPropType.isRequired, 16 | submissionTime: BNPropType.isRequired, 17 | amountPaid: PropTypes.arrayOf(BNPropType).isRequired 18 | }) 19 | 20 | export default ItemPropTypes 21 | -------------------------------------------------------------------------------- /src/hooks/window-dimensions.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react' 2 | 3 | const getWindowDimensions = () => { 4 | const { innerWidth: width, innerHeight: height } = window 5 | return { 6 | width, 7 | height 8 | } 9 | } 10 | 11 | const useWindowDimensions = () => { 12 | const [windowDimensions, setWindowDimensions] = useState( 13 | getWindowDimensions() 14 | ) 15 | 16 | const handleResize = useCallback(() => { 17 | setWindowDimensions(getWindowDimensions()) 18 | }, []) 19 | 20 | useEffect(() => { 21 | if (!handleResize) return 22 | window.addEventListener('resize', handleResize) 23 | return () => window.removeEventListener('resize', handleResize) 24 | }, [handleResize]) 25 | 26 | return windowDimensions 27 | } 28 | 29 | export default useWindowDimensions 30 | -------------------------------------------------------------------------------- /src/hooks/use-graphql-client.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { ApolloClient, InMemoryCache } from '@apollo/client' 3 | import { HttpLink } from '@apollo/client/link/http' 4 | import { subgraphUrl, validChains } from 'config/tcr-addresses' 5 | 6 | const useApolloClientFactory = (networkId: number | Empty) => { 7 | const client = useMemo(() => { 8 | if (!networkId) return null 9 | 10 | const GTCR_SUBGRAPH_URL = subgraphUrl[networkId as validChains] 11 | 12 | if (!GTCR_SUBGRAPH_URL) return null 13 | 14 | const httpLink = new HttpLink({ 15 | uri: GTCR_SUBGRAPH_URL 16 | }) 17 | 18 | return new ApolloClient({ 19 | link: httpLink, 20 | cache: new InMemoryCache() 21 | }) 22 | }, [networkId]) 23 | 24 | return client 25 | } 26 | 27 | export default useApolloClientFactory 28 | -------------------------------------------------------------------------------- /src/hooks/use-check-light-curate.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useParams } from 'react-router' 3 | import { useQuery } from '@apollo/client' 4 | import { TCR_EXISTENCE_TEST } from 'utils/graphql' 5 | 6 | const useCheckLightCurate = (): { 7 | isLightCurate: boolean 8 | isClassicCurate: boolean 9 | checking: boolean 10 | } => { 11 | const { tcrAddress } = useParams<{ tcrAddress: string }>() 12 | const { loading, data } = useQuery(TCR_EXISTENCE_TEST, { 13 | variables: { 14 | tcrAddress: tcrAddress.toLowerCase() 15 | } 16 | }) 17 | const isLightCurate = useMemo(() => data?.lregistry ?? false, [data]) 18 | const isClassicCurate = useMemo(() => data?.registry ?? false, [ 19 | data 20 | ]) 21 | return { isLightCurate, isClassicCurate, checking: loading } 22 | } 23 | 24 | export default useCheckLightCurate 25 | -------------------------------------------------------------------------------- /src/pages/no-web3.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import styled from 'styled-components' 3 | import { WalletContext } from 'contexts/wallet-context' 4 | import ErrorPage from './error-page' 5 | 6 | const StyledSpan = styled.span` 7 | text-decoration: underline; 8 | cursor: pointer; 9 | ` 10 | 11 | const NoWeb3Detected = () => { 12 | const { requestWeb3Auth } = useContext(WalletContext) 13 | return ( 14 | 19 | Please{' '} 20 | 24 | connect a wallet. 25 | 26 | 27 | } 28 | /> 29 | ) 30 | } 31 | 32 | export default NoWeb3Detected 33 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "build/" 3 | command = "npm run build" 4 | 5 | [build.environment] 6 | NODE_VERSION='16.20.2' 7 | REACT_APP_IPFS_GATEWAY="https://cdn.kleros.link" 8 | REACT_APP_DEFAULT_NETWORK="1" 9 | 10 | REACT_APP_REJECT_ALL_POLICY_URI='/ipfs/QmZ7RVU7re1g8nXDbAFMHV99pyie3dn4cY7Ga2X4h8mDpV/reject-all-policy.pdf' 11 | REACT_APP_METAMASK_SITE_URL='https://metamask.io' 12 | REACT_APP_TRUST_SITE_URL='https://trustwallet.com' 13 | REACT_APP_WALLETCONNECT_BRIDGE_URL='https://bridge.walletconnect.org' 14 | REACT_APP_INSTRUCTION_VIDEO='https://www.youtube.com/embed/DKPVWzhh8Y8' 15 | REACT_APP_COURT_FUNCTIONS_URL='https://kleros-api.netlify.app' 16 | 17 | [[redirects]] 18 | from = "/*" 19 | to = "/index.html" 20 | status = 200 21 | 22 | [[redirects]] 23 | from = "https://curate.kleros.io/*" 24 | to = "https://curate.kleros.builders/:splat" 25 | status = 301 26 | force = true 27 | 28 | -------------------------------------------------------------------------------- /src/hooks/countdown.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import humanizeDuration from 'humanize-duration' 3 | 4 | const useHumanizedCountdown = (duration, largest) => { 5 | const [remainingTime, setRemainingTime] = useState() 6 | 7 | useEffect(() => { 8 | if (!duration) return 9 | if (!remainingTime) { 10 | setRemainingTime(duration) 11 | return 12 | } 13 | const id = setInterval(() => { 14 | setRemainingTime(remainingTime => 15 | remainingTime > 0 ? remainingTime - 1000 : 0 16 | ) 17 | }, 1000) 18 | return () => clearInterval(id) 19 | }, [duration, remainingTime]) 20 | 21 | const formattedTime = `${remainingTime >= 0 ? 'In ' : ''}${humanizeDuration( 22 | remainingTime, 23 | { 24 | largest: largest || 2, 25 | round: true 26 | } 27 | )}${remainingTime < 0 ? ' ago' : ''}` 28 | 29 | return formattedTime 30 | } 31 | 32 | export default useHumanizedCountdown 33 | -------------------------------------------------------------------------------- /src/utils/network-env.js: -------------------------------------------------------------------------------- 1 | import { NETWORKS } from '../config/networks' 2 | 3 | /** 4 | * Fetch an environment variable for a given networkId. 5 | * @param {string} envVariableKey The environment variable to fetch. 6 | * @param {number} networkId The network Id. 7 | * @returns {*} The variable content for the networkId. 8 | */ 9 | function getNetworkEnv(envVariableKey, networkId) { 10 | const defaultNetwork = 11 | process.env.REACT_APP_DEFAULT_NETWORK || NETWORKS.ethereum 12 | let data = '' 13 | try { 14 | data = process.env[envVariableKey] 15 | ? JSON.parse(process.env[envVariableKey])[networkId || defaultNetwork] 16 | : '' 17 | } catch (_) { 18 | console.error(`Failed to parse env variable ${envVariableKey}`) 19 | } 20 | 21 | if (data === '') 22 | console.warn( 23 | `Warning: no value found for ${envVariableKey}, networkId: ${networkId}` 24 | ) 25 | 26 | return data 27 | } 28 | 29 | export default getNetworkEnv 30 | -------------------------------------------------------------------------------- /src/hooks/appeal-time.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { BigNumber, bigNumberify } from 'ethers/utils' 3 | 4 | const useAppealTime = (item, old = true) => 5 | useMemo(() => { 6 | if (!item) return {} 7 | if (!old && item.challenges.length === 0) return {} 8 | const round = old 9 | ? item.requests[0].rounds[0] 10 | : item.challenges[0].rounds[0] 11 | const { appealPeriodStart, appealPeriodEnd } = round 12 | const appealStart = new BigNumber(appealPeriodStart) 13 | const appealEnd = new BigNumber(appealPeriodEnd) 14 | const appealDuration = appealEnd.sub(appealStart) 15 | const appealEndLoser = appealStart.add(appealDuration.div(bigNumberify(2))) 16 | 17 | const appealRemainingTime = 18 | appealEnd.toNumber() * 1000 - Math.floor(Date.now()) 19 | const appealRemainingTimeLoser = 20 | appealEndLoser.toNumber() * 1000 - Math.floor(Date.now()) 21 | 22 | return { 23 | appealRemainingTime, 24 | appealRemainingTimeLoser 25 | } 26 | }, [item, old]) 27 | 28 | export default useAppealTime 29 | -------------------------------------------------------------------------------- /src/components/eth-amount.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Skeleton } from 'antd' 4 | import { ethers } from 'ethers' 5 | import { BigNumberish } from 'ethers/utils' 6 | 7 | const SkeletonTitleProps = { width: 30 } 8 | const StyledSkeleton = styled(Skeleton)` 9 | display: inline; 10 | 11 | .ant-skeleton-title { 12 | margin: -3px 0; 13 | } 14 | ` 15 | const ETHAmount: React.FC<{ 16 | amount: BigNumberish 17 | decimals: number 18 | displayUnit: string 19 | }> = ({ amount, decimals, displayUnit }) => { 20 | if (amount === null) 21 | return ( 22 | 23 | ) 24 | 25 | const formattedEther = ethers.utils.formatEther( 26 | typeof amount === 'number' 27 | ? amount.toLocaleString('fullwide', { useGrouping: false }) 28 | : String(amount) 29 | ) 30 | 31 | const valueDisplayed = formattedEther.replace(/\.?0+$/, '') 32 | 33 | return <>{valueDisplayed + (displayUnit || '')} 34 | } 35 | 36 | export default ETHAmount 37 | -------------------------------------------------------------------------------- /src/utils/graphql/item-search.js: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | const ITEM_SEARCH_QUERY = gql` 4 | query itemSearchQuery($where: LItem_bool_exp!, $limit: Int) { 5 | itemSearch: LItem(limit: $limit, where: $where) { 6 | id 7 | itemID 8 | data 9 | props(order_by: { label: asc }) { 10 | type: itemType 11 | value 12 | isIdentifier 13 | } 14 | registry { 15 | id 16 | } 17 | requests(limit: 1, order_by: { submissionTime: desc }) { 18 | disputed 19 | disputeID 20 | submissionTime 21 | resolved 22 | requester 23 | challenger 24 | resolutionTime 25 | deposit 26 | rounds(limit: 1, order_by: { creationTime: desc }) { 27 | appealPeriodStart 28 | appealPeriodEnd 29 | ruling 30 | hasPaidRequester 31 | hasPaidChallenger 32 | amountPaidRequester 33 | amountPaidChallenger 34 | } 35 | } 36 | } 37 | } 38 | ` 39 | 40 | export default ITEM_SEARCH_QUERY 41 | -------------------------------------------------------------------------------- /src/hooks/token-symbol.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react' 2 | import { ethers } from 'ethers' 3 | import { useWeb3Context } from 'web3-react' 4 | 5 | const ERC20_SYMBOL_ABI = ['function symbol() view returns (string)'] 6 | 7 | const useTokenSymbol = tokenAddress => { 8 | const [symbol, setSymbol] = useState('tokens') 9 | const [loading, setLoading] = useState(false) 10 | const { library } = useWeb3Context() 11 | 12 | const fetchSymbol = useCallback(async () => { 13 | if (!tokenAddress || !library) return 14 | 15 | setLoading(true) 16 | try { 17 | const token = new ethers.Contract(tokenAddress, ERC20_SYMBOL_ABI, library) 18 | const tokenSymbol = await token.symbol() 19 | setSymbol(tokenSymbol) 20 | } catch (err) { 21 | console.error('Error fetching token symbol:', err) 22 | setSymbol('tokens') 23 | } 24 | setLoading(false) 25 | }, [tokenAddress, library]) 26 | 27 | useEffect(() => { 28 | fetchSymbol() 29 | }, [fetchSymbol]) 30 | 31 | return { symbol, loading } 32 | } 33 | 34 | export default useTokenSymbol 35 | -------------------------------------------------------------------------------- /src/hooks/get-logs.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | // once upon a time, infura was giving timeout errors. 4 | // alchemy was used as a result. but users tend to use 5 | // metamask, which defaults to infura as rpc provider, 6 | // and thus these errors kept happening. 7 | // the solution then was to override the library used to 8 | // fetch the logs, and use the once provided in the env. 9 | 10 | // alchemy is now giving 429 errors a bit too frequently, 11 | // and infura somehow works seamlessly again, so this function 12 | // doesn't seem to do anything anymore. but, it's there in case 13 | // these overrides come useful again. plus, refactoring it out 14 | // would be time consuming. 15 | const useGetLogs = library => { 16 | const getLogs = useMemo( 17 | () => async query => { 18 | const defResult = await library.getLogs(query) 19 | return defResult 20 | }, 21 | // eslint-disable-next-line react-hooks/exhaustive-deps 22 | [library, library.network] 23 | ) 24 | if (!library || !library.network) return null 25 | return getLogs 26 | } 27 | 28 | export default useGetLogs 29 | -------------------------------------------------------------------------------- /src/utils/graphql/permanent-registry.js: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | const PERMANENT_REGISTRY_QUERY = gql` 4 | query permanentRegistryQuery($lowerCaseTCRAddress: String!) { 5 | registry(id: $lowerCaseTCRAddress) { 6 | token 7 | numberOfSubmitted 8 | numberOfAbsent 9 | numberOfDisputed 10 | arbitrator { 11 | id 12 | } 13 | arbitrationSettings(orderBy: timestamp, orderDirection: desc) { 14 | timestamp 15 | arbitratorExtraData 16 | metaEvidenceURI 17 | metadata { 18 | title 19 | description 20 | itemName 21 | itemNamePlural 22 | policyURI 23 | logoURI 24 | requireRemovalEvidence 25 | } 26 | } 27 | submissionMinDeposit 28 | submissionPeriod 29 | reinclusionPeriod 30 | withdrawingPeriod 31 | arbitrationParamsCooldown 32 | challengeStakeMultiplier 33 | winnerStakeMultiplier 34 | loserStakeMultiplier 35 | sharedStakeMultiplier 36 | } 37 | } 38 | ` 39 | 40 | export default PERMANENT_REGISTRY_QUERY 41 | -------------------------------------------------------------------------------- /src/components/pgtcr-deposit-input.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Form, Input } from 'antd' 4 | import { Field } from 'formik' 5 | 6 | const BasedDepositContainer = styled.div` 7 | display: flex; 8 | align-items: center; 9 | ` 10 | 11 | const PGTCRDepositInput = ({ 12 | label, 13 | name, 14 | error, 15 | touched, 16 | hasFeedback, 17 | disabled 18 | }) => ( 19 |
20 | 21 | {({ field }) => ( 22 | 28 | 29 | 36 | 37 | 38 | )} 39 | 40 |
41 | ) 42 | 43 | export default PGTCRDepositInput 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kleros 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/graphql/classic-registry-items.js: -------------------------------------------------------------------------------- 1 | // fetch the items belonging to a classic registry 2 | 3 | import { gql } from '@apollo/client' 4 | 5 | const CLASSIC_REGISTRY_ITEMS_QUERY = gql` 6 | query classicRegistryItemsQuery( 7 | $offset: Int 8 | $limit: Int 9 | $order_by: [Item_order_by!] 10 | $where: Item_bool_exp 11 | ) { 12 | items: Item( 13 | offset: $offset 14 | limit: $limit 15 | order_by: $order_by 16 | where: $where 17 | ) { 18 | itemID 19 | status 20 | data 21 | requests(limit: 1, order_by: { submissionTime: desc }) { 22 | disputed 23 | disputeID 24 | submissionTime 25 | resolved 26 | requester 27 | challenger 28 | deposit 29 | rounds(limit: 1, order_by: { creationTime: desc }) { 30 | appealed 31 | appealPeriodStart 32 | appealPeriodEnd 33 | ruling 34 | hasPaidRequester 35 | hasPaidChallenger 36 | amountPaidRequester 37 | amountPaidChallenger 38 | } 39 | } 40 | } 41 | } 42 | ` 43 | 44 | export default CLASSIC_REGISTRY_ITEMS_QUERY 45 | -------------------------------------------------------------------------------- /src/hooks/arbitration-cost.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useMemo } from 'react' 2 | import { ethers } from 'ethers' 3 | import { useDebounce } from 'use-debounce' 4 | import { abi as _arbitrator } from '@kleros/erc-792/build/contracts/IArbitrator.json' 5 | 6 | const useArbitrationCost = ({ 7 | address: inputAddress, 8 | arbitratorExtraData: inputArbitratorExtraData, 9 | library 10 | }) => { 11 | const [error, setError] = useState() 12 | const [arbitrationCost, setArbitrationCost] = useState() 13 | const [address] = useDebounce(inputAddress, 1000) 14 | const [arbitratorExtraData] = useDebounce(inputArbitratorExtraData, 1000) 15 | 16 | useEffect(() => { 17 | if (!address || !library || !arbitratorExtraData) return 18 | ;(async () => { 19 | try { 20 | const arbitrator = new ethers.Contract(address, _arbitrator, library) 21 | setArbitrationCost( 22 | await arbitrator.arbitrationCost(arbitratorExtraData) 23 | ) 24 | } catch (err) { 25 | console.error('Error fetching arbitration cost', err) 26 | setError(err) 27 | } 28 | })() 29 | }, [address, arbitratorExtraData, library]) 30 | 31 | return useMemo(() => ({ arbitrationCost, error }), [arbitrationCost, error]) 32 | } 33 | 34 | export default useArbitrationCost 35 | -------------------------------------------------------------------------------- /src/utils/ipfs-publish.js: -------------------------------------------------------------------------------- 1 | import { uploadFormDataToIPFS } from './upload-form-data-to-ipfs' 2 | 3 | const mirroredExtensions = ['.json'] 4 | 5 | /** 6 | * Send file to IPFS network. 7 | * @param {string} fileName - The name that will be used to store the file. This is useful to preserve extension type. 8 | * @param {ArrayBuffer} data - The raw data from the file to upload. 9 | * @returns {object} ipfs response. Should include the hash and path of the stored item. 10 | */ 11 | export default async function ipfsPublish(fileName, data) { 12 | const isBlob = data instanceof Blob 13 | const blobFile = isBlob 14 | ? data 15 | : new Blob([data], { type: 'application/json' }) 16 | 17 | const fileFormData = new FormData() 18 | fileFormData.append('data', blobFile, fileName) 19 | 20 | if (!mirroredExtensions.some(ext => fileName.endsWith(ext))) { 21 | const result = await uploadFormDataToIPFS(fileFormData) 22 | return result 23 | } 24 | 25 | const result = await uploadFormDataToIPFS(fileFormData, 'file', true) 26 | 27 | if (result.inconsistentCids.length > 0) { 28 | console.warn('IPFS upload result is different:', { 29 | inconsistentCids: result.inconsistentCids 30 | }) 31 | throw new Error('IPFS upload result is different.') 32 | } 33 | 34 | return result 35 | } 36 | -------------------------------------------------------------------------------- /src/components/address-input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Form, Input } from 'antd' 4 | import { Field } from 'formik' 5 | import { namespaces } from 'utils/rich-address' 6 | 7 | const StyledInput = styled(Input)` 8 | text-transform: lowercase; 9 | ` 10 | 11 | const AddressInput: React.FC<{ 12 | label: string 13 | name: string 14 | placeholder: string 15 | error: string 16 | touched: boolean 17 | hasFeedback: boolean 18 | disabled: boolean 19 | style: any 20 | }> = p => ( 21 | { 25 | // namespaces[0] is eip155 26 | const valid = namespaces[0].test(value) 27 | if (!valid) return 'Invalid format' 28 | 29 | return null 30 | }} 31 | > 32 | {({ field }: any) => ( 33 | 39 | 44 | 45 | )} 46 | 47 | ) 48 | 49 | export default AddressInput 50 | -------------------------------------------------------------------------------- /src/components/gtcr-address.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import styled from 'styled-components' 3 | import { Button } from 'antd' 4 | import PropTypes from 'prop-types' 5 | import { ZERO_ADDRESS } from '../utils/string' 6 | import ETHAddress from './eth-address' 7 | 8 | const StyledButton = styled(Button)` 9 | pointer-events: auto; 10 | text-transform: capitalize; 11 | ` 12 | 13 | const StyledSpan = styled.span` 14 | margin: 0 12px 0 0; 15 | pointer-events: auto; 16 | ` 17 | 18 | const GTCRAddress = ({ address }) => { 19 | // We reload the page because the UI needs to redetect what type of TCR it is. 20 | const navigateReload = useCallback(() => { 21 | // window.location.assign(`/tcr/${utils.getAddress(address)}`) 22 | // window.reload() 23 | // eslint-disable-next-line 24 | }, [address]) 25 | 26 | // this avoids crashes when it looks for the address "Error decoding GTCR address" 27 | if (!/^0x[a-fA-F0-9]{40}$/.test(address)) return null 28 | 29 | return ( 30 | <> 31 | 32 | 33 | 34 | Visit 35 | 36 | ) 37 | } 38 | 39 | GTCRAddress.propTypes = { 40 | address: PropTypes.string.isRequired 41 | } 42 | 43 | export default GTCRAddress 44 | -------------------------------------------------------------------------------- /src/assets/images/trust.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/file-display.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Icon } from 'antd' 3 | import { getExtension } from 'mime' 4 | import { parseIpfs } from 'utils/ipfs-parse' 5 | 6 | const FileDisplay = ({ value, allowedFileTypes }) => { 7 | const [supported, setSupported] = useState(true) 8 | 9 | useEffect(() => { 10 | const check = async () => { 11 | if (!value) return 12 | if (!allowedFileTypes) { 13 | setSupported(true) 14 | return 15 | } 16 | const allowed = allowedFileTypes.split(' ') 17 | let ext = '' 18 | if (value.includes('.')) ext = value.slice(value.lastIndexOf('.') + 1) 19 | else 20 | try { 21 | const res = await fetch(parseIpfs(value), { method: 'HEAD' }) 22 | ext = getExtension(res.headers.get('content-type') || '') || '' 23 | } catch { 24 | setSupported(false) 25 | return 26 | } 27 | 28 | setSupported(allowed.includes(ext)) 29 | } 30 | check() 31 | }, [value, allowedFileTypes]) 32 | 33 | if (!value) return empty 34 | 35 | return ( 36 | <> 37 | 38 | View File 39 | 40 | {!supported && ( 41 | 42 | File type not supported 43 | 44 | )} 45 | 46 | ) 47 | } 48 | 49 | export default FileDisplay 50 | -------------------------------------------------------------------------------- /src/utils/graphql/item-details.js: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | const LIGHT_ITEM_DETAILS_QUERY = gql` 4 | query lightItemDetailsQuery($id: String!) { 5 | litem: LItem_by_pk(id: $id) { 6 | data 7 | itemID 8 | status 9 | disputed 10 | requests(order_by: { submissionTime: desc }) { 11 | requestType 12 | disputed 13 | disputeID 14 | submissionTime 15 | resolved 16 | requester 17 | arbitrator 18 | arbitratorExtraData 19 | challenger 20 | creationTx 21 | resolutionTx 22 | deposit 23 | disputeOutcome 24 | resolutionTime 25 | evidenceGroup { 26 | id 27 | evidences(order_by: { number: desc }) { 28 | party 29 | uri 30 | number 31 | timestamp 32 | txHash 33 | name 34 | title 35 | description 36 | fileURI 37 | fileTypeExtension 38 | } 39 | } 40 | rounds(order_by: { creationTime: desc }) { 41 | appealed 42 | appealPeriodStart 43 | appealPeriodEnd 44 | ruling 45 | hasPaidRequester 46 | hasPaidChallenger 47 | amountPaidRequester 48 | amountPaidChallenger 49 | txHashAppealPossible 50 | appealedAt 51 | txHashAppealDecision 52 | } 53 | } 54 | } 55 | } 56 | ` 57 | 58 | export default LIGHT_ITEM_DETAILS_QUERY 59 | -------------------------------------------------------------------------------- /src/utils/graphql/classic-item-details.js: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | const CLASSIC_ITEM_DETAILS_QUERY = gql` 4 | query classicItemDetailsQuery($id: String!) { 5 | item: Item_by_pk(id: $id) { 6 | itemID 7 | data 8 | status 9 | disputed 10 | requests(order_by: { submissionTime: desc }) { 11 | requestType 12 | disputed 13 | disputeID 14 | submissionTime 15 | resolved 16 | requester 17 | arbitrator 18 | arbitratorExtraData 19 | challenger 20 | deposit 21 | disputeOutcome 22 | resolutionTime 23 | creationTx 24 | resolutionTx 25 | evidenceGroup { 26 | id 27 | evidences(order_by: { number: desc }) { 28 | party 29 | uri 30 | number 31 | timestamp 32 | txHash 33 | name 34 | title 35 | description 36 | fileURI 37 | fileTypeExtension 38 | } 39 | } 40 | rounds(order_by: { creationTime: desc }) { 41 | appealed 42 | appealPeriodStart 43 | appealPeriodEnd 44 | ruling 45 | hasPaidRequester 46 | hasPaidChallenger 47 | amountPaidRequester 48 | amountPaidChallenger 49 | txHashAppealPossible 50 | appealedAt 51 | txHashAppealDecision 52 | } 53 | } 54 | } 55 | } 56 | ` 57 | 58 | export default CLASSIC_ITEM_DETAILS_QUERY 59 | -------------------------------------------------------------------------------- /src/utils/graphql/light-items.js: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | const LIGHT_ITEMS_QUERY = gql` 4 | query lightItemsQuery( 5 | $offset: Int 6 | $limit: Int 7 | $order_by: [LItem_order_by!] 8 | $where: LItem_bool_exp 9 | $registryId: String! 10 | ) { 11 | lregistry: LRegistry_by_pk(id: $registryId) { 12 | numberOfAbsent 13 | numberOfRegistered 14 | numberOfRegistrationRequested 15 | numberOfClearingRequested 16 | numberOfChallengedRegistrations 17 | numberOfChallengedClearing 18 | } 19 | litems: LItem( 20 | offset: $offset 21 | limit: $limit 22 | order_by: $order_by 23 | where: $where 24 | ) { 25 | itemID 26 | status 27 | data 28 | props(order_by: { label: asc }) { 29 | value 30 | type: itemType 31 | label 32 | description 33 | isIdentifier 34 | } 35 | requests(limit: 1, order_by: { submissionTime: desc }) { 36 | disputed 37 | disputeID 38 | submissionTime 39 | resolved 40 | requester 41 | challenger 42 | resolutionTime 43 | deposit 44 | rounds(limit: 1, order_by: { creationTime: desc }) { 45 | appealPeriodStart 46 | appealPeriodEnd 47 | ruling 48 | hasPaidRequester 49 | hasPaidChallenger 50 | amountPaidRequester 51 | amountPaidChallenger 52 | } 53 | } 54 | } 55 | } 56 | ` 57 | 58 | export default LIGHT_ITEMS_QUERY 59 | -------------------------------------------------------------------------------- /src/hooks/meta-evidence.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import useGetLogs from './get-logs' 3 | import { parseIpfs } from 'utils/ipfs-parse' 4 | 5 | const useMetaEvidence = ({ arbitrable, library }) => { 6 | const [metaEvidence, setMetaEvidence] = useState() 7 | const [error, setError] = useState() 8 | const getLogs = useGetLogs(library) 9 | 10 | useEffect(() => { 11 | if (!arbitrable || !library) return 12 | if (!getLogs) return 13 | ;(async () => { 14 | try { 15 | // Take the latest meta evidence. 16 | const logs = ( 17 | await getLogs({ 18 | ...arbitrable.filters.MetaEvidence(), 19 | fromBlock: 0 20 | }) 21 | ).map(log => arbitrable.interface.parseLog(log)) 22 | if (logs.length === 0) 23 | throw new Error( 24 | `No meta evidence available for TCR at. ${arbitrable.address}` 25 | ) 26 | 27 | // Take the penultimate item. This is the most recent meta evidence 28 | // for registration requests. 29 | const { _evidence: metaEvidencePath } = logs[logs.length - 2].values 30 | const file = await (await fetch(parseIpfs(metaEvidencePath))).json() 31 | 32 | setMetaEvidence({ ...file, address: arbitrable.address }) 33 | } catch (err) { 34 | console.error('Error fetching meta evidence', err) 35 | setError(err) 36 | } 37 | })() 38 | }, [arbitrable, library, getLogs]) 39 | 40 | return { metaEvidence, error } 41 | } 42 | 43 | export default useMetaEvidence 44 | -------------------------------------------------------------------------------- /src/utils/graphql/permanent-items.js: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | const PERMANENT_ITEMS_QUERY = gql` 4 | query permanentItemsQuery( 5 | $skip: Int 6 | $first: Int 7 | $orderDirection: OrderDirection 8 | $where: Item_filter 9 | $registryId: String 10 | ) { 11 | items( 12 | skip: $skip 13 | first: $first 14 | orderDirection: $orderDirection 15 | orderBy: includedAt 16 | where: $where 17 | ) { 18 | itemID 19 | status 20 | data 21 | arbitrationDeposit 22 | withdrawingTimestamp 23 | metadata { 24 | props { 25 | value 26 | type 27 | label 28 | description 29 | isIdentifier 30 | } 31 | } 32 | createdAt 33 | includedAt 34 | stake 35 | arbitrationDeposit 36 | submissions(first: 1, orderBy: createdAt, orderDirection: desc) { 37 | submitter 38 | } 39 | challenges(first: 1, orderBy: createdAt, orderDirection: desc) { 40 | disputeID 41 | createdAt 42 | resolutionTime 43 | challenger 44 | challengerStake 45 | disputeOutcome 46 | rounds(first: 1, orderBy: creationTime, orderDirection: desc) { 47 | appealPeriodStart 48 | appealPeriodEnd 49 | ruling 50 | rulingTime 51 | hasPaidRequester 52 | hasPaidChallenger 53 | amountPaidRequester 54 | amountPaidChallenger 55 | } 56 | } 57 | } 58 | } 59 | ` 60 | 61 | export default PERMANENT_ITEMS_QUERY 62 | -------------------------------------------------------------------------------- /src/components/rich-address.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Icon, Tooltip } from 'antd' 4 | import { parseRichAddress } from '../utils/rich-address' 5 | 6 | const StyledSpan = styled.span` 7 | color: red; 8 | ` 9 | 10 | const NotValidAddressAnchor = styled.a` 11 | color: #787800; 12 | text-decoration: underline; 13 | ` 14 | 15 | const ValidAddressAnchor = styled.a` 16 | text-decoration: underline; 17 | ` 18 | 19 | const RichAddress: React.FC<{ crude: string }> = ({ crude }) => { 20 | const richAddress = parseRichAddress(crude) 21 | if (richAddress === null) { 22 | console.log('Address has wrong format or unknown prepend', crude) 23 | const errorMessage = `Unknown address "${crude}"` 24 | return {errorMessage} 25 | } 26 | const { address, reference, link, passedTest } = richAddress 27 | const labelText = `${reference.label}: ` 28 | if (!passedTest) 29 | return ( 30 | 31 | 32 |   33 | 34 | {labelText} 35 | {address.slice(0, 6)}...{address.slice(address.length - 4)} 36 | 37 | 38 | ) 39 | 40 | return ( 41 | 42 | {labelText} 43 | {address.slice(0, 6)}...{address.slice(address.length - 4)} 44 | 45 | ) 46 | } 47 | 48 | export default RichAddress 49 | -------------------------------------------------------------------------------- /src/components/permanent-item-card-content.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Button } from 'antd' 4 | import DisplaySelector from './display-selector' 5 | import { ItemTypes } from '@kleros/gtcr-encoder' 6 | import useNavigateAndScrollTop from 'hooks/navigate-and-scroll-top' 7 | 8 | export const Container = styled.div` 9 | display: flex; 10 | height: 100%; 11 | flex-direction: column; 12 | justify-content: space-between; 13 | align-items: center; 14 | ` 15 | 16 | export const StyledItemCol = styled.div` 17 | margin-bottom: 8px; 18 | text-align: center; 19 | ` 20 | 21 | const PermanentItemCardContent = ({ item, chainId, tcrAddress }) => { 22 | const navigateAndScrollTop = useNavigateAndScrollTop() 23 | 24 | return ( 25 | 26 |
27 | {item.metadata.props 28 | .filter( 29 | col => 30 | col.isIdentifier || 31 | col.type === ItemTypes.IMAGE || 32 | col.type === ItemTypes.FILE 33 | ) 34 | .map((column, j) => ( 35 | 36 | 41 | 42 | ))} 43 |
44 | 51 |
52 | ) 53 | } 54 | 55 | export default PermanentItemCardContent 56 | -------------------------------------------------------------------------------- /src/components/permanent-item-action-button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Button } from 'antd' 4 | import styled from 'styled-components' 5 | import { STATUS_CODE, getActionLabel } from '../utils/permanent-item-status' 6 | 7 | const StyledButton = styled(Button)` 8 | text-transform: capitalize; 9 | ` 10 | 11 | const ItemActionButton = ({ statusCode, itemName, itemID, onClick, type }) => { 12 | if ((!statusCode && statusCode !== 0) || !itemName || !itemID) 13 | return ( 14 | 17 | ) 18 | 19 | const disabled = 20 | statusCode === STATUS_CODE.WAITING_ARBITRATOR || 21 | statusCode === STATUS_CODE.DISPUTED 22 | 23 | return ( 24 | 39 | {getActionLabel({ statusCode, itemName })} 40 | 41 | ) 42 | } 43 | 44 | ItemActionButton.propTypes = { 45 | statusCode: PropTypes.number, 46 | itemName: PropTypes.string, 47 | itemID: PropTypes.string, 48 | onClick: PropTypes.func, 49 | type: PropTypes.string 50 | } 51 | 52 | ItemActionButton.defaultProps = { 53 | statusCode: null, 54 | itemName: null, 55 | itemID: null, 56 | onClick: null, 57 | type: null 58 | } 59 | 60 | export default ItemActionButton 61 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | Kleros · Curate 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/pages/items-router.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TCRViewProvider } from 'contexts/tcr-view-context' 3 | import { LightTCRViewProvider } from 'contexts/light-tcr-view-context' 4 | import loadable from '@loadable/component' 5 | import { useParams } from 'react-router' 6 | import Loading from 'components/loading' 7 | import useTcrNetwork from 'hooks/use-tcr-network' 8 | import { NETWORK_STATUS } from 'config/networks' 9 | import useCheckLightCurate from 'hooks/use-check-light-curate' 10 | 11 | const LightItems = loadable( 12 | () => import(/* webpackPrefetch: true */ './light-items/index'), 13 | { 14 | fallback: 15 | } 16 | ) 17 | 18 | const Items = loadable( 19 | () => import(/* webpackPrefetch: true */ './items/index'), 20 | { 21 | fallback: 22 | } 23 | ) 24 | 25 | const PermanentItems = loadable( 26 | () => import(/* webpackPrefetch: true */ './permanent-items/index'), 27 | { 28 | fallback: 29 | } 30 | ) 31 | 32 | const ItemsRouter = () => { 33 | const { tcrAddress } = useParams<{ tcrAddress: string }>() 34 | const { isLightCurate, isClassicCurate, checking } = useCheckLightCurate() 35 | const { networkStatus } = useTcrNetwork() 36 | 37 | if (checking || networkStatus !== NETWORK_STATUS.supported) return 38 | 39 | if (isLightCurate) 40 | return ( 41 | 42 | 43 | 44 | ) 45 | else if (isClassicCurate) 46 | return ( 47 | 48 | 49 | 50 | ) 51 | else return 52 | } 53 | 54 | export default ItemsRouter 55 | -------------------------------------------------------------------------------- /src/components/item-action-button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Button } from 'antd' 4 | import styled from 'styled-components' 5 | import { STATUS_CODE, getActionLabel } from '../utils/item-status' 6 | 7 | const StyledButton = styled(Button)` 8 | text-transform: capitalize; 9 | ` 10 | 11 | const ItemActionButton = ({ statusCode, itemName, itemID, onClick, type }) => { 12 | if ((!statusCode && statusCode !== 0) || !itemName || !itemID) 13 | return ( 14 | 17 | ) 18 | 19 | const disabled = 20 | statusCode === STATUS_CODE.WAITING_ARBITRATOR || 21 | statusCode === STATUS_CODE.CHALLENGED || 22 | statusCode === STATUS_CODE.WAITING_ENFORCEMENT 23 | 24 | return ( 25 | 40 | {getActionLabel({ statusCode, itemName })} 41 | 42 | ) 43 | } 44 | 45 | ItemActionButton.propTypes = { 46 | statusCode: PropTypes.number, 47 | itemName: PropTypes.string, 48 | itemID: PropTypes.string, 49 | onClick: PropTypes.func, 50 | type: PropTypes.string 51 | } 52 | 53 | ItemActionButton.defaultProps = { 54 | statusCode: null, 55 | itemName: null, 56 | itemID: null, 57 | onClick: null, 58 | type: null 59 | } 60 | 61 | export default ItemActionButton 62 | -------------------------------------------------------------------------------- /src/contexts/tour-context.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useEffect, useCallback } from 'react' 2 | import localforage from 'localforage' 3 | import PropTypes from 'prop-types' 4 | 5 | const WELCOME_MODAL_DISMISSED = 'WELCOME_MODAL_DISMISSED' 6 | const NOTIFICATION_TOUR_DISMISSED = 'NOTIFICATION_TOUR_DISMISSED' 7 | 8 | const useTourContext = () => { 9 | const [welcomeModalDismissed, setWelcomeModalDismissed] = useState(true) 10 | const [notificationTourDismissed, setNotificationTourDismissed] = useState( 11 | false 12 | ) 13 | const [userSubscribed, setUserSubscribed] = useState(false) 14 | 15 | useEffect(() => { 16 | ;(async () => { 17 | setWelcomeModalDismissed( 18 | (await localforage.getItem(WELCOME_MODAL_DISMISSED)) || false 19 | ) 20 | setNotificationTourDismissed( 21 | (await localforage.getItem(NOTIFICATION_TOUR_DISMISSED)) || false 22 | ) 23 | })() 24 | }, []) 25 | 26 | const dismissWelcomeModal = useCallback(() => { 27 | setWelcomeModalDismissed(true) 28 | localforage.setItem(WELCOME_MODAL_DISMISSED, true) 29 | }, []) 30 | 31 | const dismissNotificationsTour = useCallback(() => { 32 | setNotificationTourDismissed(true) 33 | localforage.setItem(NOTIFICATION_TOUR_DISMISSED, true) 34 | }, []) 35 | 36 | return { 37 | welcomeModalDismissed, 38 | dismissWelcomeModal, 39 | notificationTourDismissed, 40 | setUserSubscribed, 41 | dismissNotificationsTour, 42 | userSubscribed 43 | } 44 | } 45 | 46 | const TourContext = createContext() 47 | const TourProvider = ({ children }) => ( 48 | 49 | {children} 50 | 51 | ) 52 | 53 | TourProvider.propTypes = { 54 | children: PropTypes.node.isRequired 55 | } 56 | 57 | export { TourContext, TourProvider } 58 | -------------------------------------------------------------------------------- /src/pages/item-details-router.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TCRViewProvider } from 'contexts/tcr-view-context' 3 | import { LightTCRViewProvider } from 'contexts/light-tcr-view-context' 4 | import loadable from '@loadable/component' 5 | import { useParams } from 'react-router' 6 | import useTcrNetwork from 'hooks/use-tcr-network' 7 | import { NETWORK_STATUS } from 'config/networks' 8 | import useCheckLightCurate from 'hooks/use-check-light-curate' 9 | import Loading from 'components/loading' 10 | 11 | const PermanentItemDetails = loadable( 12 | () => import(/* webpackPrefetch: true */ './permanent-item-details/index'), 13 | { 14 | fallback: 15 | } 16 | ) 17 | 18 | const LightItemDetails = loadable( 19 | () => import(/* webpackPrefetch: true */ './light-item-details/index'), 20 | { 21 | fallback: 22 | } 23 | ) 24 | 25 | const ItemDetails = loadable( 26 | () => import(/* webpackPrefetch: true */ './item-details/index'), 27 | { 28 | fallback: 29 | } 30 | ) 31 | 32 | const ItemDetailsRouter = () => { 33 | const { tcrAddress, itemID } = useParams<{ 34 | tcrAddress: string 35 | itemID: string 36 | }>() 37 | const { networkStatus } = useTcrNetwork() 38 | const search = window.location.search 39 | const { isLightCurate, isClassicCurate, checking } = useCheckLightCurate() 40 | 41 | if (checking || networkStatus !== NETWORK_STATUS.supported) return 42 | 43 | if (isLightCurate) 44 | return ( 45 | 46 | 47 | 48 | ) 49 | else if (isClassicCurate) 50 | return ( 51 | 52 | 53 | 54 | ) 55 | 56 | return 57 | } 58 | 59 | export default ItemDetailsRouter 60 | -------------------------------------------------------------------------------- /src/pages/light-item-details/tour-steps.js: -------------------------------------------------------------------------------- 1 | import { capitalizeFirstLetter } from 'utils/string' 2 | 3 | export const itemTourSteps = metadata => { 4 | const { itemName, itemNamePlural } = metadata || {} 5 | 6 | return [ 7 | { 8 | content: `The ${(itemName && itemName.toLowerCase()) || 9 | 'item'} details view displays some key information about a specific ${(itemName && 10 | itemName.toLowerCase()) || 11 | 'item'}.` 12 | }, 13 | { 14 | selector: `#item-status-card`, 15 | content: `Here you can find information about the current status of the ${(itemName && 16 | itemName.toLowerCase()) || 17 | 'item'} and the challenge bounty among others.` 18 | }, 19 | { 20 | selector: `#item-action-button`, 21 | content: `Here you will find available actions for the ${(itemName && 22 | itemName.toLowerCase()) || 23 | 'item'}. ${(itemNamePlural && capitalizeFirstLetter(itemNamePlural)) || 24 | 'Items'} can be submitted, removed or challenged depending on their status.` 25 | }, 26 | { 27 | selector: `#item-details-card`, 28 | content: `This is the ${(itemName && itemName.toLowerCase()) || 29 | 'item'} details card. These are important fields to check against the listing criteria of this list.` 30 | }, 31 | { 32 | selector: `#request-timelines`, 33 | content: `This is the ${(itemName && itemName.toLowerCase()) || 34 | 'item'} history card. Here you will find important information of ongoing submissions and removal requests such as rulings, evidence and appeals. If there is a dispute, this is also where you will submit evidence.` 35 | }, 36 | { 37 | selector: `#badges`, 38 | content: `This is the badges section. Badges are an easy way to see if the ${(itemName && 39 | itemName.toLowerCase()) || 40 | 'item'} is present on another list, or to submit it.` 41 | } 42 | ] 43 | } 44 | 45 | export default itemTourSteps 46 | -------------------------------------------------------------------------------- /src/pages/error-page/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import PropTypes from 'prop-types' 4 | import { ReactComponent as Acropolis } from 'assets/images/acropolis.svg' 5 | 6 | const StyledDiv = styled.div` 7 | display: flex; 8 | flex-direction: column; 9 | height: 100%; 10 | ` 11 | const StyledAcropolis = styled(Acropolis)` 12 | height: auto; 13 | width: 100%; 14 | ` 15 | const StyledInfoDiv = styled.div` 16 | flex: 1; 17 | padding: 0 9.375vw 62px; 18 | text-align: center; 19 | ` 20 | const Styled404Div = styled.div` 21 | font-size: 88px; 22 | font-weight: bold; 23 | line-height: 112px; 24 | ` 25 | const StyledMessageLine1 = styled.div` 26 | font-size: 28px; 27 | font-weight: bold; 28 | ` 29 | const StyledMessageLine2 = styled.div` 30 | font-size: 24px; 31 | ` 32 | const StyledMessageLine3 = styled.div` 33 | font-size: 16px; 34 | margin-top: 25px; 35 | ` 36 | const ErrorPage = ({ code, title, message, tip }) => ( 37 | 38 | 39 | 40 | 41 | {code || '404'} 42 | 43 | 44 | {title} 45 | 46 | 47 | {message || 'The gods could not find the page you are looking for!'} 48 | 49 | 50 | {tip} 51 | 52 | 53 | 54 | ) 55 | 56 | ErrorPage.propTypes = { 57 | code: PropTypes.string, 58 | title: PropTypes.string, 59 | message: PropTypes.string, 60 | tip: PropTypes.oneOfType([PropTypes.string, PropTypes.object]) 61 | } 62 | 63 | ErrorPage.defaultProps = { 64 | code: null, 65 | title: null, 66 | message: null, 67 | tip: null 68 | } 69 | 70 | export default ErrorPage 71 | -------------------------------------------------------------------------------- /src/config/connectors.js: -------------------------------------------------------------------------------- 1 | import { Connectors } from 'web3-react' 2 | import WalletConnectApi from '@walletconnect/web3-subprovider' 3 | import FortmaticApi from 'fortmatic' 4 | import { NETWORKS_INFO, NETWORKS } from 'config/networks' 5 | import { SAVED_NETWORK_KEY } from '../utils/string' 6 | import getNetworkEnv from '../utils/network-env' 7 | 8 | const { 9 | NetworkOnlyConnector, 10 | InjectedConnector, 11 | LedgerConnector, 12 | FortmaticConnector, 13 | WalletConnectConnector 14 | } = Connectors 15 | 16 | const connectors = {} 17 | 18 | const defaultNetwork = Number( 19 | localStorage.getItem(SAVED_NETWORK_KEY) ?? 20 | process.env.REACT_APP_DEFAULT_NETWORK 21 | ) 22 | 23 | if (process.env.REACT_APP_RPC_URLS) { 24 | const supportedNetworkURLs = JSON.parse(process.env.REACT_APP_RPC_URLS) 25 | connectors.Infura = new NetworkOnlyConnector({ 26 | providerURL: supportedNetworkURLs[defaultNetwork] 27 | }) 28 | connectors.xDai = new NetworkOnlyConnector({ 29 | providerURL: supportedNetworkURLs[NETWORKS.gnosis] 30 | }) 31 | 32 | connectors.Ledger = new LedgerConnector({ 33 | supportedNetworkURLs, 34 | defaultNetwork 35 | }) 36 | 37 | if (process.env.REACT_APP_WALLETCONNECT_BRIDGE_URL) 38 | connectors.WalletConnect = new WalletConnectConnector({ 39 | api: WalletConnectApi, 40 | bridge: process.env.REACT_APP_WALLETCONNECT_BRIDGE_URL, 41 | supportedNetworkURLs, 42 | defaultNetwork 43 | }) 44 | } 45 | 46 | const fortmaticApiKey = getNetworkEnv('REACT_APP_FORMATIC_API_KEYS') 47 | if (fortmaticApiKey) 48 | connectors.Fortmatic = new FortmaticConnector({ 49 | api: FortmaticApi, 50 | apiKey: fortmaticApiKey, 51 | logoutOnDeactivation: false, 52 | testNetwork: 53 | defaultNetwork === NETWORKS.ethereum 54 | ? null 55 | : NETWORKS_INFO[defaultNetwork].name 56 | }) 57 | 58 | if (window.ethereum) 59 | connectors.Injected = new InjectedConnector({ 60 | supportedNetworks: [NETWORKS.ethereum, NETWORKS.gnosis, NETWORKS.sepolia] 61 | }) 62 | 63 | export default connectors 64 | -------------------------------------------------------------------------------- /src/components/item-card-content.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import DisplaySelector from './display-selector' 4 | import { ItemTypes } from '@kleros/gtcr-encoder' 5 | import { Button } from 'antd' 6 | import useNavigateAndScrollTop from 'hooks/navigate-and-scroll-top' 7 | import { StyledItemCol } from './light-tcr-card-content' 8 | import { Container } from './light-item-card-content' 9 | 10 | const ItemCardContent = ({ item, chainId, tcrAddress }) => { 11 | const navigateAndScrollTop = useNavigateAndScrollTop() 12 | 13 | return ( 14 | 15 |
16 | {item.columns 17 | .filter( 18 | col => 19 | col.isIdentifier || 20 | col.type === ItemTypes.IMAGE || 21 | col.type === ItemTypes.FILE 22 | ) 23 | .map((column, j) => ( 24 | 25 | 30 | 31 | ))} 32 |
33 | 42 |
43 | ) 44 | } 45 | 46 | ItemCardContent.propTypes = { 47 | item: PropTypes.shape({ 48 | columns: PropTypes.arrayOf( 49 | PropTypes.shape({ 50 | isIdentifier: PropTypes.bool, 51 | type: PropTypes.oneOf(Object.values(ItemTypes)), 52 | value: PropTypes.oneOfType([ 53 | PropTypes.bool, 54 | PropTypes.string, 55 | PropTypes.number, 56 | PropTypes.object 57 | ]) 58 | }) 59 | ), 60 | tcrData: PropTypes.shape({ 61 | ID: PropTypes.string.isRequired 62 | }).isRequired 63 | }).isRequired, 64 | tcrAddress: PropTypes.string.isRequired 65 | } 66 | 67 | export default ItemCardContent 68 | -------------------------------------------------------------------------------- /src/components/tour.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback, useContext } from 'react' 2 | import Tour from 'reactour' 3 | import localforage from 'localforage' 4 | import PropTypes from 'prop-types' 5 | import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock' 6 | import { TourContext } from '../contexts/tour-context' 7 | 8 | const AppTour = ({ steps, dismissedKey }) => { 9 | const [dismissed, setDismissed] = useState(true) 10 | const { welcomeModalDismissed } = useContext(TourContext) 11 | 12 | const disableBody = target => disableBodyScroll(target) 13 | const enableBody = target => enableBodyScroll(target) 14 | 15 | useEffect(() => { 16 | ;(async () => { 17 | const wasDismissed = (await localforage.getItem(dismissedKey)) || false 18 | setDismissed(wasDismissed) 19 | })() 20 | }, [dismissedKey]) 21 | 22 | const dontShowAgain = useCallback(() => { 23 | setDismissed(true) 24 | localforage.setItem(dismissedKey, true) 25 | }, [dismissedKey]) 26 | 27 | return ( 28 | 37 | ) 38 | } 39 | 40 | AppTour.propTypes = { 41 | dismissedKey: PropTypes.string.isRequired, 42 | steps: PropTypes.arrayOf( 43 | PropTypes.shape({ 44 | selector: PropTypes.string, 45 | content: PropTypes.oneOfType([ 46 | PropTypes.node, 47 | PropTypes.element, 48 | PropTypes.func 49 | ]).isRequired, 50 | position: PropTypes.oneOfType([ 51 | PropTypes.arrayOf(PropTypes.number), 52 | PropTypes.oneOf(['top', 'right', 'bottom', 'left', 'center']) 53 | ]), 54 | action: PropTypes.func, 55 | // eslint-disable-next-line react/forbid-prop-types 56 | style: PropTypes.object, 57 | stepInteraction: PropTypes.bool, 58 | navDotAriaLabel: PropTypes.string 59 | }) 60 | ).isRequired 61 | } 62 | 63 | export default AppTour 64 | -------------------------------------------------------------------------------- /src/components/custom-input.js: -------------------------------------------------------------------------------- 1 | import { Form, Input } from 'antd' 2 | import { Field } from 'formik' 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | import { ItemTypes } from '@kleros/gtcr-encoder' 6 | 7 | const CustomInput = ({ 8 | label, 9 | name, 10 | placeholder, 11 | error, 12 | touched, 13 | addonAfter, 14 | hasFeedback, 15 | type, 16 | step, 17 | disabled, 18 | style 19 | }) => ( 20 | 21 | {({ field }) => ( 22 | 28 | {type === ItemTypes.NUMBER ? ( 29 | 36 | ) : ( 37 | 43 | )} 44 | 45 | )} 46 | 47 | ) 48 | 49 | CustomInput.propTypes = { 50 | name: PropTypes.string.isRequired, 51 | label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), 52 | placeholder: PropTypes.string, 53 | error: PropTypes.string, 54 | touched: PropTypes.bool, 55 | addonAfter: PropTypes.node, 56 | hasFeedback: PropTypes.bool, 57 | type: PropTypes.oneOf(Object.values(ItemTypes)), 58 | step: PropTypes.number, 59 | disabled: PropTypes.bool, 60 | // eslint-disable-next-line react/require-default-props 61 | // eslint-disable-next-line react/forbid-prop-types 62 | style: PropTypes.object 63 | } 64 | 65 | CustomInput.defaultProps = { 66 | label: null, 67 | placeholder: '', 68 | error: null, 69 | touched: null, 70 | addonAfter: null, 71 | hasFeedback: null, 72 | type: null, 73 | step: null, 74 | disabled: null, 75 | style: null 76 | } 77 | 78 | export default CustomInput 79 | -------------------------------------------------------------------------------- /src/components/beta-warning.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback } from 'react' 2 | import styled, { css } from 'styled-components' 3 | import { smallScreenStyle } from 'styles/small-screen-style' 4 | import { Alert } from 'antd' 5 | import localforage from 'localforage' 6 | import TextLoop from 'react-text-loop' 7 | import useWindowDimensions from '../hooks/window-dimensions' 8 | 9 | const BETA_WARNING_DISMISSED = 'BETA_WARNING_DISMISSED' 10 | 11 | const BannerContainer = styled.div` 12 | background-color: #fffbe6; 13 | z-index: 1000; 14 | padding: 0 8%; 15 | 16 | ${smallScreenStyle( 17 | () => css` 18 | padding: 0; 19 | ` 20 | )} 21 | ` 22 | 23 | const BannerText = ( 24 | 25 |
26 | Warning: This is beta software. There is{' '} 27 | 28 | a bug bounty on Curate. 29 | {' '} 30 | Participate for a chance to win up to 100 ETH. 31 |
32 |
33 | ) 34 | 35 | const LoopBannerText = ( 36 | 37 |
Warning: This is beta software.
38 |
Win up to 25 ETH by...
39 |
40 | ...participating on the{' '} 41 | bug bounty.{' '} 42 |
43 |
44 | ) 45 | 46 | const WarningBanner = () => { 47 | const [dismissed, setDismissed] = useState(true) 48 | const { width } = useWindowDimensions() 49 | 50 | useEffect(() => { 51 | ;(async () => { 52 | const wasDismissed = 53 | (await localforage.getItem(BETA_WARNING_DISMISSED)) || false 54 | setDismissed(wasDismissed) 55 | })() 56 | }, []) 57 | const onClose = useCallback( 58 | () => localforage.setItem(BETA_WARNING_DISMISSED, true), 59 | [] 60 | ) 61 | 62 | if (dismissed !== false) return
63 | 64 | return ( 65 | 66 | 73 | 74 | ) 75 | } 76 | 77 | export default WarningBanner 78 | -------------------------------------------------------------------------------- /src/pages/permanent-item-details/item-action-modal.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import { STATUS_CODE, getActionLabel } from 'utils/permanent-item-status' 3 | import ChallengeModal from './modals/challenge' 4 | import SubmitModal from './modals/submit' 5 | import CrowdfundModal from './modals/crowdfund' 6 | 7 | const ItemActionModal = ({ 8 | statusCode, 9 | isOpen, 10 | itemName, 11 | onClose, 12 | fileURI, 13 | item, 14 | metaEvidence, 15 | appealCost, 16 | arbitrationCost 17 | }) => { 18 | // Common button properties. 19 | const rest = { 20 | visible: isOpen, 21 | title: getActionLabel({ statusCode, itemName }), 22 | onCancel: onClose 23 | } 24 | const r = useMemo(() => item?.registry, [item]) 25 | 26 | switch (statusCode) { 27 | case STATUS_CODE.ACCEPTED: 28 | case STATUS_CODE.PENDING: 29 | return ( 30 | 38 | ) 39 | case STATUS_CODE.ABSENT: 40 | return ( 41 | 53 | ) 54 | case STATUS_CODE.CROWDFUNDING: 55 | case STATUS_CODE.CROWDFUNDING_WINNER: 56 | return ( 57 | 64 | ) 65 | case STATUS_CODE.WAITING_ARBITRATOR: 66 | case STATUS_CODE.PENDING_WITHDRAWAL: 67 | return null 68 | default: 69 | throw new Error(`Unhandled status code ${statusCode}`) 70 | } 71 | } 72 | 73 | export default ItemActionModal 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Generalized Token Curated List 3 |

4 | 5 |

6 | JavaScript Style Guide 7 | Conventional Commits 8 | Commitizen Friendly 9 | 10 |

11 | 12 | ## Get Started 13 | 14 | 1. Clone this repo. 15 | 2. Duplicate `.env.example` and rename it to `.env`. Fill the environment variables. 16 | 3. Run `nvm use 16` 17 | 4. Run `yarn` to install dependencies and then `yarn build:theme && yarn start` to start the dev server. 18 | 19 | > Tested on node version 10. 20 | 21 | ## Supporting New Field Types 22 | 23 | The Generalized TCR clients can learn how to parse and decode data stored onchain by reading what are the field types of each column from the `metadata` object stored on the meta evidence file. 24 | 25 | ### Important 26 | 27 | To support a new field type, it is required to update the evidence display interface as well. Otherwise it might not know how to parse it and crash on arbitrator clients, preventing them from properly judging a case. 28 | 29 | The evidence display interface code of the Generalized TCR can be found at [https://github.com/kleros/gtcr-injected-uis](https://github.com/kleros/gtcr-injected-uis). 30 | 31 | ## Other Scripts 32 | 33 | - `yarn format` - Lint, fix and prettify all the project. 34 | - `yarn run cz` - Run commitizen. 35 | - `yarn run build` - Create a production build. 36 | 37 | ## Netlify Deployment 38 | 39 | When setting up the repo for publishing on netlify: 40 | 1. Fill the env variables in netlify.toml; 41 | 2. Set the following environment variables on the site's build config in netlify's dashboard: 42 | ``` 43 | REACT_APP_RPC_URLS 44 | REACT_APP_FORMATIC_API_KEYS 45 | REACT_APP_NOTIFICATIONS_API_URL 46 | ``` -------------------------------------------------------------------------------- /src/components/custom-registries/seer/use-seer-markets-data.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { isSeerRegistry } from 'components/custom-registries/seer/is-seer-registry' 3 | 4 | const useSeerMarketsData = ( 5 | chainId: string, 6 | tcrAddress: string, 7 | items: any[] 8 | ) => { 9 | const [seerMarketsData, setSeerMarketsData] = useState({}) 10 | 11 | useEffect(() => { 12 | if (!isSeerRegistry(tcrAddress, chainId) || !items || items.length === 0) 13 | return 14 | 15 | const fetchSeerData = async () => { 16 | const contractAddresses = items 17 | .map(item => item?.decodedData?.[1]?.toLowerCase()) 18 | .filter(Boolean) 19 | if (contractAddresses.length === 0) return 20 | 21 | try { 22 | let subgraphUrl = '' 23 | if (chainId === '1') 24 | subgraphUrl = process.env.REACT_APP_SEER_SUBGRAPH_MAINNET ?? '' 25 | else if (chainId === '100') 26 | subgraphUrl = process.env.REACT_APP_SEER_SUBGRAPH_GNOSIS ?? '' 27 | const query = ` 28 | { 29 | markets(where: {id_in: [${contractAddresses 30 | .map(addr => `"${addr}"`) 31 | .join(',')}]}) { 32 | id 33 | marketName 34 | outcomes 35 | } 36 | } 37 | ` 38 | const response = await fetch(subgraphUrl, { 39 | method: 'POST', 40 | headers: { 'Content-Type': 'application/json' }, 41 | body: JSON.stringify({ query }) 42 | }) 43 | if (!response.ok) throw new Error('Seer subgraph query failed') 44 | const data = await response.json() 45 | const markets = data.data.markets 46 | const marketsData = markets.reduce((acc: any[], market: any) => { 47 | acc[market.id] = { 48 | marketName: market.marketName, 49 | outcomes: market.outcomes 50 | } 51 | return acc 52 | }, {}) 53 | setSeerMarketsData(marketsData) 54 | } catch (err) { 55 | console.error('Failed to fetch Seer markets:', err) 56 | } 57 | } 58 | 59 | fetchSeerData() 60 | }, [chainId, tcrAddress, items]) 61 | 62 | return seerMarketsData 63 | } 64 | 65 | export default useSeerMarketsData 66 | -------------------------------------------------------------------------------- /src/components/tcr-metadata-display.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Icon, Tooltip } from 'antd' 4 | import { ItemTypes } from '@kleros/gtcr-encoder' 5 | import PropTypes from 'prop-types' 6 | import DisplaySelector from './display-selector' 7 | import { parseIpfs } from 'utils/ipfs-parse' 8 | 9 | const StyledField = styled.div` 10 | margin-bottom: 16px; 11 | margin-right: 16px; 12 | word-break: break-word; 13 | ` 14 | 15 | const TCRMetadataDisplay = ({ logoURI, tcrTitle, tcrDescription, fileURI }) => ( 16 | <> 17 | 18 | 19 | Logo: 20 | 21 |   22 | 23 | 24 | 25 | : 26 | 27 | 28 | 29 | Title 30 | 31 |   32 | 33 | 34 | 35 | : 36 | 37 | 38 | 39 | Description 40 | 41 |   42 | 43 | 44 | 45 | : 46 | 47 | 48 | 49 | Primary document 50 | 51 |   52 | 53 | 54 | 55 | :{' '} 56 | 57 | Link 58 | 59 | 60 | 61 | ) 62 | 63 | TCRMetadataDisplay.propTypes = { 64 | tcrTitle: PropTypes.string.isRequired, 65 | tcrDescription: PropTypes.string.isRequired, 66 | logoURI: PropTypes.string, 67 | fileURI: PropTypes.string.isRequired 68 | } 69 | 70 | TCRMetadataDisplay.defaultProps = { 71 | logoURI: null 72 | } 73 | 74 | export default TCRMetadataDisplay 75 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_IPFS_GATEWAY=https://cdn.kleros.link 2 | REACT_APP_HOSTED_GRAPH_IPFS_ENDPOINT=https://api.thegraph.com/ipfs 3 | REACT_APP_FRAME_SITE_URL=https://frame.sh/ 4 | REACT_APP_METAMASK_SITE_URL=https://metamask.io 5 | REACT_APP_TRUST_SITE_URL=https://trustwallet.com 6 | REACT_APP_DEFAULT_NETWORK=1 7 | REACT_APP_REJECT_ALL_POLICY_URI="/ipfs/QmZ7RVU7re1g8nXDbAFMHV99pyie3dn4cY7Ga2X4h8mDpV/reject-all-policy.pdf" 8 | REACT_APP_COURT_FUNCTIONS_URL=https://kleros-api.netlify.app 9 | 10 | # All fields below this line are optional ------------- 11 | 12 | REACT_APP_WALLETCONNECT_BRIDGE_URL=https://bridge.walletconnect.org 13 | REACT_APP_NOTIFICATIONS_API_URL=http://localhost:3001 14 | # JSON string mapping networkID to JSON RPC url. 15 | # Example: REACT_APP_RPC_URLS={"1":"https://mainnet.infura.io/v3/1337deadbeef...", "11155111":"https://sepolia.infura.io/v3/1337deadbeef..."} 16 | REACT_APP_RPC_URLS={"100":"https://rpc.gnosischain.com","1":"https://mainnet.infura.io/v3/","11155111":"https://sepolia.infura.io/v3/"} 17 | 18 | # JSON string mapping networkID to Fortmatic api key. 19 | # Example: REACT_APP_FORMATIC_API_KEYS={"1":"pk_live_FCEE7548E5...", "42":"pk_live_0916382..."} 20 | REACT_APP_FORMATIC_API_KEYS= 21 | 22 | # If provided, the welcome modal display the following video. 23 | REACT_APP_INSTRUCTION_VIDEO=https://www.youtube.com/embed/DKPVWzhh8Y8 24 | 25 | REACT_APP_SUBGRAPH_MAINNET=https://api.studio.thegraph.com/query/61738/legacy-curate-mainnet/version/latest 26 | REACT_APP_SUBGRAPH_GNOSIS=https://api.studio.thegraph.com/query/61738/legacy-curate-gnosis/version/latest 27 | REACT_APP_SUBGRAPH_SEPOLIA=https://api.studio.thegraph.com/query/61738/legacy-curate-sepolia/version/latest 28 | 29 | REACT_APP_SUBGRAPH_MAINNET_PERMANENT=https://api.studio.thegraph.com/query/108432/pgtcr-mainnet/version/latest 30 | REACT_APP_SUBGRAPH_GNOSIS_PERMANENT=https://api.studio.thegraph.com/query/64150/pgtcr-gnosis/version/latest 31 | REACT_APP_SUBGRAPH_SEPOLIA_PERMANENT=https://api.studio.thegraph.com/query/108432/pgtcr-sepolia/version/latest 32 | 33 | # For the Seer registries in Mainnet and Gnosis. 34 | REACT_APP_SEER_SUBGRAPH_MAINNET=https://gateway.thegraph.com/api/[api-key]/subgraphs/id/BMQD869m8LnGJJfqMRjcQ16RTyUw6EUx5jkh3qWhSn3M 35 | REACT_APP_SEER_SUBGRAPH_GNOSIS=https://gateway.thegraph.com/api/[api-key]/subgraphs/id/B4vyRqJaSHD8dRDb3BFRoAzuBK18c1QQcXq94JbxDxWH -------------------------------------------------------------------------------- /src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled, { css } from 'styled-components' 3 | import { smallScreenStyle } from 'styles/small-screen-style' 4 | import icons from './social-icons' 5 | 6 | const StyledFooter = styled.footer` 7 | display: flex; 8 | width: 100%; 9 | padding: 24px 48px; 10 | background: #4d00b4; 11 | flex-wrap: wrap; 12 | justify-content: space-between; 13 | align-items: center; 14 | gap: 20px; 15 | 16 | ${smallScreenStyle( 17 | () => css` 18 | flex-direction: column; 19 | justify-content: center; 20 | ` 21 | )} 22 | ` 23 | 24 | const SocialLinksContainer = styled.div` 25 | display: grid; 26 | gap: 16px; 27 | grid: 1fr / 1fr 1fr 1fr 1fr 1fr 1fr 1fr; 28 | align-items: center; 29 | text-align: center; 30 | ` 31 | 32 | const StyledSecuredByKleros = styled.a` 33 | display: flex; 34 | align-items: center; 35 | color: white; 36 | ` 37 | 38 | const StyledSocialLink = styled.a` 39 | color: white; 40 | text-decoration: none; 41 | ` 42 | 43 | const SOCIAL_NAV = [ 44 | { 45 | icon: icons.github, 46 | href: 'https://github.com/kleros' 47 | }, 48 | { 49 | icon: icons.slack, 50 | href: 'https://slack.kleros.io/' 51 | }, 52 | { 53 | icon: icons.reddit, 54 | href: 'https://reddit.com/r/Kleros/' 55 | }, 56 | { 57 | icon: icons.x, 58 | href: 'https://x.com/kleros_io?' 59 | }, 60 | { 61 | icon: icons.blog, 62 | href: 'https://blog.kleros.io/' 63 | }, 64 | { 65 | icon: icons.telegram, 66 | href: 'https://t.me/kleros' 67 | }, 68 | { 69 | icon: icons.linkedin, 70 | href: 'https://www.linkedin.com/company/kleros/' 71 | } 72 | ] 73 | 74 | const Footer = () => ( 75 | 76 | 77 | {icons.securedByKleros} 78 | 79 | 80 | {SOCIAL_NAV.map((item, index) => ( 81 | 82 | {item.icon} 83 | 84 | ))} 85 | 86 | 87 | ) 88 | 89 | export default Footer 90 | 91 | const SocialLink: React.FC<{ 92 | children: React.ReactNode 93 | href: string 94 | }> = ({ children, href }) => ( 95 | 96 | {children} 97 | 98 | ) 99 | -------------------------------------------------------------------------------- /src/assets/images/walletconnect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WalletConnect 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/custom-registries/seer/seer-card-content.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | interface ISeerCardContent { 5 | chainId: string 6 | contractAddress: string 7 | marketName?: string 8 | outcomes?: string[] 9 | } 10 | 11 | const Container = styled.div` 12 | font-family: 'Arial'; 13 | max-width: 300px; 14 | margin: 16px auto; 15 | padding: 10px; 16 | border: 1px solid #e0e0e0; 17 | border-radius: 8px; 18 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 19 | ` 20 | 21 | const SeerLink = styled.a` 22 | color: #007bff; 23 | text-decoration: none; 24 | font-weight: bold; 25 | font-size: 14px; 26 | 27 | &:hover, 28 | &:focus { 29 | text-decoration: underline; 30 | } 31 | ` 32 | 33 | const MarketName = styled.h3` 34 | margin: 0 0 12px; 35 | font-size: 1.2em; 36 | color: #333; 37 | ` 38 | 39 | const OutcomesHeading = styled.h4` 40 | margin: 0 0 12px; 41 | font-size: 0.9em; 42 | color: #666; 43 | ` 44 | 45 | const OutcomeItem = styled.div` 46 | display: flex; 47 | align-items: center; 48 | margin-bottom: 6px; 49 | padding: 4px; 50 | background-color: #f9f9f9; 51 | border-radius: 4px; 52 | ` 53 | 54 | const OutcomeName = styled.span` 55 | font-size: 0.9em; 56 | color: #333; 57 | ` 58 | 59 | const LoadingMessage = styled.p` 60 | color: #666; 61 | font-size: 12px; 62 | ` 63 | 64 | const SeerCardContent: React.FC = ({ 65 | chainId, 66 | contractAddress, 67 | marketName, 68 | outcomes 69 | }) => { 70 | const filteredOutcomes = outcomes?.filter( 71 | (outcome: string) => outcome !== 'Invalid result' 72 | ) 73 | 74 | if (!marketName) 75 | return Loading Seer details... 76 | 77 | return ( 78 | 79 |

80 | 85 | Go to Seer 86 | 87 |

88 | {marketName} 89 | Outcomes 90 | {filteredOutcomes?.map((outcome, index) => ( 91 | 92 | {outcome} 93 | 94 | ))} 95 |
96 | ) 97 | } 98 | 99 | export default SeerCardContent 100 | -------------------------------------------------------------------------------- /src/pages/permanent-item-details/modals/evidence.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { Typography, Button } from 'antd' 3 | import { ethers } from 'ethers' 4 | import _gtcr from 'assets/abis/PermanentGTCR.json' 5 | import { WalletContext } from 'contexts/wallet-context' 6 | import itemPropTypes from 'prop-types/item' 7 | import EvidenceForm from 'components/evidence-form.js' 8 | import ipfsPublish from 'utils/ipfs-publish.js' 9 | import { getIPFSPath } from 'utils/get-ipfs-path' 10 | import { StyledModal } from './challenge' 11 | 12 | const EvidenceModal = ({ item, ...rest }) => { 13 | // Get contract data. 14 | const tcrAddress = item?.registry?.id 15 | const { pushWeb3Action } = useContext(WalletContext) 16 | 17 | const submitEvidence = async ({ title, description, evidenceAttachment }) => { 18 | pushWeb3Action(async (_, signer) => { 19 | const gtcr = new ethers.Contract(tcrAddress, _gtcr, signer) 20 | 21 | const evidenceJSON = { 22 | title: title, 23 | description, 24 | ...evidenceAttachment 25 | } 26 | 27 | const enc = new TextEncoder() 28 | const fileData = enc.encode(JSON.stringify(evidenceJSON)) 29 | 30 | /* eslint-enable prettier/prettier */ 31 | const ipfsResult = await ipfsPublish('evidence.json', fileData) 32 | const ipfsEvidencePath = getIPFSPath(ipfsResult) 33 | 34 | // Request signature and submit. 35 | const tx = await gtcr.submitEvidence(item.itemID, ipfsEvidencePath) 36 | 37 | return { 38 | tx, 39 | actionMessage: 'Submitting evidence...', 40 | onTxMined: () => rest.onCancel() 41 | } 42 | }) 43 | } 44 | 45 | const EVIDENCE_FORM_ID = 'submitEvidenceForm' 46 | 47 | return ( 48 | 51 | Back 52 | , 53 | 61 | ]} 62 | {...rest} 63 | > 64 | Evidence Submission 65 | 70 | 71 | ) 72 | } 73 | 74 | EvidenceModal.propTypes = { 75 | item: itemPropTypes 76 | } 77 | 78 | EvidenceModal.defaultProps = { 79 | item: null 80 | } 81 | 82 | export default EvidenceModal 83 | -------------------------------------------------------------------------------- /src/assets/abis/PermanentGTCRFactory.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "constructor", 4 | "inputs": [ 5 | { "name": "_GTCR", "type": "address", "internalType": "address" } 6 | ], 7 | "stateMutability": "nonpayable" 8 | }, 9 | { 10 | "type": "function", 11 | "name": "GTCR", 12 | "inputs": [], 13 | "outputs": [{ "name": "", "type": "address", "internalType": "address" }], 14 | "stateMutability": "view" 15 | }, 16 | { 17 | "type": "function", 18 | "name": "count", 19 | "inputs": [], 20 | "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], 21 | "stateMutability": "view" 22 | }, 23 | { 24 | "type": "function", 25 | "name": "deploy", 26 | "inputs": [ 27 | { 28 | "name": "_arbitrator", 29 | "type": "address", 30 | "internalType": "contract IArbitrator" 31 | }, 32 | { 33 | "name": "_arbitratorExtraData", 34 | "type": "bytes", 35 | "internalType": "bytes" 36 | }, 37 | { "name": "_metaEvidence", "type": "string", "internalType": "string" }, 38 | { "name": "_governor", "type": "address", "internalType": "address" }, 39 | { 40 | "name": "_token", 41 | "type": "address", 42 | "internalType": "contract IERC20" 43 | }, 44 | { 45 | "name": "_submissionMinDeposit", 46 | "type": "uint256", 47 | "internalType": "uint256" 48 | }, 49 | { 50 | "name": "_periods", 51 | "type": "uint256[4]", 52 | "internalType": "uint256[4]" 53 | }, 54 | { 55 | "name": "_stakeMultipliers", 56 | "type": "uint256[4]", 57 | "internalType": "uint256[4]" 58 | } 59 | ], 60 | "outputs": [ 61 | { 62 | "name": "instance", 63 | "type": "address", 64 | "internalType": "contract PermanentGTCR" 65 | } 66 | ], 67 | "stateMutability": "nonpayable" 68 | }, 69 | { 70 | "type": "function", 71 | "name": "instances", 72 | "inputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], 73 | "outputs": [ 74 | { 75 | "name": "", 76 | "type": "address", 77 | "internalType": "contract PermanentGTCR" 78 | } 79 | ], 80 | "stateMutability": "view" 81 | }, 82 | { 83 | "type": "event", 84 | "name": "NewGTCR", 85 | "inputs": [ 86 | { 87 | "name": "_address", 88 | "type": "address", 89 | "indexed": true, 90 | "internalType": "contract PermanentGTCR" 91 | } 92 | ], 93 | "anonymous": false 94 | } 95 | ] 96 | -------------------------------------------------------------------------------- /src/components/contract-explorer-url.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { getAddressPage } from 'utils/network-utils' 4 | 5 | const StyledIcon = styled.svg` 6 | height: 1.2rem; 7 | width: auto; 8 | vertical-align: text-bottom; 9 | 10 | .main-stop { 11 | stop-color: #863fe5d9; 12 | } 13 | .alt-stop { 14 | stop-color: #4d00b4d9; 15 | } 16 | path { 17 | fill: url(#gradient); 18 | } 19 | ` 20 | 21 | const StyledLink = styled.a` 22 | display: flex; 23 | height: 32px; 24 | align-items: center; 25 | text-decoration: underline; 26 | color: #4d00b473; 27 | ` 28 | 29 | const ContractExplorerUrl: React.FC<{ 30 | networkId: number 31 | contractAddress: string 32 | }> = ({ networkId, contractAddress }) => { 33 | const url = `${getAddressPage({ networkId, address: contractAddress })}#code` 34 | 35 | return ( 36 | 37 | 41 | 42 | 43 | 44 | 45 | 49 | 55 | 61 | 62 | 63 | 64 | ) 65 | } 66 | export default ContractExplorerUrl 67 | -------------------------------------------------------------------------------- /src/components/layout/app-menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled, { css } from 'styled-components' 3 | import { smallScreenStyle } from 'styles/small-screen-style' 4 | import { NavLink } from 'react-router-dom' 5 | import { Menu, Dropdown, Button, Icon } from 'antd' 6 | import MenuIcon from 'assets/images/menu-icon.png' 7 | 8 | const DesktopMenu = styled(Menu)` 9 | font-weight: bold; 10 | line-height: 64px !important; 11 | text-align: center; 12 | background-color: transparent !important; 13 | 14 | ${smallScreenStyle( 15 | () => css` 16 | display: none; 17 | ` 18 | )} 19 | ` 20 | 21 | const MobileDropdown = styled.div` 22 | display: none; 23 | 24 | ${smallScreenStyle( 25 | () => css` 26 | display: block; 27 | ` 28 | )} 29 | ` 30 | 31 | const StyledMenuItem = styled(Menu.Item)` 32 | background-color: transparent !important; 33 | ` 34 | 35 | const StyledButton = styled(Button)` 36 | background-color: #1e075f !important; 37 | color: #fff !important; 38 | padding: 0 !important; 39 | border: none !important; 40 | ` 41 | 42 | const StyledImg = styled.img` 43 | width: 28px; 44 | height: 28px; 45 | ` 46 | 47 | const menuItems = [ 48 | { key: 'browse', content: Browse, isNavLink: true }, 49 | { 50 | key: 'factory', 51 | content: Create a List, 52 | isNavLink: true 53 | }, 54 | { 55 | key: 'x', 56 | content: ( 57 | 58 | Follow Curate 59 | 60 | ), 61 | isNavLink: false 62 | }, 63 | { 64 | key: 'help', 65 | content: ( 66 | 67 | Get Help 68 | 69 | ), 70 | isNavLink: false 71 | } 72 | ] 73 | 74 | const renderMenuItems = () => 75 | menuItems.map(({ key, content }) => ( 76 | {content} 77 | )) 78 | 79 | const AppMenu = () => ( 80 | <> 81 | 86 | {renderMenuItems()} 87 | 88 | 89 | 90 | {renderMenuItems()}} trigger={['click']}> 91 | 92 | 93 | 94 | 95 | 96 | 97 | ) 98 | 99 | export default AppMenu 100 | -------------------------------------------------------------------------------- /src/config/networks.js: -------------------------------------------------------------------------------- 1 | export const NETWORKS = Object.freeze({ 2 | ethereum: 1, 3 | gnosis: 100, 4 | sepolia: 11155111 5 | }) 6 | 7 | export const DEFAULT_NETWORK = NETWORKS.ethereum 8 | 9 | export const NETWORK_STATUS = Object.freeze({ 10 | unknown: 'unknown', 11 | unsupported: 'unsupported', 12 | swtiching: 'switching', 13 | adding: 'adding', 14 | supported: 'supported' 15 | }) 16 | 17 | const RPC_URLS = JSON.parse(process.env.REACT_APP_RPC_URLS) 18 | 19 | export const NETWORKS_INFO = Object.freeze({ 20 | [NETWORKS.ethereum]: { 21 | name: 'Ethereum Mainnet', 22 | color: '#29b6af', 23 | supported: true, 24 | chainId: 1, 25 | shortName: 'eth', 26 | chain: 'ETH', 27 | network: 'mainnet', 28 | networkId: 1, 29 | nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, 30 | rpc: [RPC_URLS[NETWORKS.ethereum]], 31 | faucets: [], 32 | explorers: [ 33 | { 34 | name: 'etherscan', 35 | url: 'https://etherscan.io', 36 | standard: 'EIP3091' 37 | } 38 | ], 39 | infoURL: 'https://ethereum.org' 40 | }, 41 | [NETWORKS.gnosis]: { 42 | name: 'Gnosis Chain', 43 | color: '#48A9A6', 44 | supported: true, 45 | chainId: 100, 46 | shortName: 'xdai', 47 | chain: 'XDAI', 48 | network: 'xdai', 49 | networkId: 100, 50 | nativeCurrency: { name: 'xDAI', symbol: 'xDAI', decimals: 18 }, 51 | rpc: [ 52 | 'https://rpc.gnosischain.com/', 53 | 'https://xdai.poanetwork.dev', 54 | 'wss://rpc.gnosischain.com/wss', 55 | 'wss://xdai.poanetwork.dev/wss', 56 | 'http://xdai.poanetwork.dev', 57 | 'https://dai.poa.network', 58 | 'ws://xdai.poanetwork.dev:8546' 59 | ], 60 | faucets: [], 61 | explorers: [ 62 | { 63 | name: 'gnosisscan', 64 | url: 'https://gnosisscan.io', 65 | standard: 'EIP3091' 66 | } 67 | ], 68 | infoURL: 'https://forum.poa.network/c/xdai-chain' 69 | }, 70 | [NETWORKS.sepolia]: { 71 | name: 'Ethereum Sepolia', 72 | color: '#29b6af', 73 | supported: true, 74 | chainId: 5, 75 | shortName: 'sepoliaeth', 76 | chain: 'SepoliaETH', 77 | network: 'sepolia', 78 | networkId: 5, 79 | nativeCurrency: { 80 | name: 'SepoliaEther', 81 | symbol: 'SepoliaEther', 82 | decimals: 18 83 | }, 84 | rpc: [RPC_URLS[NETWORKS.sepolia]], 85 | faucets: [], 86 | explorers: [ 87 | { 88 | name: 'etherscan', 89 | url: 'https://sepolia.etherscan.io', 90 | standard: 'EIP3091' 91 | } 92 | ], 93 | infoURL: 'https://ethereum.org' 94 | } 95 | }) 96 | 97 | export default NETWORKS_INFO 98 | -------------------------------------------------------------------------------- /src/utils/fast-signer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Taken from unlock protocol to get around https://github.com/ethers-io/ethers.js/issues/511. 3 | * https://github.com/unlock-protocol/unlock/blob/master/unlock-js/src/FastJsonRpcSigner.js 4 | */ 5 | 6 | import { ethers } from 'ethers' 7 | 8 | const { utils } = ethers 9 | 10 | /** 11 | * This file is only needed with ethers v4. v5 will come with an UncheckedJsonSigner 12 | * that we can use. 13 | * 14 | * See https://github.com/ethers-io/ethers.js/issues/511 15 | */ 16 | export default class FastJsonRpcSigner extends ethers.Signer { 17 | constructor(signer) { 18 | super() 19 | utils.defineReadOnly(this, 'signer', signer) 20 | utils.defineReadOnly(this, 'provider', signer.provider) 21 | } 22 | 23 | getAddress() { 24 | return this.signer.getAddress() 25 | } 26 | 27 | async sendTransaction(transaction) { 28 | const hash = await this.signer.sendUncheckedTransaction(transaction) 29 | 30 | let gasLimit 31 | if (transaction.gasLimit) 32 | gasLimit = utils.bigNumberify( 33 | utils.hexStripZeros(utils.hexlify(transaction.gasLimit)) 34 | ) 35 | 36 | let gasPrice 37 | if (transaction.gasPrice) 38 | gasPrice = utils.bigNumberify( 39 | utils.hexStripZeros(utils.hexlify(transaction.gasLimit)) 40 | ) 41 | 42 | const ret = { 43 | ...transaction, 44 | hash: hash, 45 | blockHash: null, 46 | blockNumber: null, 47 | creates: null, 48 | gasLimit, 49 | gasPrice, 50 | value: utils.bigNumberify(transaction.value || 0), 51 | networkId: 0, 52 | nonce: 0, 53 | transactionIndex: 0, 54 | confirmations: 0, 55 | to: await transaction.to, 56 | from: await this.signer.getAddress(), 57 | wait: async (confirmations = 0) => { 58 | const tx = await this.provider.getTransaction(hash) 59 | return { 60 | hash, 61 | logs: [], 62 | wait: async () => { 63 | const receipt = await this.provider.waitForTransaction(hash) 64 | if (receipt == null && confirmations === 0) return null 65 | 66 | if (receipt.status === 0) 67 | ethers.errors.throwError( 68 | 'transaction failed', 69 | ethers.errors.CALL_EXCEPTION, 70 | { 71 | transactionHash: tx.hash, 72 | transaction: tx 73 | } 74 | ) 75 | 76 | return receipt 77 | } 78 | } 79 | } 80 | } 81 | return ret 82 | } 83 | 84 | // unused in project atm, but here for completeness 85 | signMessage(message) { 86 | return this.signer.signMessage(message) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/graphql/permanent-item-details.js: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | const PERMANENT_ITEM_DETAILS_QUERY = gql` 4 | query permanentItemDetailsQuery($id: String!) { 5 | item(id: $id) { 6 | data 7 | itemID 8 | status 9 | stake 10 | submitter 11 | includedAt 12 | arbitrationDeposit 13 | withdrawingTimestamp 14 | submissions(orderBy: createdAt, orderDirection: desc) { 15 | id 16 | createdAt 17 | creationTx 18 | finishedAt 19 | withdrawingTimestamp 20 | withdrawingTx 21 | submitter 22 | initialStake 23 | arbitrationDeposit 24 | } 25 | challenges(orderBy: createdAt, orderDirection: desc) { 26 | disputeID 27 | createdAt 28 | creationTx 29 | resolutionTime 30 | resolutionTx 31 | challenger 32 | challengerStake 33 | disputeOutcome 34 | arbitrationSetting { 35 | arbitratorExtraData 36 | } 37 | rounds(orderBy: creationTime, orderDirection: desc) { 38 | appealPeriodStart 39 | appealPeriodEnd 40 | ruling 41 | rulingTime 42 | hasPaidRequester 43 | hasPaidChallenger 44 | amountPaidRequester 45 | amountPaidChallenger 46 | } 47 | } 48 | evidences(orderBy: number, orderDirection: desc) { 49 | party 50 | URI 51 | number 52 | timestamp 53 | txHash 54 | metadata { 55 | name 56 | title 57 | description 58 | fileURI 59 | fileTypeExtension 60 | } 61 | } 62 | registry { 63 | id 64 | token 65 | numberOfSubmitted 66 | numberOfAbsent 67 | numberOfDisputed 68 | arbitrator { 69 | id 70 | } 71 | arbitrationSettings { 72 | timestamp 73 | arbitratorExtraData 74 | metaEvidenceURI 75 | metadata { 76 | title 77 | description 78 | itemName 79 | itemNamePlural 80 | policyURI 81 | logoURI 82 | requireRemovalEvidence 83 | } 84 | } 85 | submissionMinDeposit 86 | submissionPeriod 87 | reinclusionPeriod 88 | withdrawingPeriod 89 | arbitrationParamsCooldown 90 | challengeStakeMultiplier 91 | winnerStakeMultiplier 92 | loserStakeMultiplier 93 | sharedStakeMultiplier 94 | } 95 | } 96 | } 97 | ` 98 | 99 | export default PERMANENT_ITEM_DETAILS_QUERY 100 | -------------------------------------------------------------------------------- /src/pages/items/item-card-title.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useMemo } from 'react' 2 | import { Tooltip, Icon } from 'antd' 3 | import ItemStatusBadge from 'components/item-status-badge' 4 | import ETHAmount from 'components/eth-amount' 5 | import { itemToStatusCode } from 'utils/item-status' 6 | import { WalletContext } from 'contexts/wallet-context' 7 | import { TCRViewContext } from 'contexts/tcr-view-context' 8 | import useHumanizedCountdown from 'hooks/countdown' 9 | import useNativeCurrency from 'hooks/native-currency' 10 | import { 11 | Container, 12 | StatusAndBountyContainer, 13 | BountyContainer, 14 | StyledFontAwesomeIcon, 15 | CountdownContainer 16 | } from 'pages/light-items/item-card-title' 17 | 18 | const ItemCardTitle = ({ statusCode, tcrData }) => { 19 | const { challengePeriodDuration } = useContext(TCRViewContext) 20 | const { timestamp } = useContext(WalletContext) 21 | const { disputed, submissionTime } = tcrData || {} 22 | const nativeCurrency = useNativeCurrency() 23 | 24 | // Get remaining challenge period, if applicable and build countdown. 25 | const challengeRemainingTime = useMemo(() => { 26 | if (!tcrData || disputed || !submissionTime || !challengePeriodDuration) 27 | return 28 | 29 | const deadline = 30 | submissionTime.add(challengePeriodDuration).toNumber() * 1000 31 | 32 | return deadline - Date.now() 33 | }, [challengePeriodDuration, disputed, submissionTime, tcrData]) 34 | 35 | const challengeCountdown = useHumanizedCountdown(challengeRemainingTime, 1) 36 | const bounty = tcrData.deposit 37 | 38 | if (typeof statusCode !== 'number') 39 | statusCode = itemToStatusCode(tcrData, timestamp, challengePeriodDuration) 40 | 41 | return ( 42 | 43 | 44 | 45 | {challengeRemainingTime > 0 && ( 46 | 47 | 48 | 53 | 54 | 55 | 56 | )} 57 | 58 | {challengeRemainingTime > 0 && ( 59 | 60 | Ends {challengeCountdown} 61 | 62 |   63 | 64 | 65 | 66 | )} 67 | 68 | ) 69 | } 70 | 71 | export default ItemCardTitle 72 | -------------------------------------------------------------------------------- /src/utils/string.js: -------------------------------------------------------------------------------- 1 | import { keccak256, getAddress, bigNumberify } from 'ethers/utils' 2 | 3 | export const truncateETHAddress = ethAddr => 4 | `${ethAddr.slice(0, 5)}...${ethAddr.slice(40)}` 5 | 6 | export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' 7 | export const ZERO_BYTES32 = 8 | '0x0000000000000000000000000000000000000000000000000000000000000000' 9 | 10 | export const sanitize = str => 11 | str 12 | .toString() 13 | .toLowerCase() 14 | .replace(/([^a-z0-9.]+)/gi, '-') // Only allow numbers and aplhanumeric. 15 | 16 | export const LOREM_IPSUM = `Natus ipsam unde et accusamus. Autem et laboriosam non harum voluptas necessitatibus commodi. Enim suscipit cumque aut voluptas quibusdam soluta quis. Velit modi dolores voluptate pariatur. Eligendi veniam aut esse. Aut nam itaque repellendus explicabo dolores. 17 | 18 | Voluptates magnam error sequi occaecati facere.` 19 | 20 | export const isVowel = x => /[aeiouAEIOU]/.test(x) 21 | 22 | export const capitalizeFirstLetter = str => 23 | str && str.length > 0 ? str.charAt(0).toUpperCase() + str.slice(1) : str 24 | 25 | export const isChecksumAddress = address => { 26 | // Check each case 27 | var addressHash = keccak256(address.toLowerCase()) 28 | for (var i = 0; i < 40; i++) 29 | // the nth letter should be uppercase if the nth digit of casemap is 1 30 | if ( 31 | (parseInt(addressHash[i], 16) > 7 && 32 | address[i].toUpperCase() !== address[i]) || 33 | (parseInt(addressHash[i], 16) <= 7 && 34 | address[i].toLowerCase() !== address[i]) 35 | ) 36 | return false 37 | 38 | return true 39 | } 40 | 41 | export const isETHAddress = address => { 42 | if (!address) return false 43 | if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) return false 44 | else if ( 45 | /^(0x)?[0-9a-f]{40}$/.test(address) || 46 | /^(0x)?[0-9A-F]{40}$/.test(address) 47 | ) 48 | return true 49 | else 50 | try { 51 | getAddress(address) 52 | return true 53 | } catch { 54 | return false 55 | } 56 | } 57 | 58 | export const jurorsAndCourtIDFromExtraData = arbitratorExtraData => { 59 | const courtID = bigNumberify( 60 | `0x${arbitratorExtraData.slice(2, 66)}` 61 | ).toNumber() 62 | 63 | const numberOfJurors = bigNumberify( 64 | `0x${arbitratorExtraData.slice(66, 130)}` 65 | ).toNumber() 66 | 67 | return { courtID, numberOfJurors } 68 | } 69 | 70 | export const getArticleFor = str => (str && isVowel(str[0]) ? 'an' : 'a') 71 | 72 | export const SAVED_NETWORK_KEY = 'SAVED_NETWORK_KEY' 73 | 74 | export const addPeriod = (input = '') => { 75 | if (input.length === 0) return '' 76 | return input[input.length - 1] === '.' ? input : `${input}.` 77 | } 78 | 79 | export const hexlify = number => { 80 | if (!number) return '0x00' 81 | else return `0x${Number(number).toString(16)}` 82 | } 83 | -------------------------------------------------------------------------------- /src/pages/permanent-items/item-card-title.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import styled from 'styled-components' 3 | import { Tooltip, Icon } from 'antd' 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 5 | import ItemStatusBadge from 'components/permanent-item-status-badge' 6 | import ETHAmount from 'components/eth-amount' 7 | import useHumanizedCountdown from 'hooks/countdown' 8 | import { STATUS_CODE } from 'utils/permanent-item-status' 9 | 10 | export const Container = styled.div` 11 | display: flex; 12 | flex-direction: column; 13 | min-height: 40.5px; 14 | justify-content: center; 15 | ` 16 | 17 | export const StatusAndBountyContainer = styled.div` 18 | display: flex; 19 | justify-content: space-between; 20 | ` 21 | 22 | export const BountyContainer = styled.div` 23 | display: flex; 24 | flex-direction: column; 25 | ` 26 | 27 | export const StyledFontAwesomeIcon = styled(FontAwesomeIcon)` 28 | margin-left: 6px; 29 | ` 30 | 31 | export const CountdownContainer = styled.div` 32 | color: #ffffff5c; 33 | font-size: 13px; 34 | margin-left: 12px; 35 | ` 36 | 37 | const ItemCardTitle = ({ statusCode, item, registry }) => { 38 | // Get remaining submission period, if applicable and build countdown. 39 | const timeUntilValid = useMemo(() => { 40 | if ( 41 | !['Submitted', 'Reincluded'].includes(item.status) || 42 | statusCode === STATUS_CODE.PENDING_WITHDRAWAL 43 | ) 44 | return 45 | 46 | const deadline = 47 | item.status === 'Submitted' 48 | ? Number(item.includedAt) + Number(registry.submissionPeriod) 49 | : Number(item.includedAt) + Number(registry.reinclusionPeriod) 50 | 51 | return deadline - Date.now() 52 | }, [item, registry, statusCode]) 53 | 54 | const validityCountdown = useHumanizedCountdown(timeUntilValid, 1) 55 | 56 | const bounty = item.stake 57 | 58 | return ( 59 | 60 | 61 | 62 | 63 | {statusCode !== STATUS_CODE.ABSENT && ( 64 | 65 | 66 | 67 | 68 | 69 | 70 | )} 71 | 72 | {timeUntilValid > 0 && ( 73 | 74 | Ends {validityCountdown} 75 | 76 |   77 | 78 | 79 | 80 | )} 81 | 82 | ) 83 | } 84 | 85 | export default ItemCardTitle 86 | -------------------------------------------------------------------------------- /src/hooks/factory.js: -------------------------------------------------------------------------------- 1 | import { useWeb3Context } from 'web3-react' 2 | import { getAddress } from 'ethers/utils' 3 | import { subgraphUrl, subgraphUrlPermanent } from 'config/tcr-addresses' 4 | 5 | const useFactory = () => { 6 | const { networkId } = useWeb3Context() 7 | const GTCR_SUBGRAPH_URL = subgraphUrl[networkId] 8 | const PGTCR_SUBGRAPH_URL = subgraphUrlPermanent[networkId] 9 | 10 | const deployedWithLightFactory = async tcrAddress => { 11 | if (!tcrAddress) return false 12 | try { 13 | tcrAddress = getAddress(tcrAddress) 14 | } catch (_) { 15 | return false 16 | } 17 | const query = { 18 | query: ` 19 | { 20 | lregistry:LRegistry_by_pk(id: "${tcrAddress.toLowerCase()}") { 21 | id 22 | } 23 | } 24 | ` 25 | } 26 | const { data } = await ( 27 | await fetch(GTCR_SUBGRAPH_URL, { 28 | method: 'POST', 29 | headers: { 'Content-Type': 'application/json' }, 30 | body: JSON.stringify(query) 31 | }) 32 | ).json() 33 | 34 | if (data.lregistry) return true 35 | 36 | return false 37 | } 38 | 39 | const deployedWithFactory = async tcrAddress => { 40 | if (!tcrAddress) return false 41 | 42 | try { 43 | tcrAddress = getAddress(tcrAddress) 44 | } catch (_) { 45 | return false 46 | } 47 | 48 | const query = { 49 | query: ` 50 | { 51 | registry:Registry_by_pk(id: "${tcrAddress.toLowerCase()}") { 52 | id 53 | } 54 | } 55 | ` 56 | } 57 | const { data } = await ( 58 | await fetch(GTCR_SUBGRAPH_URL, { 59 | method: 'POST', 60 | headers: { 'Content-Type': 'application/json' }, 61 | body: JSON.stringify(query) 62 | }) 63 | ).json() 64 | 65 | if (data.registry) return true 66 | 67 | return false 68 | } 69 | 70 | const deployedWithPermanentFactory = async tcrAddress => { 71 | if (!tcrAddress) return false 72 | 73 | try { 74 | tcrAddress = getAddress(tcrAddress) 75 | } catch (_) { 76 | return false 77 | } 78 | 79 | const query = { 80 | query: ` 81 | { 82 | registry(id: "${tcrAddress.toLowerCase()}") { 83 | id 84 | } 85 | } 86 | ` 87 | } 88 | const { data } = await ( 89 | await fetch(PGTCR_SUBGRAPH_URL, { 90 | method: 'POST', 91 | headers: { 'Content-Type': 'application/json' }, 92 | body: JSON.stringify(query) 93 | }) 94 | ).json() 95 | if (data.registry) return true 96 | 97 | return false 98 | } 99 | 100 | return { 101 | deployedWithLightFactory, 102 | deployedWithFactory, 103 | deployedWithPermanentFactory 104 | } 105 | } 106 | 107 | export default useFactory 108 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contribution Guidelines 2 | 3 | 👉 Please start by reading our guidelines here: https://kleros.gitbook.io/docs/contribution-guidelines/overview 4 | 5 | ## Opening an issue 6 | 7 | You should usually open an issue in the following situations: 8 | 9 | - Report an error you can’t solve yourself 10 | - Discuss a high-level topic or idea (for example, community, vision or policies) 11 | - Propose a new feature or other project idea 12 | 13 | ### Tips for communicating on issues: 14 | 15 | - **If you see an open issue that you want to tackle,** comment on the issue to let people know you’re on it. That way, people are less likely to duplicate your work. 16 | - **If an issue was opened a while ago,** it’s possible that it’s being addressed somewhere else, or has already been resolved, so comment to ask for confirmation before starting work. 17 | - **If you opened an issue, but figured out the answer later on your own,** comment on the issue to let people know, then close the issue. Even documenting that outcome is a contribution to the project. 18 | 19 | ## Opening a pull request 20 | 21 | You should usually open a pull request in the following situations: 22 | 23 | - Submit trivial fixes (for example, a typo, a broken link or an obvious error). 24 | - Start work on a contribution that was already asked for, or that you’ve already discussed, in an issue. 25 | 26 | A pull request doesn’t have to represent finished work. It’s usually better to open a _draft_ pull request early on, so others can watch or give feedback on your progress. Just mark it as a “WIP” (Work in Progress) in the subject line. You can always add more commits later. 27 | 28 | As a contributor who is not an organization member, here’s how to submit a pull request: 29 | 30 | - **Fork the repository** and clone it locally. Connect your local to the original repository by adding it as a remote. Pull in changes from this repository often so that you stay up to date so that when you submit your pull request, merge conflicts will be less likely. 31 | - **Create a branch** for your edits. 32 | - **Reference any relevant issues** or supporting documentation in your PR (for example, “Closes #37.”) 33 | - **Include screenshots of the before and after** if your changes include differences in HTML/CSS. Drag and drop the images into the body of your pull request. 34 | - **Test your changes!** Run your changes against any existing tests if they exist and create new ones when needed. Whether tests exist or not, make sure your changes don’t break the existing project. 35 | - **Contribute in the style of the project** to the best of your abilities. This may mean using indents, semi-colons or comments differently than you would in your own repository, but makes it easier for the maintainer to merge, others to understand and maintain in the future. 36 | 37 | If you are an organization member, a branch can be created directly in this repository, there is no need to fork it. -------------------------------------------------------------------------------- /src/assets/abis/GTCRFactory.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "contract GeneralizedTCR", 8 | "name": "_address", 9 | "type": "address" 10 | } 11 | ], 12 | "name": "NewGTCR", 13 | "type": "event" 14 | }, 15 | { 16 | "constant": true, 17 | "inputs": [], 18 | "name": "count", 19 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 20 | "payable": false, 21 | "stateMutability": "view", 22 | "type": "function" 23 | }, 24 | { 25 | "constant": false, 26 | "inputs": [ 27 | { 28 | "internalType": "contract IArbitrator", 29 | "name": "_arbitrator", 30 | "type": "address" 31 | }, 32 | { 33 | "internalType": "bytes", 34 | "name": "_arbitratorExtraData", 35 | "type": "bytes" 36 | }, 37 | { "internalType": "address", "name": "_connectedTCR", "type": "address" }, 38 | { 39 | "internalType": "string", 40 | "name": "_registrationMetaEvidence", 41 | "type": "string" 42 | }, 43 | { 44 | "internalType": "string", 45 | "name": "_clearingMetaEvidence", 46 | "type": "string" 47 | }, 48 | { "internalType": "address", "name": "_governor", "type": "address" }, 49 | { 50 | "internalType": "uint256", 51 | "name": "_submissionBaseDeposit", 52 | "type": "uint256" 53 | }, 54 | { 55 | "internalType": "uint256", 56 | "name": "_removalBaseDeposit", 57 | "type": "uint256" 58 | }, 59 | { 60 | "internalType": "uint256", 61 | "name": "_submissionChallengeBaseDeposit", 62 | "type": "uint256" 63 | }, 64 | { 65 | "internalType": "uint256", 66 | "name": "_removalChallengeBaseDeposit", 67 | "type": "uint256" 68 | }, 69 | { 70 | "internalType": "uint256", 71 | "name": "_challengePeriodDuration", 72 | "type": "uint256" 73 | }, 74 | { 75 | "internalType": "uint256[3]", 76 | "name": "_stakeMultipliers", 77 | "type": "uint256[3]" 78 | } 79 | ], 80 | "name": "deploy", 81 | "outputs": [], 82 | "payable": false, 83 | "stateMutability": "nonpayable", 84 | "type": "function" 85 | }, 86 | { 87 | "constant": true, 88 | "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 89 | "name": "instances", 90 | "outputs": [ 91 | { 92 | "internalType": "contract GeneralizedTCR", 93 | "name": "", 94 | "type": "address" 95 | } 96 | ], 97 | "payable": false, 98 | "stateMutability": "view", 99 | "type": "function" 100 | } 101 | ] 102 | -------------------------------------------------------------------------------- /src/components/display-selector.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Typography, Avatar, Checkbox } from 'antd' 4 | import PropTypes from 'prop-types' 5 | import GTCRAddress from './gtcr-address' 6 | import { ItemTypes } from '@kleros/gtcr-encoder' 7 | import { ZERO_ADDRESS } from '../utils/string' 8 | import RichAddress from './rich-address' 9 | import ETHAddress from './eth-address' 10 | import LongText from './long-text' 11 | import FileDisplay from './file-display' 12 | import { parseIpfs } from 'utils/ipfs-parse' 13 | 14 | const pohRichAddress = 'eip155:1:0xc5e9ddebb09cd64dfacab4011a0d5cedaf7c9bdb' 15 | 16 | const StyledImage = styled.img` 17 | object-fit: contain; 18 | height: 100px; 19 | width: 100px; 20 | padding: 5px; 21 | ` 22 | 23 | const protocolRegex = /:\/\// 24 | 25 | const DisplaySelector = ({ type, value, linkImage, allowedFileTypes }) => { 26 | switch (type) { 27 | case ItemTypes.GTCR_ADDRESS: 28 | return 29 | case ItemTypes.ADDRESS: 30 | return 31 | case ItemTypes.RICH_ADDRESS: 32 | return 33 | case ItemTypes.TEXT: 34 | case ItemTypes.NUMBER: 35 | return {value} 36 | case ItemTypes.BOOLEAN: 37 | return 38 | case ItemTypes.LONG_TEXT: 39 | return 40 | case ItemTypes.FILE: { 41 | return 42 | } 43 | case ItemTypes.IMAGE: 44 | return value ? ( 45 | linkImage ? ( 46 | 47 | 48 | 49 | ) : ( 50 | 51 | ) 52 | ) : ( 53 | 54 | ) 55 | case ItemTypes.LINK: 56 | return ( 57 | 58 | {value} 59 | 60 | ) 61 | default: 62 | return ( 63 | 64 | Error: Unhandled Type {type} for data {value} 65 | 66 | ) 67 | } 68 | } 69 | 70 | DisplaySelector.propTypes = { 71 | type: PropTypes.oneOf(Object.values(ItemTypes)).isRequired, 72 | value: PropTypes.oneOfType([ 73 | PropTypes.string, 74 | PropTypes.number, 75 | PropTypes.bool, 76 | PropTypes.object 77 | ]), 78 | linkImage: PropTypes.bool, 79 | allowedFileTypes: PropTypes.string 80 | } 81 | 82 | DisplaySelector.defaultProps = { 83 | linkImage: null, 84 | allowedFileTypes: null, 85 | value: null 86 | } 87 | 88 | export default DisplaySelector 89 | -------------------------------------------------------------------------------- /src/components/permanent-item-status-badge.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | STATUS_COLOR, 4 | STATUS_TEXT, 5 | STATUS_CODE 6 | } from '../utils/permanent-item-status' 7 | import { Badge, Icon, Skeleton } from 'antd' 8 | import styled from 'styled-components' 9 | 10 | const SkeletonTitleProps = { width: 90 } 11 | const StyledSkeleton = styled(Skeleton)` 12 | display: inline; 13 | 14 | .ant-skeleton-title { 15 | margin: -3px 0; 16 | } 17 | ` 18 | 19 | export const ItemStatusBadgeWrap = styled.div` 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | ` 24 | 25 | const ItemStatusIconWrap = styled.div` 26 | margin-left: 8px; 27 | ` 28 | 29 | const iconTypes = { 30 | [STATUS_CODE.ACCEPTED]: 'check-circle', 31 | [STATUS_CODE.DISPUTED]: 'fire', 32 | [STATUS_CODE.CROWDFUNDING]: 'dollar', 33 | [STATUS_CODE.CROWDFUNDING_WINNER]: 'dollar', 34 | [STATUS_CODE.PENDING]: 'hourglass', 35 | [STATUS_CODE.PENDING_WITHDRAWAL]: 'hourglass', 36 | [STATUS_CODE.ABSENT]: 'close', 37 | [STATUS_CODE.WAITING_ARBITRATOR]: 'hourglass' 38 | } 39 | 40 | export const ItemStatusIcon = ({ statusCode }) => ( 41 | 42 | 43 | 44 | ) 45 | 46 | // For clarity, here "badge" refers to the ant design component, 47 | // and not badges related to connection between TCRs. 48 | export const badgeStatus = statusCode => { 49 | switch (statusCode) { 50 | case STATUS_CODE.CROWDFUNDING: 51 | case STATUS_CODE.CROWDFUNDING_WINNER: 52 | case STATUS_CODE.WAITING_ARBITRATOR: 53 | case STATUS_CODE.DISPUTED: 54 | case STATUS_CODE.PENDING: 55 | case STATUS_CODE.PENDING_WITHDRAWAL: 56 | return 'processing' 57 | case STATUS_CODE.ABSENT: 58 | case STATUS_CODE.ACCEPTED: 59 | return 'default' 60 | default: 61 | throw new Error(`Unhandled status code ${statusCode}`) 62 | } 63 | } 64 | 65 | // A wrapper around antdesign's badge component. 66 | const ItemStatusBadge = ({ item, timestamp, statusCode, dark }) => { 67 | if (statusCode) 68 | return ( 69 | 70 | 76 | 77 | 78 | ) 79 | 80 | if (typeof statusCode !== 'number' && !item && !timestamp) 81 | return ( 82 | 83 | ) 84 | 85 | return ( 86 | 87 | 93 | 94 | 95 | ) 96 | } 97 | 98 | export default ItemStatusBadge 99 | -------------------------------------------------------------------------------- /src/components/light-item-card-content.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Button } from 'antd' 4 | import PropTypes from 'prop-types' 5 | import DisplaySelector from './display-selector' 6 | import { ItemTypes } from '@kleros/gtcr-encoder' 7 | import useNavigateAndScrollTop from 'hooks/navigate-and-scroll-top' 8 | import SeerCardContent from 'components/custom-registries/seer/seer-card-content' 9 | import { isSeerRegistry } from 'components/custom-registries/seer/is-seer-registry' 10 | 11 | export const Container = styled.div` 12 | display: flex; 13 | height: 100%; 14 | flex-direction: column; 15 | justify-content: space-between; 16 | align-items: center; 17 | ` 18 | 19 | export const StyledItemCol = styled.div` 20 | margin-bottom: 8px; 21 | text-align: center; 22 | ` 23 | 24 | const LightItemCardContent = ({ item, chainId, tcrAddress }) => { 25 | const navigateAndScrollTop = useNavigateAndScrollTop() 26 | 27 | const allowedFileTypes = 28 | item.columns.filter(col => col.allowedFileTypes)[0]?.allowedFileTypes || '' 29 | 30 | return ( 31 | 32 |
33 | {item.tcrData.mergedData 34 | .filter( 35 | col => 36 | col.isIdentifier || 37 | col.type === ItemTypes.IMAGE || 38 | col.type === ItemTypes.FILE 39 | ) 40 | .map((column, j) => ( 41 | 42 | 47 | 48 | ))} 49 | {isSeerRegistry(tcrAddress, chainId) && item && ( 50 | 58 | )} 59 |
60 | 69 |
70 | ) 71 | } 72 | 73 | LightItemCardContent.propTypes = { 74 | item: PropTypes.shape({ 75 | tcrData: PropTypes.shape({ 76 | ID: PropTypes.string.isRequired, 77 | mergedData: PropTypes.arrayOf( 78 | PropTypes.shape({ 79 | isIdentifier: PropTypes.bool, 80 | type: PropTypes.oneOf(Object.values(ItemTypes)), 81 | value: PropTypes.oneOfType([ 82 | PropTypes.bool, 83 | PropTypes.string, 84 | PropTypes.number, 85 | PropTypes.object 86 | ]) 87 | }) 88 | ) 89 | }).isRequired 90 | }).isRequired, 91 | tcrAddress: PropTypes.string.isRequired 92 | } 93 | 94 | export default LightItemCardContent 95 | -------------------------------------------------------------------------------- /src/pages/items/tour-steps.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { capitalizeFirstLetter, getArticleFor } from 'utils/string' 3 | 4 | const itemsTourSteps = metadata => { 5 | const { tcrTitle, itemName, itemNamePlural } = metadata || {} 6 | return [ 7 | { 8 | selector: `#tcr-info-column`, 9 | content: () => ( 10 |
11 | Let's take a quick tour of the list view.{' '} 12 | 13 | 🚎 14 | 15 |
16 |
17 | Here, you can view information of the current list. This gives you 18 | context on what each item is.{' '} 19 | {metadata && 20 | `In the case of ${tcrTitle}, each item is ${getArticleFor( 21 | itemName 22 | )} ${itemName.toLowerCase()}`} 23 | . 24 |
25 | ) 26 | }, 27 | { 28 | selector: `#submit-item-button`, 29 | content: `To submit ${ 30 | itemName 31 | ? `${getArticleFor(itemName)} ${itemName.toLowerCase()}` 32 | : 'item' 33 | } to ${tcrTitle || 'the list'}, click this button.` 34 | }, 35 | { 36 | selector: `#policy-link`, 37 | content: () => ( 38 |
39 | Here you can find the listing policy for this list.{' '} 40 | 41 | 📜 42 | 43 |
44 |
45 | 46 | ⚠️ 47 | 48 | Before making your submission, make sure it complies with the Listing 49 | Policy. If you submit a non-compliant list, it will be rejected and 50 | you will lose your deposit 51 | 52 | ⚠️ 53 | 54 |
55 | ) 56 | }, 57 | { 58 | selector: `#items-search-bar`, 59 | content: () => ( 60 |
61 | Use this bar to search for{' '} 62 | {itemName 63 | ? (itemNamePlural && itemNamePlural.toLowerCase()) || 64 | `${itemName.toLowerCase()}s` 65 | : 'items'}{' '} 66 | submitted by users. 67 | 68 | 🔍 69 | 70 |
71 | ) 72 | }, 73 | { 74 | selector: `#items-filters`, 75 | content: () => ( 76 |
77 | The filtering options will allow you to fine tune your search.{' '} 78 | 79 | 🔬 80 | 81 |
82 | ) 83 | }, 84 | { 85 | selector: `#items-grid-view`, 86 | content: `${ 87 | itemName 88 | ? capitalizeFirstLetter(itemNamePlural) || 89 | `${capitalizeFirstLetter(itemName)}s` 90 | : 'items' 91 | } in the "Submitted" and "Removing" state can be challenged to potentially earn rewards.` 92 | } 93 | ] 94 | } 95 | 96 | export default itemsTourSteps 97 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Plugins 3 | plugins: ['unicorn', 'react-hooks', 'prettier', 'jsx-a11y', 'promise'], 4 | 5 | // Settings 6 | settings: { 7 | 'import/resolver': { 8 | node: { 9 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 10 | moduleDirectory: ['node_modules', 'src/'] 11 | } 12 | } 13 | }, 14 | 15 | // Extends 16 | extends: [ 17 | 'react-app', // create-react-app config 18 | 'standard', // JS Standard 19 | 'standard-jsx', // JS Standard JSX 20 | 'plugin:unicorn/recommended', // unicorn 21 | 'plugin:prettier/recommended', // prettier overrides 22 | 'prettier/standard', 23 | 'prettier/react', 24 | 'plugin:jsx-a11y/recommended', 25 | 'plugin:import/errors', 26 | 'plugin:import/warnings', 27 | 'plugin:promise/recommended' 28 | ], 29 | 30 | // Rule Overrides 31 | rules: { 32 | // Generic JS 33 | 'no-unused-vars': [ 34 | 2, 35 | { 36 | vars: 'all', 37 | args: 'all', 38 | ignoreRestSiblings: false, 39 | caughtErrors: 'all', 40 | varsIgnorePattern: '^_', 41 | argsIgnorePattern: '^_' 42 | } 43 | ], 44 | 'prefer-const': 2, 45 | 'arrow-body-style': [2, 'as-needed'], 46 | curly: [2, 'multi'], 47 | 'padding-line-between-statements': [ 48 | 2, 49 | { blankLine: 'never', prev: 'import', next: 'import' } 50 | ], 51 | 'no-useless-concat': 2, 52 | 'prefer-template': 2, 53 | 54 | // unicorn 55 | 'unicorn/no-fn-reference-in-iterator': 0, // Allows [].map(func) 56 | 'unicorn/catch-error-name': [2, { name: 'err' }], 57 | 'unicorn/prevent-abbreviations': 'off', 58 | 'unicorn/no-abusive-eslint-disable': 'off', 59 | 'unicorn/number-literal-case': 'off', 60 | 61 | // import 62 | 'import/no-unresolved': 2, 63 | 'import/named': 2, 64 | 'import/default': 2, 65 | 'import/namespace': 2, 66 | 'import/no-named-as-default': 2, 67 | 'import/no-named-as-default-member': 2, 68 | 'import/no-extraneous-dependencies': 2, 69 | 'import/newline-after-import': 2, 70 | 'import/no-named-default': 2, 71 | 'import/no-useless-path-segments': 2, 72 | 73 | // React 74 | 'react/prefer-stateless-function': 2, 75 | 'react/destructuring-assignment': [2, 'always'], 76 | // I don't use prop types, I don't want to deal with them while patching Curate 77 | 'react/prop-types': 0, 78 | 'react/forbid-prop-types': 0, 79 | 'react/no-unused-prop-types': 0, 80 | 'react/require-default-props': 0, 81 | 'react/default-props-match-prop-types': 0, 82 | 83 | 'react/destructuring-assignment': 0, 84 | 85 | // hooks 86 | 'react-hooks/rules-of-hooks': 'error', 87 | 'react-hooks/exhaustive-deps': 'warn', 88 | 89 | // JS Standard 90 | 'standard/computed-property-even-spacing': 0, 91 | 'jsx-a11y/href-no-hash': 0, // Buggy 92 | 93 | // prettier 94 | 'prettier/prettier': [ 95 | 2, 96 | { 97 | semi: false, 98 | singleQuote: true 99 | } 100 | ] 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/components/smart-contract-wallet-warning.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react' 2 | import { Alert } from 'antd' 3 | import styled from 'styled-components' 4 | import { useWeb3Context } from 'web3-react' 5 | 6 | const StyledAlert = styled(Alert)` 7 | text-align: center; 8 | 9 | .ant-alert-message { 10 | font-weight: bold; 11 | } 12 | ` 13 | 14 | const StyledP = styled.p` 15 | margin: 0; 16 | ` 17 | 18 | const EIP7702_PREFIX = '0xef0100' 19 | const STORAGE_KEY = '@kleros/curate/alert/smart-contract-wallet-warning' 20 | 21 | export default function SmartContractWalletWarning() { 22 | const { account, library } = useWeb3Context() 23 | const [isSmartContractWallet, setIsSmartContractWallet] = useState( 24 | false 25 | ) 26 | const [showWarning, setShowWarning] = useState(true) 27 | 28 | const updateAccountWarningDismissalState = useCallback((account: string) => { 29 | try { 30 | const storedValue = localStorage.getItem(`${STORAGE_KEY}:${account}`) 31 | if (storedValue === null) setShowWarning(true) 32 | else setShowWarning(JSON.parse(storedValue)) 33 | } catch { 34 | setShowWarning(true) 35 | } 36 | }, []) 37 | 38 | const checkIfSmartContractWallet = useCallback( 39 | (account: string, library: any) => { 40 | library.provider 41 | .send('eth_getCode', [account, 'latest']) 42 | .then((res: { result: string }) => { 43 | const formattedCode = res.result.toLowerCase() 44 | const isEip7702Eoa = formattedCode.startsWith(EIP7702_PREFIX) 45 | 46 | // Do not show warning for EIP-7702 EOAs 47 | setIsSmartContractWallet(formattedCode !== '0x' && !isEip7702Eoa) 48 | return null 49 | }) 50 | .catch((err: Error) => { 51 | console.error('Error checking smart contract wallet', err) 52 | setIsSmartContractWallet(false) 53 | }) 54 | }, 55 | [] 56 | ) 57 | 58 | const handleClose = useCallback(() => { 59 | setShowWarning(false) 60 | localStorage.setItem(`${STORAGE_KEY}:${account}`, JSON.stringify(false)) 61 | }, [account]) 62 | 63 | useEffect(() => { 64 | if (!account || !library) { 65 | setIsSmartContractWallet(false) 66 | return 67 | } 68 | 69 | updateAccountWarningDismissalState(account) 70 | checkIfSmartContractWallet(account, library) 71 | 72 | // eslint-disable-next-line react-hooks/exhaustive-deps 73 | }, [account, library]) 74 | 75 | if (!showWarning || !isSmartContractWallet) return null 76 | 77 | return ( 78 | 82 | You are using a smart contract wallet. This is not recommended.{' '} 83 | 88 | Learn more. 89 | 90 | 91 | } 92 | type="warning" 93 | banner 94 | closable 95 | onClose={handleClose} 96 | /> 97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /src/utils/notifications.js: -------------------------------------------------------------------------------- 1 | export const NOTIFICATION_TYPES = { 2 | SUBMISSION_PENDING: 'SUBMISSION_PENDING', 3 | REMOVAL_PENDING: 'REMOVAL_PENDING', 4 | SUBMISSION_ACCEPTED: 'SUBMISSION_ACCEPTED', 5 | REMOVAL_ACCEPTED: 'REMOVAL_ACCEPTED', 6 | SUBMISSION_CHALLENGED: 'SUBMISSION_CHALLENGED', 7 | REMOVAL_CHALLENGED: 'REMOVAL_CHALLENGED', 8 | EVIDENCE_SUBMITTED: 'EVIDENCE_SUBMITTED', 9 | APPEALED: 'APPEALED', 10 | APPEALABLE_RULING: 'APPEALABLE_RULING', 11 | FINAL_RULING: 'FINAL_RULING', 12 | HAS_PAID_FEES: 'HAS_PAID_FEES' 13 | } 14 | 15 | export const typeToMessage = { 16 | [NOTIFICATION_TYPES.SUBMISSION_PENDING]: 'Submission pending execution.', 17 | [NOTIFICATION_TYPES.REMOVAL_PENDING]: 'Removal pending execution.', 18 | [NOTIFICATION_TYPES.SUBMISSION_ACCEPTED]: 'Submission accepted.', 19 | [NOTIFICATION_TYPES.REMOVAL_ACCEPTED]: 'Removal accepted.', 20 | [NOTIFICATION_TYPES.SUBMISSION_CHALLENGED]: 'Submission challenged.', 21 | [NOTIFICATION_TYPES.REMOVAL_CHALLENGED]: 'Removal challenged.', 22 | [NOTIFICATION_TYPES.EVIDENCE_SUBMITTED]: 'Evidence submitted.', 23 | [NOTIFICATION_TYPES.APPEALED]: 'Ruling appealed', 24 | [NOTIFICATION_TYPES.APPEALABLE_RULING]: 'The Arbitrator gave a ruling', 25 | [NOTIFICATION_TYPES.FINAL_RULING]: 'Ruling enforced.', 26 | [NOTIFICATION_TYPES.HAS_PAID_FEES]: 'Side fully funded' 27 | } 28 | 29 | export const getNotificationColorFor = notificationType => { 30 | switch (notificationType) { 31 | case NOTIFICATION_TYPES.SUBMISSION_PENDING: 32 | case NOTIFICATION_TYPES.REMOVAL_PENDING: 33 | return '#ccc' 34 | case NOTIFICATION_TYPES.EVIDENCE_SUBMITTED: 35 | case NOTIFICATION_TYPES.SUBMISSION_ACCEPTED: 36 | case NOTIFICATION_TYPES.REMOVAL_ACCEPTED: 37 | return '#208efa' // Antd Blue. 38 | case NOTIFICATION_TYPES.APPEALED: 39 | case NOTIFICATION_TYPES.SUBMISSION_CHALLENGED: 40 | case NOTIFICATION_TYPES.REMOVAL_CHALLENGED: 41 | return '#fa8d39' // Antd Orange. 42 | case NOTIFICATION_TYPES.APPEALABLE_RULING: 43 | case NOTIFICATION_TYPES.HAS_PAID_FEES: 44 | return '#722ed1' // Antd Purple. 45 | case NOTIFICATION_TYPES.FINAL_RULING: 46 | return '#f95638' // Antd Volcano. 47 | default: 48 | throw new Error('Unhandled notification type') 49 | } 50 | } 51 | 52 | export const getNotificationIconFor = notificationType => { 53 | switch (notificationType) { 54 | case NOTIFICATION_TYPES.SUBMISSION_PENDING: 55 | case NOTIFICATION_TYPES.REMOVAL_PENDING: 56 | return 'hourglass-half' 57 | case NOTIFICATION_TYPES.EVIDENCE_SUBMITTED: 58 | return 'file-alt' 59 | case NOTIFICATION_TYPES.SUBMISSION_ACCEPTED: 60 | case NOTIFICATION_TYPES.REMOVAL_ACCEPTED: 61 | return 'check' 62 | case NOTIFICATION_TYPES.APPEALED: 63 | case NOTIFICATION_TYPES.SUBMISSION_CHALLENGED: 64 | case NOTIFICATION_TYPES.REMOVAL_CHALLENGED: 65 | return 'balance-scale' 66 | case NOTIFICATION_TYPES.APPEALABLE_RULING: 67 | case NOTIFICATION_TYPES.FINAL_RULING: 68 | return 'gavel' 69 | case NOTIFICATION_TYPES.HAS_PAID_FEES: 70 | return 'exclamation-triangle' 71 | default: 72 | throw new Error('Unhandled notification type') 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/components/tcr-card-content.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Skeleton, Button } from 'antd' 3 | import PropTypes from 'prop-types' 4 | import { useWeb3Context } from 'web3-react' 5 | import { ItemTypes } from '@kleros/gtcr-encoder' 6 | import DisplaySelector from './display-selector' 7 | import { fetchMetaEvidence } from 'hooks/tcr-view' 8 | import useNavigateAndScrollTop from 'hooks/navigate-and-scroll-top' 9 | import { parseIpfs } from 'utils/ipfs-parse' 10 | import { 11 | StyledItemCol, 12 | StyledResult, 13 | Container 14 | } from './light-tcr-card-content' 15 | 16 | const TCRCardContent = ({ 17 | tcrAddress, 18 | currentTCRAddress, 19 | ID, 20 | hideDetailsButton 21 | }) => { 22 | const { networkId } = useWeb3Context() 23 | 24 | const [metaEvidence, setMetaEvidence] = useState() 25 | const navigateAndScrollTop = useNavigateAndScrollTop() 26 | 27 | useEffect(() => { 28 | ;(async () => { 29 | const fetchedData = await fetchMetaEvidence(tcrAddress, networkId) 30 | 31 | const response = await fetch(parseIpfs(fetchedData.metaEvidenceURI)) 32 | const file = await response.json() 33 | setMetaEvidence(file) 34 | })() 35 | }, [networkId, tcrAddress]) 36 | 37 | const { metadata } = metaEvidence || {} 38 | 39 | if (!metaEvidence) return 40 | 41 | if (!metadata) 42 | return ( 43 | 47 | ) 48 | 49 | try { 50 | return ( 51 | 52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 | 61 | {!hideDetailsButton && ( 62 | 71 | )} 72 | 81 | 82 |
83 | ) 84 | } catch (err) { 85 | return 86 | } 87 | } 88 | 89 | TCRCardContent.propTypes = { 90 | tcrAddress: PropTypes.string, 91 | currentTCRAddress: PropTypes.string, 92 | ID: PropTypes.string, 93 | hideDetailsButton: PropTypes.bool 94 | } 95 | 96 | TCRCardContent.defaultProps = { 97 | tcrAddress: null, 98 | currentTCRAddress: null, 99 | ID: null, 100 | hideDetailsButton: false 101 | } 102 | 103 | export default TCRCardContent 104 | -------------------------------------------------------------------------------- /src/hooks/required-fees.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { PARTY, SUBGRAPH_RULING } from '../utils/item-status' 3 | 4 | const useRequiredFees = ({ 5 | side, 6 | sharedStakeMultiplier, 7 | winnerStakeMultiplier, 8 | loserStakeMultiplier, 9 | item, 10 | MULTIPLIER_DIVISOR, 11 | appealCost 12 | }) => 13 | useMemo(() => { 14 | if ( 15 | !sharedStakeMultiplier || 16 | !winnerStakeMultiplier || 17 | !loserStakeMultiplier || 18 | !MULTIPLIER_DIVISOR || 19 | !item || 20 | item.resolved || 21 | !appealCost 22 | ) 23 | return {} 24 | 25 | const round = item.requests 26 | ? item.requests[0].rounds[0] 27 | : item.challenges[0].rounds[0] // for pgtcr 28 | const { 29 | ruling: currentRuling, 30 | amountPaidRequester, 31 | amountPaidChallenger 32 | } = round 33 | 34 | // Calculate the fee stake multiplier. 35 | // The fee stake is the reward shared among parties that crowdfunded 36 | // the appeal of the party that wins the dispute. 37 | const sideIsWinner = 38 | currentRuling === SUBGRAPH_RULING.NONE 39 | ? null 40 | : (currentRuling === SUBGRAPH_RULING.ACCEPT && 41 | side === PARTY.REQUESTER) || 42 | (currentRuling === SUBGRAPH_RULING.REJECT && 43 | side === PARTY.CHALLENGER) 44 | const feeStakeMultiplier = 45 | sideIsWinner === null 46 | ? sharedStakeMultiplier 47 | : sideIsWinner 48 | ? winnerStakeMultiplier 49 | : loserStakeMultiplier 50 | 51 | // Calculate full cost to fund the side. 52 | // Full appeal cost = appeal cost + appeal cost * fee stake multiplier. 53 | const requiredForSide = appealCost.add( 54 | appealCost.mul(feeStakeMultiplier).div(MULTIPLIER_DIVISOR) 55 | ) 56 | 57 | if (requiredForSide.isZero()) return {} // No fees required. 58 | 59 | const amountPaid = side === 1 ? amountPaidRequester : amountPaidChallenger 60 | // Calculate amount still required to fully fund the side. 61 | const amountStillRequired = requiredForSide.sub(amountPaid) 62 | 63 | // Calculate the max reward the user can earn by contributing fees. 64 | // Potential reward = appeal cost * opponent fee stake multiplier * share available for contribution. 65 | const opponentFeeStakeMultiplier = 66 | sideIsWinner === null 67 | ? sharedStakeMultiplier 68 | : sideIsWinner 69 | ? loserStakeMultiplier 70 | : winnerStakeMultiplier 71 | 72 | // This is the total potential reward if the user contributed 100% of the fees. 73 | const totalReward = appealCost 74 | .mul(opponentFeeStakeMultiplier) 75 | .div(MULTIPLIER_DIVISOR) 76 | 77 | // Available reward = opponent fee stake * % contributions pending. 78 | const potentialReward = amountStillRequired 79 | .mul(MULTIPLIER_DIVISOR) 80 | .div(requiredForSide) 81 | .mul(totalReward) 82 | .div(MULTIPLIER_DIVISOR) 83 | return { requiredForSide, amountStillRequired, potentialReward, appealCost } 84 | }, [ 85 | MULTIPLIER_DIVISOR, 86 | item, 87 | loserStakeMultiplier, 88 | sharedStakeMultiplier, 89 | side, 90 | winnerStakeMultiplier, 91 | appealCost 92 | ]) 93 | 94 | export default useRequiredFees 95 | -------------------------------------------------------------------------------- /src/components/rich-address-input.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Input, Select } from 'antd' 2 | import { Field } from 'formik' 3 | import React from 'react' 4 | import { 5 | references, 6 | parseRichAddress, 7 | RichAddress 8 | } from '../utils/rich-address' 9 | 10 | const { Option } = Select 11 | 12 | const chainOptions = references 13 | .filter(reference => !reference?.deprecated) 14 | .map(reference => ( 15 | 21 | )) 22 | 23 | const defaultAddressType = `${references[0].namespaceId}:${references[0].id}` 24 | 25 | const RichAddressInput: React.FC<{ 26 | label: string 27 | name: string 28 | error: string 29 | touched: boolean 30 | hasFeedback: boolean 31 | disabled: boolean 32 | style: any 33 | values: any 34 | setFieldValue: any 35 | }> = p => { 36 | const value = p.values[p.name] 37 | const changeAddressType = (addressType: string) => { 38 | const richAddress = parseRichAddress(value) 39 | const address = richAddress ? richAddress.address : '' 40 | const newRichAddress = `${addressType}:${address}` 41 | p.setFieldValue(p.name, newRichAddress) 42 | } 43 | 44 | const changeAddress = ({ target }: any) => { 45 | const richAddress = parseRichAddress(value) 46 | const addressType = richAddress 47 | ? `${richAddress.reference.namespaceId}:${richAddress.reference.id}` 48 | : defaultAddressType 49 | const address = target.value 50 | const newRichAddress = `${addressType}:${address}` 51 | p.setFieldValue(p.name, newRichAddress) 52 | } 53 | 54 | return ( 55 | { 57 | const richAddress = parseRichAddress(value) as RichAddress 58 | if (!richAddress.passedTest) return 'Invalid format' 59 | 60 | return null 61 | }} 62 | name={p.name} 63 | style={{ style: p.style }} 64 | > 65 | {({ field }: any) => { 66 | const richAddress = parseRichAddress(field.value) 67 | const addressType = richAddress 68 | ? `${richAddress.reference.namespaceId}:${richAddress.reference.id}` 69 | : defaultAddressType 70 | const address = richAddress ? richAddress.address : '' 71 | return ( 72 | 78 | 86 | 93 | 94 | ) 95 | }} 96 | 97 | ) 98 | } 99 | 100 | export default RichAddressInput 101 | -------------------------------------------------------------------------------- /src/pages/item-details/item-action-modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { STATUS_CODE, getActionLabel } from 'utils/item-status' 3 | import RemoveModal from './modals/remove' 4 | import ChallengeModal from './modals/challenge' 5 | import SubmitModal from './modals/submit' 6 | import SubmitConnectModal from './modals/submit-connect' 7 | import CrowdfundModal from './modals/crowdfund' 8 | import PropTypes from 'prop-types' 9 | 10 | const ItemActionModal = ({ 11 | statusCode, 12 | isOpen, 13 | itemName, 14 | onClose, 15 | fileURI, 16 | item, 17 | isConnectedTCR, 18 | submissionDeposit, 19 | challengePeriodDuration, 20 | tcrAddress, 21 | metaEvidence, 22 | gtcrView, 23 | appealCost 24 | }) => { 25 | // Common button properties. 26 | const rest = { 27 | visible: isOpen, 28 | title: getActionLabel({ statusCode, itemName }), 29 | onCancel: onClose 30 | } 31 | 32 | switch (statusCode) { 33 | case STATUS_CODE.REGISTERED: { 34 | return ( 35 | 41 | ) 42 | } 43 | case STATUS_CODE.REJECTED: 44 | return isConnectedTCR ? ( 45 | 51 | ) : ( 52 | 60 | ) 61 | case STATUS_CODE.REMOVAL_REQUESTED: 62 | case STATUS_CODE.SUBMITTED: 63 | return ( 64 | 71 | ) 72 | case STATUS_CODE.CROWDFUNDING: 73 | case STATUS_CODE.CROWDFUNDING_WINNER: 74 | return ( 75 | 82 | ) 83 | case STATUS_CODE.WAITING_ARBITRATOR: 84 | case STATUS_CODE.WAITING_ENFORCEMENT: 85 | return null 86 | default: 87 | throw new Error(`Unhandled status code ${statusCode}`) 88 | } 89 | } 90 | 91 | ItemActionModal.propTypes = { 92 | statusCode: PropTypes.number.isRequired, 93 | isOpen: PropTypes.bool.isRequired, 94 | itemName: PropTypes.string.isRequired, 95 | onClose: PropTypes.func.isRequired, 96 | fileURI: PropTypes.string.isRequired, 97 | item: PropTypes.object.isRequired, 98 | isConnectedTCR: PropTypes.bool.isRequired, 99 | submissionDeposit: PropTypes.number.isRequired, 100 | challengePeriodDuration: PropTypes.number.isRequired, 101 | tcrAddress: PropTypes.string.isRequired, 102 | metaEvidence: PropTypes.string.isRequired, 103 | gtcrView: PropTypes.object.isRequired 104 | } 105 | 106 | export default ItemActionModal 107 | -------------------------------------------------------------------------------- /src/pages/light-item-details/item-action-modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { STATUS_CODE, getActionLabel } from 'utils/item-status' 3 | import RemoveModal from './modals/remove' 4 | import ChallengeModal from './modals/challenge' 5 | import SubmitModal from './modals/submit' 6 | import SubmitConnectModal from './modals/submit-connect' 7 | import CrowdfundModal from './modals/crowdfund' 8 | import PropTypes from 'prop-types' 9 | 10 | const ItemActionModal = ({ 11 | statusCode, 12 | isOpen, 13 | itemName, 14 | onClose, 15 | fileURI, 16 | item, 17 | isConnectedTCR, 18 | submissionDeposit, 19 | challengePeriodDuration, 20 | tcrAddress, 21 | metaEvidence, 22 | gtcrView, 23 | appealCost 24 | }) => { 25 | // Common button properties. 26 | const rest = { 27 | visible: isOpen, 28 | title: getActionLabel({ statusCode, itemName }), 29 | onCancel: onClose 30 | } 31 | 32 | switch (statusCode) { 33 | case STATUS_CODE.REGISTERED: { 34 | return ( 35 | 41 | ) 42 | } 43 | case STATUS_CODE.REJECTED: 44 | return isConnectedTCR ? ( 45 | 51 | ) : ( 52 | 60 | ) 61 | case STATUS_CODE.REMOVAL_REQUESTED: 62 | case STATUS_CODE.SUBMITTED: 63 | return ( 64 | 71 | ) 72 | case STATUS_CODE.CROWDFUNDING: 73 | case STATUS_CODE.CROWDFUNDING_WINNER: 74 | return ( 75 | 82 | ) 83 | case STATUS_CODE.WAITING_ARBITRATOR: 84 | case STATUS_CODE.WAITING_ENFORCEMENT: 85 | return null 86 | default: 87 | throw new Error(`Unhandled status code ${statusCode}`) 88 | } 89 | } 90 | 91 | ItemActionModal.propTypes = { 92 | statusCode: PropTypes.number.isRequired, 93 | isOpen: PropTypes.bool.isRequired, 94 | itemName: PropTypes.string.isRequired, 95 | onClose: PropTypes.func.isRequired, 96 | fileURI: PropTypes.string.isRequired, 97 | item: PropTypes.object.isRequired, 98 | isConnectedTCR: PropTypes.bool.isRequired, 99 | submissionDeposit: PropTypes.number.isRequired, 100 | challengePeriodDuration: PropTypes.number.isRequired, 101 | tcrAddress: PropTypes.string.isRequired, 102 | metaEvidence: PropTypes.string.isRequired, 103 | gtcrView: PropTypes.object.isRequired 104 | } 105 | 106 | export default ItemActionModal 107 | -------------------------------------------------------------------------------- /src/pages/light-items/tour-steps.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { parseIpfs } from 'utils/ipfs-parse' 3 | import { capitalizeFirstLetter, getArticleFor } from 'utils/string' 4 | 5 | const itemsTourSteps = metadata => { 6 | const { tcrTitle, itemName, itemNamePlural, metaEvidence } = metadata || {} 7 | const { fileURI } = metaEvidence || {} 8 | return [ 9 | { 10 | selector: `#tcr-info-column`, 11 | content: () => ( 12 |
13 | Let's take a quick tour of the list view.{' '} 14 | 15 | 🚎 16 | 17 |
18 |
19 | Here, you can view information of the current list. This gives you 20 | context on what each item is.{' '} 21 | {metadata && 22 | `In the case of ${tcrTitle}, each item is ${getArticleFor( 23 | itemName 24 | )} ${itemName?.toLowerCase()}`} 25 | . 26 |
27 | ) 28 | }, 29 | { 30 | selector: `#submit-item-button`, 31 | content: `To submit ${ 32 | itemName 33 | ? `${getArticleFor(itemName)} ${itemName.toLowerCase()}` 34 | : 'item' 35 | } to ${tcrTitle || 'the list'}, click this button.` 36 | }, 37 | { 38 | selector: `#policy-link`, 39 | content: () => ( 40 |
41 | Here you can find the listing policy for this list.{' '} 42 | 43 | 📜 44 | 45 |
46 |
47 | 48 | ⚠️ 49 | 50 | Before making your submission, make sure it complies with the{' '} 51 | Listing Policy. If you submit a 52 | non-compliant list, it will be rejected and you will lose your 53 | deposit. 54 | 55 | ⚠️ 56 | 57 |
58 | ) 59 | }, 60 | { 61 | selector: `#items-search-bar`, 62 | content: () => ( 63 |
64 | Use this bar to search for{' '} 65 | {itemName 66 | ? (itemNamePlural && itemNamePlural.toLowerCase()) || 67 | `${itemName.toLowerCase()}s` 68 | : 'items'}{' '} 69 | submitted by users. 70 | 71 | 🔍 72 | 73 |
74 | ) 75 | }, 76 | { 77 | selector: `#items-filters`, 78 | content: () => ( 79 |
80 | The filtering options will allow you to fine tune your search.{' '} 81 | 82 | 🔬 83 | 84 |
85 | ) 86 | }, 87 | { 88 | selector: `#items-grid-view`, 89 | content: `${ 90 | itemName 91 | ? capitalizeFirstLetter(itemNamePlural) || 92 | `${capitalizeFirstLetter(itemName)}s` 93 | : 'items' 94 | } in the "Submitted" and "Removing" state can be challenged to potentially earn rewards.` 95 | } 96 | ] 97 | } 98 | 99 | export default itemsTourSteps 100 | -------------------------------------------------------------------------------- /src/assets/abis/LightGTCRFactory.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "_GTCR", 7 | "type": "address" 8 | } 9 | ], 10 | "payable": false, 11 | "stateMutability": "nonpayable", 12 | "type": "constructor" 13 | }, 14 | { 15 | "anonymous": false, 16 | "inputs": [ 17 | { 18 | "indexed": true, 19 | "internalType": "contract LightGeneralizedTCR", 20 | "name": "_address", 21 | "type": "address" 22 | } 23 | ], 24 | "name": "NewGTCR", 25 | "type": "event" 26 | }, 27 | { 28 | "constant": true, 29 | "inputs": [], 30 | "name": "GTCR", 31 | "outputs": [ 32 | { 33 | "internalType": "address", 34 | "name": "", 35 | "type": "address" 36 | } 37 | ], 38 | "payable": false, 39 | "stateMutability": "view", 40 | "type": "function" 41 | }, 42 | { 43 | "constant": true, 44 | "inputs": [], 45 | "name": "count", 46 | "outputs": [ 47 | { 48 | "internalType": "uint256", 49 | "name": "", 50 | "type": "uint256" 51 | } 52 | ], 53 | "payable": false, 54 | "stateMutability": "view", 55 | "type": "function" 56 | }, 57 | { 58 | "constant": false, 59 | "inputs": [ 60 | { 61 | "internalType": "contract IArbitrator", 62 | "name": "_arbitrator", 63 | "type": "address" 64 | }, 65 | { 66 | "internalType": "bytes", 67 | "name": "_arbitratorExtraData", 68 | "type": "bytes" 69 | }, 70 | { 71 | "internalType": "address", 72 | "name": "_connectedTCR", 73 | "type": "address" 74 | }, 75 | { 76 | "internalType": "string", 77 | "name": "_registrationMetaEvidence", 78 | "type": "string" 79 | }, 80 | { 81 | "internalType": "string", 82 | "name": "_clearingMetaEvidence", 83 | "type": "string" 84 | }, 85 | { 86 | "internalType": "address", 87 | "name": "_governor", 88 | "type": "address" 89 | }, 90 | { 91 | "internalType": "uint256[4]", 92 | "name": "_baseDeposits", 93 | "type": "uint256[4]" 94 | }, 95 | { 96 | "internalType": "uint256", 97 | "name": "_challengePeriodDuration", 98 | "type": "uint256" 99 | }, 100 | { 101 | "internalType": "uint256[3]", 102 | "name": "_stakeMultipliers", 103 | "type": "uint256[3]" 104 | }, 105 | { 106 | "internalType": "address", 107 | "name": "_relayContract", 108 | "type": "address" 109 | } 110 | ], 111 | "name": "deploy", 112 | "outputs": [], 113 | "payable": false, 114 | "stateMutability": "nonpayable", 115 | "type": "function" 116 | }, 117 | { 118 | "constant": true, 119 | "inputs": [ 120 | { 121 | "internalType": "uint256", 122 | "name": "", 123 | "type": "uint256" 124 | } 125 | ], 126 | "name": "instances", 127 | "outputs": [ 128 | { 129 | "internalType": "contract LightGeneralizedTCR", 130 | "name": "", 131 | "type": "address" 132 | } 133 | ], 134 | "payable": false, 135 | "stateMutability": "view", 136 | "type": "function" 137 | } 138 | ] 139 | -------------------------------------------------------------------------------- /src/hooks/use-tcr-network.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { useHistory, useParams } from 'react-router' 3 | import { useWeb3Context } from 'web3-react' 4 | import { NETWORKS_INFO, NETWORK_STATUS, DEFAULT_NETWORK } from 'config/networks' 5 | import { hexlify } from 'utils/string' 6 | import { defaultTcrAddresses } from 'config/tcr-addresses' 7 | 8 | const useTcrNetwork = () => { 9 | const history = useHistory() 10 | const { networkId, active, library } = useWeb3Context() 11 | const { chainId } = useParams() 12 | const [networkStatus, setNetworkStatus] = useState(NETWORK_STATUS.unknown) 13 | 14 | useEffect(() => { 15 | window.ethereum && 16 | window.ethereum.on('chainChanged', chainId => { 17 | chainId = Number(chainId) 18 | const tcrAddress = defaultTcrAddresses[chainId] 19 | 20 | setNetworkStatus(status => { 21 | if (status !== NETWORK_STATUS.swtiching && tcrAddress) { 22 | setTimeout(() => { 23 | history.push(`/tcr/${chainId}/${tcrAddress}`) 24 | window.location.reload() 25 | }) 26 | return NETWORK_STATUS.supported 27 | } else return NETWORK_STATUS.unsupported 28 | }) 29 | }) 30 | // eslint-disable-next-line 31 | }, []) 32 | 33 | useEffect(() => { 34 | ;(async () => { 35 | // metamask is not provided 36 | if (!window.ethereum) return setNetworkStatus(NETWORK_STATUS.supported) 37 | 38 | // if the wallet is not connected yet 39 | if (!networkId) return setNetworkStatus(NETWORK_STATUS.unknown) 40 | 41 | let networkToSwitch = Number(chainId) 42 | 43 | if (!NETWORKS_INFO[chainId]) networkToSwitch = DEFAULT_NETWORK 44 | 45 | // if current network is already supported 46 | if (networkId === networkToSwitch) 47 | setNetworkStatus(NETWORK_STATUS.supported) 48 | // if it needs to change network or chainId 49 | // change network if the metamask network has not connected yet 50 | else { 51 | const hexlifiedChainId = hexlify(networkToSwitch) 52 | 53 | try { 54 | setNetworkStatus(NETWORK_STATUS.swtiching) 55 | await library.send('wallet_switchEthereumChain', [ 56 | { chainId: hexlifiedChainId } 57 | ]) 58 | } catch (err) { 59 | // the target network is not added to the metamask 60 | if (err.code === 4902) 61 | if (NETWORKS_INFO[chainId].rpc) { 62 | // add new network to metamask if the target network info is available 63 | setNetworkStatus(NETWORK_STATUS.adding) 64 | await library.send('wallet_addEthereumChain', [ 65 | { 66 | chainId: hexlifiedChainId, 67 | nativeCurrency: NETWORKS_INFO[chainId].nativeCurrency, 68 | chainName: NETWORKS_INFO[chainId].name, 69 | rpcUrls: NETWORKS_INFO[chainId].rpc, 70 | blockExplorerUrls: NETWORKS_INFO[chainId].explorers.url 71 | } 72 | ]) 73 | } 74 | // wait until a user adds the new network manually 75 | else { 76 | setNetworkStatus(NETWORK_STATUS.unsupported) 77 | } 78 | else setNetworkStatus(NETWORK_STATUS.unknown) 79 | } 80 | } 81 | })() 82 | // eslint-disable-next-line 83 | }, [active, networkId, library, chainId]) 84 | 85 | return { networkStatus, networkId, active, library } 86 | } 87 | 88 | export default useTcrNetwork 89 | -------------------------------------------------------------------------------- /src/hooks/use-path-validation.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { useHistory } from 'react-router' 3 | import { TCR_EXISTENCE_TEST } from 'utils/graphql' 4 | import { ApolloClient, InMemoryCache } from '@apollo/client' 5 | import { HttpLink } from '@apollo/client/link/http' 6 | import { useWeb3Context } from 'web3-react' 7 | import { SAVED_NETWORK_KEY } from 'utils/string' 8 | import { DEFAULT_NETWORK } from 'config/networks' 9 | import { defaultTcrAddresses, subgraphUrl } from 'config/tcr-addresses' 10 | 11 | const usePathValidation = () => { 12 | const history = useHistory() 13 | const { networkId, account } = useWeb3Context() 14 | 15 | const [pathResolved, setPathResolved] = useState(false) 16 | const [invalidTcrAddr, setInvalidTcrAddr] = useState(false) 17 | 18 | useEffect(() => { 19 | if (networkId === undefined) return 20 | if (account) return // their provider will prompt to change it 21 | const pathname = history.location.pathname 22 | const newPathRegex = /\/tcr\/(\d+)\/0x/ 23 | if (!newPathRegex.test(pathname)) return // let it redirect to new path first 24 | const matches = pathname.match(newPathRegex) 25 | const chainId = matches ? matches[1] : DEFAULT_NETWORK 26 | const pathChainId = Number(chainId) 27 | if (networkId !== pathChainId) { 28 | localStorage.setItem(SAVED_NETWORK_KEY, pathChainId.toString()) 29 | window.location.reload() 30 | } 31 | }, [history.location.pathname, networkId, account]) 32 | 33 | useEffect(() => { 34 | const checkPathValidation = async () => { 35 | const pathname = history.location.pathname 36 | const search = history.location.search 37 | const isOldPath = /\/tcr\/0x/.test(pathname) 38 | 39 | if (isOldPath) { 40 | let chainId = null 41 | const matches = pathname.match(/tcr\/(0x[0-9a-zA-Z]+)/) 42 | const tcrAddress = matches ? matches[1].toLowerCase() : null 43 | 44 | const ADDRs = Object.values(defaultTcrAddresses).map(addr => 45 | (addr as string).toLowerCase() 46 | ) 47 | const CHAIN_IDS = Object.keys(defaultTcrAddresses) 48 | const tcrIndex = ADDRs.findIndex(addr => addr === tcrAddress) 49 | 50 | if (tcrIndex >= 0) chainId = Number(CHAIN_IDS[tcrIndex]) 51 | else { 52 | const queryResults = await Promise.all( 53 | Object.values(subgraphUrl).map(subgraph => { 54 | const client = new ApolloClient({ 55 | link: new HttpLink({ uri: subgraph as string }), 56 | cache: new InMemoryCache() 57 | }) 58 | return client.query({ 59 | query: TCR_EXISTENCE_TEST, 60 | variables: { 61 | tcrAddress 62 | } 63 | }) 64 | }) 65 | ) 66 | const validIndex = queryResults.findIndex( 67 | ({ data: { lregistry, registry } }) => 68 | lregistry !== null || registry !== null 69 | ) 70 | 71 | if (validIndex >= 0) chainId = Object.keys(subgraphUrl)[validIndex] 72 | } 73 | 74 | if (chainId) { 75 | const newPathname = pathname.replace('/tcr/', `/tcr/${chainId}/`) 76 | history.push({ pathname: newPathname, search }) 77 | } else setInvalidTcrAddr(true) 78 | } 79 | setPathResolved(true) 80 | } 81 | checkPathValidation() 82 | }, [history, setPathResolved]) 83 | 84 | return [pathResolved, invalidTcrAddr] 85 | } 86 | 87 | export default usePathValidation 88 | -------------------------------------------------------------------------------- /src/pages/permanent-item-details/modals/withdraw.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react' 2 | import { Modal, Typography, Button, Alert } from 'antd' 3 | import styled from 'styled-components' 4 | import { ethers } from 'ethers' 5 | import _gtcr from 'assets/abis/PermanentGTCR.json' 6 | import { WalletContext } from 'contexts/wallet-context' 7 | import humanizeDuration from 'humanize-duration' 8 | 9 | export const StyledModal: any = styled(Modal)` 10 | & > .ant-modal-content { 11 | border-top-left-radius: 14px; 12 | border-top-right-radius: 14px; 13 | } 14 | ` 15 | 16 | export const StyledAlert = styled(Alert)` 17 | margin-bottom: 16px; 18 | text-transform: initial; 19 | ` 20 | 21 | interface WithdrawModalProps { 22 | isOpen: boolean 23 | onCancel: () => void 24 | item: any 25 | registry: any 26 | itemName: string 27 | } 28 | 29 | const WithdrawModal: React.FC = ({ 30 | isOpen, 31 | onCancel, 32 | item, 33 | registry, 34 | itemName 35 | }) => { 36 | const { pushWeb3Action } = useContext(WalletContext) 37 | const [loading, setLoading] = useState(false) 38 | 39 | const handleStartWithdraw = async () => { 40 | if (!item || !registry) return 41 | 42 | setLoading(true) 43 | 44 | const executeWithdraw = async (_: any, signer: any) => { 45 | const gtcr = new ethers.Contract(registry.id, _gtcr, signer) 46 | return { 47 | tx: await gtcr.startWithdrawItem(item.itemID), 48 | actionMessage: 'Starting withdrawal' 49 | } 50 | } 51 | 52 | try { 53 | await pushWeb3Action(executeWithdraw) 54 | onCancel() // Close modal on success 55 | } catch (err) { 56 | console.error('Withdrawal failed:', err) 57 | } finally { 58 | setLoading(false) 59 | } 60 | } 61 | 62 | return ( 63 | 70 | 76 | 77 | {registry?.withdrawingPeriod && ( 78 | 86 | )} 87 | 88 | 89 | Are you sure you want to withdraw "{itemName}" from the registry? This 90 | will initiate the withdrawal period after which the item will be 91 | permanently removed. 92 | 93 | 94 |
95 | 102 | 105 |
106 |
107 | ) 108 | } 109 | 110 | export default WithdrawModal 111 | -------------------------------------------------------------------------------- /src/bootstrap/app.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import styled from 'styled-components' 3 | import { BrowserRouter } from 'react-router-dom' 4 | import { Helmet } from 'react-helmet' 5 | import Footer from '../components/footer.tsx' 6 | import Web3Provider from 'web3-react' 7 | import { Layout } from 'antd' 8 | import { register } from './service-worker' 9 | import { WalletProvider } from 'contexts/wallet-context' 10 | import { TourProvider } from 'contexts/tour-context' 11 | import WalletModal from 'components/modals/wallet-modal' 12 | import WelcomeModal from 'components/modals/welcome-modal' 13 | import SmartContractWalletWarning from 'components/smart-contract-wallet-warning' 14 | import AppBar from 'components/layout/app-bar' 15 | import AppRouter from './app-router' 16 | import connectors from 'config/connectors' 17 | import 'antd/dist/antd.css' 18 | import './theme.css' 19 | import './fontawesome' 20 | 21 | const StyledClickaway = styled.div` 22 | background-color: black; 23 | position: fixed; 24 | width: 100%; 25 | height: 100%; 26 | opacity: ${properties => (properties.isMenuClosed ? 0 : 0.4)}; 27 | transition: opacity 0.3s; 28 | pointer-events: ${properties => (properties.isMenuClosed ? 'none' : 'auto')}; 29 | ` 30 | 31 | const StyledLayout = styled(Layout)` 32 | min-height: 100vh !important; 33 | ` 34 | 35 | const FooterWrapper = styled.div` 36 | margin-top: auto !important; 37 | ` 38 | 39 | const App = () => { 40 | const [isMenuClosed, setIsMenuClosed] = useState(true) 41 | 42 | // this useEffect redirects the URL to a correct one in case Court sent you to an incorrect URL using old ?chainId= syntax 43 | useEffect(() => { 44 | const url = window.location.href 45 | let tcrAddress, itemId, chainId 46 | 47 | if (url.includes('?chainId=')) { 48 | tcrAddress = url.split('/')[4] 49 | itemId = url.split('/')[5].split('?')[0] 50 | chainId = url.split('=')[1] 51 | const redirectUrl = url.replace( 52 | `/tcr/${tcrAddress}/${itemId}?chainId=${chainId}`, 53 | `/tcr/${chainId}/${tcrAddress}/${itemId}` 54 | ) 55 | window.location.replace(redirectUrl) 56 | } 57 | }, []) 58 | 59 | return ( 60 | 61 | 62 | 63 | 64 | 65 | Kleros · Curate 66 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | setIsMenuClosed(true)} 79 | /> 80 | 81 | 82 | 83 |