= ({
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 |
59 | Submit
60 |
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 |
62 | navigateAndScrollTop(
63 | `/tcr/${chainId}/${tcrAddress}/${item.tcrData.ID}`
64 | )
65 | }
66 | >
67 | Details
68 |
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 |
64 | navigateAndScrollTop(
65 | `/tcr/${networkId}/${currentTCRAddress}/${ID}`
66 | )
67 | }
68 | >
69 | Details
70 |
71 | )}
72 |
75 | navigateAndScrollTop(`/tcr/${networkId}/${tcrAddress}`)
76 | }
77 | style={{ marginLeft: '12px' }}
78 | >
79 | Open List
80 |
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 |
19 | {reference.name}
20 |
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 |
84 | {chainOptions}
85 |
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 |
100 | Back
101 |
102 |
103 | Start Withdraw
104 |
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 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | )
92 | }
93 |
94 | export default App
95 |
96 | register({
97 | onUpdate: () =>
98 | console.info('An update is ready. Please close and reopen all tabs.', 0)
99 | })
100 |
--------------------------------------------------------------------------------
/src/components/light-tcr-card-content.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import styled from 'styled-components'
3 | import { Result, Skeleton, Button } from 'antd'
4 | import PropTypes from 'prop-types'
5 | import { useWeb3Context } from 'web3-react'
6 | import { ItemTypes } from '@kleros/gtcr-encoder'
7 | import DisplaySelector from './display-selector'
8 | import { fetchMetaEvidence } from 'hooks/tcr-view'
9 | import useNavigateAndScrollTop from 'hooks/navigate-and-scroll-top'
10 | import { parseIpfs } from 'utils/ipfs-parse'
11 |
12 | export const Container = styled.div`
13 | height: 100%;
14 | display: flex;
15 | flex-direction: column;
16 | justify-content: space-between;
17 | `
18 |
19 | export const StyledItemCol = styled.div`
20 | margin-bottom: 8px;
21 | text-align: center;
22 | display: flex;
23 | align-items: center;
24 | justify-content: center;
25 | `
26 |
27 | export const StyledResult = styled(Result)`
28 | padding: 0;
29 |
30 | & > .ant-result-title {
31 | line-height: 1.2;
32 | font-size: 1.4em;
33 | }
34 | `
35 |
36 | const TCRCardContent = ({
37 | tcrAddress,
38 | currentTCRAddress,
39 | ID,
40 | hideDetailsButton
41 | }) => {
42 | const { networkId } = useWeb3Context()
43 |
44 | const [metaEvidence, setMetaEvidence] = useState()
45 | const navigateAndScrollTop = useNavigateAndScrollTop()
46 |
47 | useEffect(() => {
48 | ;(async () => {
49 | const fetchedData = await fetchMetaEvidence(tcrAddress, networkId)
50 |
51 | const response = await fetch(parseIpfs(fetchedData.metaEvidenceURI))
52 | const file = await response.json()
53 | setMetaEvidence(file)
54 | })()
55 | }, [networkId, tcrAddress])
56 | const { metadata } = metaEvidence || {}
57 |
58 | if (!metaEvidence) return
59 |
60 | if (!metadata)
61 | return (
62 |
66 | )
67 |
68 | try {
69 | return (
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | {!hideDetailsButton && (
81 |
83 | navigateAndScrollTop(
84 | `/tcr/${networkId}/${currentTCRAddress}/${ID}`
85 | )
86 | }
87 | >
88 | Details
89 |
90 | )}
91 |
94 | navigateAndScrollTop(`/tcr/${networkId}/${tcrAddress}`)
95 | }
96 | style={{ marginLeft: '12px' }}
97 | >
98 | Open List
99 |
100 |
101 |
102 | )
103 | } catch (err) {
104 | return
105 | }
106 | }
107 |
108 | TCRCardContent.propTypes = {
109 | tcrAddress: PropTypes.string,
110 | currentTCRAddress: PropTypes.string,
111 | ID: PropTypes.string,
112 | hideDetailsButton: PropTypes.bool
113 | }
114 |
115 | TCRCardContent.defaultProps = {
116 | tcrAddress: null,
117 | currentTCRAddress: null,
118 | ID: null,
119 | hideDetailsButton: false
120 | }
121 |
122 | export default TCRCardContent
123 |
--------------------------------------------------------------------------------
/src/pages/light-items/item-card-title.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useMemo } from 'react'
2 | import styled from 'styled-components'
3 | import { Tooltip, Icon } from 'antd'
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
5 | import PropTypes from 'prop-types'
6 | import ItemStatusBadge from 'components/item-status-badge'
7 | import ETHAmount from 'components/eth-amount'
8 | import ItemPropTypes from 'prop-types/item'
9 | import { itemToStatusCode } from 'utils/item-status'
10 | import { WalletContext } from 'contexts/wallet-context'
11 | import { LightTCRViewContext } from 'contexts/light-tcr-view-context'
12 | import useHumanizedCountdown from 'hooks/countdown'
13 | import useNativeCurrency from 'hooks/native-currency'
14 |
15 | export const Container = styled.div`
16 | display: flex;
17 | flex-direction: column;
18 | min-height: 40.5px;
19 | justify-content: center;
20 | `
21 |
22 | export const StatusAndBountyContainer = styled.div`
23 | display: flex;
24 | justify-content: space-between;
25 | `
26 |
27 | export const BountyContainer = styled.div`
28 | display: flex;
29 | flex-direction: column;
30 | `
31 |
32 | export const StyledFontAwesomeIcon = styled(FontAwesomeIcon)`
33 | margin-left: 6px;
34 | `
35 |
36 | export const CountdownContainer = styled.div`
37 | color: #ffffff5c;
38 | font-size: 13px;
39 | margin-left: 12px;
40 | `
41 |
42 | const ItemCardTitle = ({ statusCode, tcrData }) => {
43 | const { challengePeriodDuration } = useContext(LightTCRViewContext)
44 | const { timestamp } = useContext(WalletContext)
45 | const { disputed, submissionTime } = tcrData || {}
46 | const nativeCurrency = useNativeCurrency()
47 |
48 | // Get remaining challenge period, if applicable and build countdown.
49 | const challengeRemainingTime = useMemo(() => {
50 | if (!tcrData || disputed || !submissionTime || !challengePeriodDuration)
51 | return
52 |
53 | const deadline =
54 | submissionTime.add(challengePeriodDuration).toNumber() * 1000
55 |
56 | return deadline - Date.now()
57 | }, [challengePeriodDuration, disputed, submissionTime, tcrData])
58 |
59 | const challengeCountdown = useHumanizedCountdown(challengeRemainingTime, 1)
60 |
61 | if (typeof statusCode !== 'number')
62 | statusCode = itemToStatusCode(tcrData, timestamp, challengePeriodDuration)
63 |
64 | const bounty = tcrData.deposit
65 |
66 | return (
67 |
68 |
69 |
70 | {challengeRemainingTime > 0 && (
71 |
72 |
73 |
78 |
79 |
80 |
81 | )}
82 |
83 | {challengeRemainingTime > 0 && (
84 |
85 | Ends {challengeCountdown}
86 |
87 |
88 |
89 |
90 |
91 | )}
92 |
93 | )
94 | }
95 |
96 | ItemCardTitle.propTypes = {
97 | statusCode: PropTypes.number,
98 | tcrData: ItemPropTypes
99 | }
100 |
101 | ItemCardTitle.defaultProps = {
102 | statusCode: null,
103 | tcrData: null
104 | }
105 |
106 | export default ItemCardTitle
107 |
--------------------------------------------------------------------------------
/src/components/permanent-tcr-card-content.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import styled from 'styled-components'
3 | import { Result, Skeleton, Button } from 'antd'
4 | import PropTypes from 'prop-types'
5 | import { useWeb3Context } from 'web3-react'
6 | import { ItemTypes } from '@kleros/gtcr-encoder'
7 | import DisplaySelector from './display-selector'
8 | import { fetchMetaEvidence } from 'hooks/tcr-view'
9 | import useNavigateAndScrollTop from 'hooks/navigate-and-scroll-top'
10 | import { parseIpfs } from 'utils/ipfs-parse'
11 |
12 | // Note: I think this file may be useless?
13 |
14 | export const Container = styled.div`
15 | height: 100%;
16 | display: flex;
17 | flex-direction: column;
18 | justify-content: space-between;
19 | `
20 |
21 | export const StyledItemCol = styled.div`
22 | margin-bottom: 8px;
23 | text-align: center;
24 | display: flex;
25 | align-items: center;
26 | justify-content: center;
27 | `
28 |
29 | export const StyledResult = styled(Result)`
30 | padding: 0;
31 |
32 | & > .ant-result-title {
33 | line-height: 1.2;
34 | font-size: 1.4em;
35 | }
36 | `
37 |
38 | const TCRCardContent = ({
39 | tcrAddress,
40 | currentTCRAddress,
41 | ID,
42 | hideDetailsButton
43 | }) => {
44 | const { networkId } = useWeb3Context()
45 |
46 | const [metaEvidence, setMetaEvidence] = useState()
47 | const navigateAndScrollTop = useNavigateAndScrollTop()
48 |
49 | useEffect(() => {
50 | ;(async () => {
51 | const fetchedData = await fetchMetaEvidence(tcrAddress, networkId)
52 | const response = await fetch(parseIpfs(fetchedData.metaEvidenceURI))
53 | const file = await response.json()
54 | setMetaEvidence(file)
55 | })()
56 | }, [networkId, tcrAddress])
57 | const { metadata } = metaEvidence || {}
58 |
59 | if (!metaEvidence) return
60 |
61 | if (!metadata)
62 | return (
63 |
67 | )
68 |
69 | try {
70 | return (
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | {!hideDetailsButton && (
82 |
84 | navigateAndScrollTop(
85 | `/tcr/${networkId}/${currentTCRAddress}/${ID}`
86 | )
87 | }
88 | >
89 | Details
90 |
91 | )}
92 |
95 | navigateAndScrollTop(`/tcr/${networkId}/${tcrAddress}`)
96 | }
97 | style={{ marginLeft: '12px' }}
98 | >
99 | Open List
100 |
101 |
102 |
103 | )
104 | } catch (err) {
105 | return
106 | }
107 | }
108 |
109 | TCRCardContent.propTypes = {
110 | tcrAddress: PropTypes.string,
111 | currentTCRAddress: PropTypes.string,
112 | ID: PropTypes.string,
113 | hideDetailsButton: PropTypes.bool
114 | }
115 |
116 | TCRCardContent.defaultProps = {
117 | tcrAddress: null,
118 | currentTCRAddress: null,
119 | ID: null,
120 | hideDetailsButton: false
121 | }
122 |
123 | export default TCRCardContent
124 |
--------------------------------------------------------------------------------
/src/pages/item-details/modals/evidence.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { Typography, Button } from 'antd'
3 | import { ethers } from 'ethers'
4 | import { abi as _gtcr } from '@kleros/tcr/build/contracts/GeneralizedTCR.json'
5 | import { TCRViewContext } from 'contexts/tcr-view-context'
6 | import { WalletContext } from 'contexts/wallet-context'
7 | import itemPropTypes from 'prop-types/item'
8 | import EvidenceForm from 'components/evidence-form.js'
9 | import ipfsPublish from 'utils/ipfs-publish.js'
10 | import { getIPFSPath } from 'utils/get-ipfs-path'
11 | import { TourContext } from 'contexts/tour-context'
12 | import { StyledModal } from 'pages/light-item-details/modals/challenge'
13 |
14 | const EvidenceModal = ({ item, ...rest }) => {
15 | // Get contract data.
16 | const { tcrAddress } = useContext(TCRViewContext)
17 | const { pushWeb3Action } = useContext(WalletContext)
18 | const { setUserSubscribed } = useContext(TourContext)
19 |
20 | const submitEvidence = async ({ title, description, evidenceAttachment }) => {
21 | pushWeb3Action(async ({ account, networkId }, signer) => {
22 | try {
23 | const gtcr = new ethers.Contract(tcrAddress, _gtcr, signer)
24 | const evidenceJSON = {
25 | title: title,
26 | description,
27 | ...evidenceAttachment
28 | }
29 |
30 | const enc = new TextEncoder()
31 | const fileData = enc.encode(JSON.stringify(evidenceJSON))
32 | /* eslint-enable prettier/prettier */
33 | const ipfsEvidencePath = getIPFSPath(
34 | await ipfsPublish('evidence.json', fileData)
35 | )
36 |
37 | // Request signature and submit.
38 | const tx = await gtcr.submitEvidence(item.itemID, ipfsEvidencePath)
39 |
40 | rest.onCancel() // Hide the submission modal.
41 |
42 | // Subscribe for notifications
43 | if (process.env.REACT_APP_NOTIFICATIONS_API_URL && !!networkId)
44 | fetch(
45 | `${process.env.REACT_APP_NOTIFICATIONS_API_URL}/${networkId}/api/subscribe`,
46 | {
47 | method: 'post',
48 | headers: { 'Content-Type': 'application/json' },
49 | body: JSON.stringify({
50 | subscriberAddr: ethers.utils.getAddress(account),
51 | tcrAddr: tcrAddress,
52 | itemID: item.itemID,
53 | networkID: networkId
54 | })
55 | }
56 | )
57 | .then(() => setUserSubscribed(true))
58 | .catch(err => {
59 | console.error('Failed to subscribe for notifications.', err)
60 | })
61 | return {
62 | tx,
63 | actionMessage: 'Submitting evidence'
64 | }
65 | } catch (err) {
66 | console.error('Error submitting evidence:', err)
67 | }
68 | })
69 | }
70 |
71 | const EVIDENCE_FORM_ID = 'submitEvidenceForm'
72 |
73 | return (
74 |
77 | Back
78 | ,
79 |
85 | Submit
86 |
87 | ]}
88 | {...rest}
89 | >
90 | Evidence Submission
91 |
96 |
97 | )
98 | }
99 |
100 | EvidenceModal.propTypes = {
101 | item: itemPropTypes
102 | }
103 |
104 | EvidenceModal.defaultProps = {
105 | item: null
106 | }
107 |
108 | export default EvidenceModal
109 |
--------------------------------------------------------------------------------
/src/pages/light-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/LightGeneralizedTCR.json'
5 | import { LightTCRViewContext } from 'contexts/light-tcr-view-context'
6 | import { WalletContext } from 'contexts/wallet-context'
7 | import itemPropTypes from 'prop-types/item'
8 | import EvidenceForm from 'components/evidence-form.js'
9 | import ipfsPublish from 'utils/ipfs-publish.js'
10 | import { getIPFSPath } from 'utils/get-ipfs-path'
11 | import { TourContext } from 'contexts/tour-context'
12 | import { StyledModal } from './challenge'
13 |
14 | const EvidenceModal = ({ item, ...rest }) => {
15 | // Get contract data.
16 | const { tcrAddress } = useContext(LightTCRViewContext)
17 | const { pushWeb3Action } = useContext(WalletContext)
18 | const { setUserSubscribed } = useContext(TourContext)
19 |
20 | const submitEvidence = async ({ title, description, evidenceAttachment }) => {
21 | pushWeb3Action(async ({ account, networkId }, signer) => {
22 | try {
23 | const gtcr = new ethers.Contract(tcrAddress, _gtcr, signer)
24 | const evidenceJSON = {
25 | title: title,
26 | description,
27 | ...evidenceAttachment
28 | }
29 |
30 | const enc = new TextEncoder()
31 | const fileData = enc.encode(JSON.stringify(evidenceJSON))
32 | /* eslint-enable prettier/prettier */
33 | const ipfsEvidencePath = getIPFSPath(
34 | await ipfsPublish('evidence.json', fileData)
35 | )
36 |
37 | // Request signature and submit.
38 | const tx = await gtcr.submitEvidence(item.itemID, ipfsEvidencePath)
39 |
40 | rest.onCancel() // Hide the submission modal.
41 |
42 | // Subscribe for notifications
43 | if (process.env.REACT_APP_NOTIFICATIONS_API_URL && !!networkId)
44 | fetch(
45 | `${process.env.REACT_APP_NOTIFICATIONS_API_URL}/${networkId}/api/subscribe`,
46 | {
47 | method: 'post',
48 | headers: { 'Content-Type': 'application/json' },
49 | body: JSON.stringify({
50 | subscriberAddr: ethers.utils.getAddress(account),
51 | tcrAddr: tcrAddress,
52 | itemID: item.itemID,
53 | networkID: networkId
54 | })
55 | }
56 | )
57 | .then(() => setUserSubscribed(true))
58 | .catch(err => {
59 | console.error('Failed to subscribe for notifications.', err)
60 | })
61 | return {
62 | tx,
63 | actionMessage: 'Submitting evidence',
64 | onTxMined: () => {}
65 | }
66 | } catch (err) {
67 | console.error('Error submitting evidence:', err)
68 | }
69 | })
70 | }
71 |
72 | const EVIDENCE_FORM_ID = 'submitEvidenceForm'
73 |
74 | return (
75 |
78 | Back
79 | ,
80 |
86 | Submit
87 |
88 | ]}
89 | {...rest}
90 | >
91 | Evidence Submission
92 |
97 |
98 | )
99 | }
100 |
101 | EvidenceModal.propTypes = {
102 | item: itemPropTypes
103 | }
104 |
105 | EvidenceModal.defaultProps = {
106 | item: null
107 | }
108 |
109 | export default EvidenceModal
110 |
--------------------------------------------------------------------------------
/src/bootstrap/app-router.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo } from 'react'
2 | import { Route, Switch, Redirect } from 'react-router-dom'
3 | import { ApolloProvider } from '@apollo/client'
4 | import { useWeb3Context, Connectors } from 'web3-react'
5 | import loadable from '@loadable/component'
6 | import ErrorPage from 'pages/error-page'
7 | import NoWeb3Detected from 'pages/no-web3'
8 | import Loading from 'components/loading'
9 | import connectors from 'config/connectors'
10 | import { DEFAULT_NETWORK } from 'config/networks'
11 | import { hexlify } from 'utils/string'
12 | import usePathValidation from 'hooks/use-path-validation'
13 | import useGraphQLClient from 'hooks/use-graphql-client'
14 | import { Web3ContextCurate } from 'types/web3-context'
15 | import { defaultTcrAddresses, validChains } from 'config/tcr-addresses'
16 |
17 | const { Connector } = Connectors
18 |
19 | const ItemsRouter = loadable(
20 | () => import(/* webpackPrefetch: true */ 'pages/items-router'),
21 | { fallback: }
22 | )
23 |
24 | const ItemDetailsRouter = loadable(
25 | () => import(/* webpackPrefetch: true */ 'pages/item-details-router'),
26 | { fallback: }
27 | )
28 |
29 | const Factory = loadable(
30 | () => import(/* webpackPrefetch: true */ 'pages/factory/index'),
31 | { fallback: }
32 | )
33 |
34 | const ClassicFactory = loadable(
35 | () => import(/* webpackPrefetch: true */ 'pages/factory-classic/index'),
36 | { fallback: }
37 | )
38 |
39 | const PermanentFactory = loadable(
40 | () => import(/* webpackPrefetch: true */ 'pages/factory-permanent/index'),
41 | { fallback: }
42 | )
43 |
44 | const AppRouter = () => {
45 | const { networkId, error }: Web3ContextCurate = useWeb3Context()
46 | const isUnsupported = useMemo(
47 | () => error?.code === Connector.errorCodes.UNSUPPORTED_NETWORK,
48 | [error]
49 | )
50 | const tcrAddress = defaultTcrAddresses[networkId as validChains]
51 | const [pathResolved, invalidTcrAddr] = usePathValidation()
52 | const client = useGraphQLClient(networkId)
53 |
54 | useEffect(() => {
55 | if (isUnsupported && window.ethereum) {
56 | const chainIdTokens = window.location.pathname.match(/\/tcr\/(\d+)\//)
57 | const chainId = hexlify(
58 | chainIdTokens && chainIdTokens?.length > 1
59 | ? chainIdTokens[1]
60 | : DEFAULT_NETWORK
61 | )
62 |
63 | window.ethereum.request({
64 | method: 'wallet_switchEthereumChain',
65 | params: [{ chainId }]
66 | })
67 | }
68 | }, [isUnsupported])
69 |
70 | if (Object.entries(connectors).length === 0) return
71 |
72 | if (isUnsupported && error)
73 | return (
74 |
80 | Switching network to supported one
81 |
82 | >
83 | }
84 | />
85 | )
86 | else if (!networkId || !pathResolved) return
87 | else if (invalidTcrAddr || !client) return
88 |
89 | return (
90 |
91 |
92 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | )
105 | }
106 |
107 | export default AppRouter
108 |
--------------------------------------------------------------------------------
/src/components/base-deposit-input.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 | import styled from 'styled-components'
3 | import { Form, Input, Tooltip, Icon } from 'antd'
4 | import { bigNumberify, parseEther } from 'ethers/utils'
5 | import { Field } from 'formik'
6 | import PropTypes from 'prop-types'
7 | import BNPropType from '../prop-types/bn'
8 | import ETHAmount from './eth-amount'
9 | import useNativeCurrency from '../hooks/native-currency'
10 |
11 | const BasedDepositContainer = styled.div`
12 | display: flex;
13 | align-items: center;
14 | `
15 | const TotalCostContainer = styled.span`
16 | min-width: 15%;
17 | text-align: center;
18 | `
19 |
20 | const BaseDepositInput = ({
21 | label,
22 | name,
23 | error,
24 | touched,
25 | hasFeedback,
26 | disabled,
27 | arbitrationCost,
28 | values
29 | }) => {
30 | const baseDeposit = useMemo(() => {
31 | try {
32 | return bigNumberify(parseEther(values[name].toString()))
33 | } catch (err) {
34 | console.warn('failed to parse basedeposit value', err)
35 | // No op. Wait for proper user input.
36 | }
37 | }, [name, values])
38 | const nativeCurrency = useNativeCurrency()
39 |
40 | const totalDeposit = useMemo(
41 | () => baseDeposit && arbitrationCost && baseDeposit.add(arbitrationCost),
42 | [arbitrationCost, baseDeposit]
43 | )
44 |
45 | return (
46 |
47 |
48 | {({ field }) => (
49 |
55 |
56 |
63 |
64 | Total
65 |
66 |
67 |
68 | :{' '}
69 |
74 |
75 |
76 |
77 | )}
78 |
79 |
80 | )
81 | }
82 |
83 | BaseDepositInput.propTypes = {
84 | name: PropTypes.string.isRequired,
85 | label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
86 | error: PropTypes.string,
87 | touched: PropTypes.bool,
88 | hasFeedback: PropTypes.bool,
89 | step: PropTypes.number,
90 | disabled: PropTypes.bool,
91 | arbitrationCost: BNPropType,
92 | values: PropTypes.oneOfType([
93 | PropTypes.shape({
94 | submissionBaseDeposit: PropTypes.number.isRequired,
95 | removalBaseDeposit: PropTypes.number.isRequired,
96 | submissionChallengeBaseDeposit: PropTypes.number.isRequired,
97 | removalChallengeBaseDeposit: PropTypes.number.isRequired
98 | }).isRequired,
99 | PropTypes.shape({
100 | relSubmissionBaseDeposit: PropTypes.number.isRequired,
101 | relRemovalBaseDeposit: PropTypes.number.isRequired,
102 | relSubmissionChallengeBaseDeposit: PropTypes.number.isRequired,
103 | relRemovalChallengeBaseDeposit: PropTypes.number.isRequired
104 | }).isRequired
105 | ]).isRequired
106 | }
107 |
108 | BaseDepositInput.defaultProps = {
109 | label: null,
110 | error: null,
111 | touched: null,
112 | hasFeedback: null,
113 | step: null,
114 | disabled: null,
115 | arbitrationCost: null
116 | }
117 |
118 | export default BaseDepositInput
119 |
--------------------------------------------------------------------------------