diff --git a/package.json b/package.json index 571dad61..355bf93e 100644 --- a/package.json +++ b/package.json @@ -31,14 +31,19 @@ "styled-components": "^4.4.1", "subscriptions-transport-ws": "^0.9.16", "urql": "^1.6.3", + "wonka": "^4.0.7", "use-wallet": "^0.4.4" }, "scripts": { "start": "npm run sync-assets && REACT_APP_BUILD=$(git log --pretty=format:'%h' -n 1) react-app-rewired start", "start:local": "REACT_APP_CHAIN_ID=1337 npm run start ", + "start:local:mock": "REACT_APP_MOCK_DATA=1 npm run start:local", "start:mainnet": "REACT_APP_CHAIN_ID=1 npm run start", + "start:mainnet:mock": "REACT_APP_MOCK_DATA=1 npm run start:mainnet", "start:ropsten": "REACT_APP_CHAIN_ID=3 npm run start", + "start:ropsten:mock": "REACT_APP_MOCK_DATA=1 npm run start:ropsten", "start:rinkeby": "REACT_APP_CHAIN_ID=4 npm run start", + "start:rinkeby:mock": "REACT_APP_MOCK_DATA=1 npm run start:rinkeby", "build": "./scripts/build.sh", "build:local": "REACT_APP_CHAIN_ID=1337 npm run build", "build:mainnet": "REACT_APP_CHAIN_ID=1 npm run build", diff --git a/src/environment.js b/src/environment.js index 867fa27c..531f3613 100644 --- a/src/environment.js +++ b/src/environment.js @@ -28,6 +28,9 @@ const ENV_VARS = { ENABLE_SENTRY() { return process.env.REACT_APP_ENABLE_SENTRY === '1' }, + MOCK_DATA() { + return process.env.REACT_APP_MOCK_DATA === '1' + }, DEFAULT_ETH_NODE() { return process.env.REACT_APP_DEFAULT_ETH_NODE || '' }, diff --git a/src/graphql-exchanges.js b/src/graphql-exchanges.js index c5dd99a4..b909c38e 100644 --- a/src/graphql-exchanges.js +++ b/src/graphql-exchanges.js @@ -1,11 +1,13 @@ import { fetchExchange, subscriptionExchange } from 'urql' import { SubscriptionClient } from 'subscriptions-transport-ws' import { captureMessage } from '@sentry/browser' +import env from './environment' import endpoints from './endpoints' +import { mockFetchExchange, mockSubscriptionExchange } from './mock/exchanges' const GRAPH_API_ENDPOINTS = endpoints() const subscriptionClient = new SubscriptionClient(GRAPH_API_ENDPOINTS[1], { - reconnect: true, + reconnect: !env('MOCK_DATA'), reconnectionAttempts: 10, }) @@ -16,11 +18,13 @@ const DEFAULT_SUBSCRIPTION_EXCHANGE = subscriptionExchange({ }) export function getFetchExchange() { - return DEFAULT_FETCH_EXCHANGE + return env('MOCK_DATA') ? mockFetchExchange : DEFAULT_FETCH_EXCHANGE } export function getSubscriptionExchange() { - return DEFAULT_SUBSCRIPTION_EXCHANGE + return env('MOCK_DATA') + ? mockSubscriptionExchange + : DEFAULT_SUBSCRIPTION_EXCHANGE } let connectionAttempts = 0 diff --git a/src/hooks/query-hooks.js b/src/hooks/query-hooks.js index 52578f50..327c72ac 100644 --- a/src/hooks/query-hooks.js +++ b/src/hooks/query-hooks.js @@ -1,8 +1,8 @@ import { useQuery } from 'urql' -import { JurorFirstANJActivationMovement } from '../queries/balances' -import { ActiveJurors, JurorFeesClaimed } from '../queries/juror' import { JurorDrafts } from '../queries/jurorDrafts' +import { ActiveJurors, JurorFeesClaimed } from '../queries/juror' +import { JurorFirstANJActivationMovement } from '../queries/balances' export function useJurorDraftQuery(jurorId) { const [result] = useQuery({ diff --git a/src/hooks/subscription-hooks.js b/src/hooks/subscription-hooks.js index d3d79646..29146cc4 100644 --- a/src/hooks/subscription-hooks.js +++ b/src/hooks/subscription-hooks.js @@ -4,19 +4,19 @@ import { useCourtConfig } from '../providers/CourtConfig' // queries import { OpenTasks } from '../queries/tasks' +import { AllDisputes, SingleDispute } from '../queries/disputes' +import { AppealsByMaker, AppealsByTaker } from '../queries/appeals' +import { JurorDraftsFrom, JurorDraftsNotRewarded } from '../queries/jurorDrafts' import { CourtConfig, FeeMovements, JurorsRegistryModule, } from '../queries/court' -import { AllDisputes, SingleDispute } from '../queries/disputes' -import { AppealsByMaker, AppealsByTaker } from '../queries/appeals' import { JurorANJBalances, JurorANJWalletBalance, JurorTreasuryBalances, } from '../queries/balances' -import { JurorDraftsFrom, JurorDraftsNotRewarded } from '../queries/jurorDrafts' // utils import { bigNum } from '../lib/math-utils' diff --git a/src/hooks/useCourtContracts.js b/src/hooks/useCourtContracts.js index b77ac295..7bf9dfbe 100644 --- a/src/hooks/useCourtContracts.js +++ b/src/hooks/useCourtContracts.js @@ -592,6 +592,7 @@ export function useTotalANTStakedPolling(timeout = 1000) { timeoutId = setTimeout(() => { const agentBalancePromise = antContract.balanceOf(networkAgentAddress) const vaultBalancePromise = antContract.balanceOf(networkReserveAddress) + return Promise.all([agentBalancePromise, vaultBalancePromise]) .then(([antInAgent, antInVault]) => { if (!cancelled) { diff --git a/src/mock-data.js b/src/mock-data.js deleted file mode 100644 index 2955df0e..00000000 --- a/src/mock-data.js +++ /dev/null @@ -1,58 +0,0 @@ -export const balances = { - wallet: { amount: '3.304,76', value: '3.300' }, - inactive: { amount: '3.304,76', tokenSymbol: 'ANJ', value: '3.300' }, - active: { amount: '3.304,76', value: '3.300' }, - rewards: { amount: '3.304,76', value: '3.300' }, -} - -export const latestActivity = [ - { - account: '0x8401Eb5ff34cc943f096A32EF3d5113FEbE8D4Eb', - action: 'Started', - target: { label: 'review evidence', link: 'url' }, - date: '26/11/19 AT 16:00', - }, - { - account: '0xb4124cEB3451635DAcedd11767f004d8a28c6eE7', - action: 'Comitted their', - target: { label: 'vote', link: 'url' }, - date: '26/11/19 AT 16:00', - }, - { - account: '0x49C01b61Aa3e4cD4C4763c78EcFE75888b49ef50', - action: 'Executed', - target: { label: 'ruling', link: 'url' }, - date: '26/11/19 AT 16:00', - }, -] - -export const tasks = [ - { - taskName: 'Commit Vote', - disputeId: 12, - status: 'open', - juror: '0x593e1F9809658d0c92e9f092cF01Aad7D0d734f3', - dueDate: 1578928467000, - }, - { - taskName: 'Reveal vote', - disputeId: 15, - status: 'open', - juror: '0x099278297012066d61c9505132b3Aa71F625E414', - dueDate: 1578928467000, - }, - { - taskName: 'Commit Vote', - disputeId: 20, - status: 'open', - juror: '0x593e1F9809658d0c92e9f092cF01Aad7D0d734f3', - dueDate: 1578928467000, - }, - { - taskName: 'Commit vote', - disputeId: 14, - status: 'open', - juror: '0x099278297012066d61c9505132b3Aa71F625E414', - dueDate: 1578928467000, - }, -] diff --git a/src/mock/data/CourtConfig.js b/src/mock/data/CourtConfig.js new file mode 100644 index 00000000..48c60bf1 --- /dev/null +++ b/src/mock/data/CourtConfig.js @@ -0,0 +1,61 @@ +import { getNetworkConfig } from '../../networks' +import { bigExp } from '../helper' +import dayjs from 'dayjs' + +const TERM_DURATION = 60 // 1 minute +const CURRENT_TERM = 100 +const COURT_START = dayjs() // Court started 100 terms ago + .subtract(TERM_DURATION * CURRENT_TERM, 'second') + .unix() + +const anjToken = { + id: '', + name: 'Aragon Network Juror Token', + symbol: 'ANJ', + decimals: 18, +} + +const feeToken = { + id: '', + name: 'Dai stablecoin', + symbol: 'DAI', + decimals: 18, +} + +const courtConfig = { + id: getNetworkConfig().court, + currentTerm: CURRENT_TERM, + termDuration: TERM_DURATION, // 4 minutes + anjToken, + feeToken, + jurorFee: bigExp('10'), + draftFee: bigExp('18', 16), + settleFee: bigExp('1', 17), + evidenceTerms: 21, + commitTerms: '3', + revealTerms: '3', + appealTerms: '3', + appealConfirmationTerms: '3', + terms: [ + { + startTime: COURT_START, + }, + ], + finalRoundReduction: '5000', + firstRoundJurorsNumber: '3', + appealStepFactor: '3', + maxRegularAppealRounds: '2', + appealCollateralFactor: '30000', + appealConfirmCollateralFactor: '20000', + minActiveBalance: bigExp('100'), + penaltyPct: '1000', + modules: [], + subscriptions: { + currentPeriod: '0', + feeAmount: bigExp('10'), + periodDuration: '600', + periods: [], + }, +} + +export default courtConfig diff --git a/src/mock/data/Disputes.js b/src/mock/data/Disputes.js new file mode 100644 index 00000000..f04fe46a --- /dev/null +++ b/src/mock/data/Disputes.js @@ -0,0 +1,124 @@ +import courtConfig from './CourtConfig' +import ROUNDS from './Rounds' +import { hash256 } from '../../lib/web3-utils' +import { dayjs } from '../../utils/date-utils' +import { DisputeState } from '../types' +import { accounts } from '../helper' + +const DEFAULT_SUBMITTER = accounts[5] +const DEFAULT_EVIDENCE = + '0x697066733a516d55765a53545a3958767156786b624446664a576e6644394759703376376d71353778464d563173774e34314c' +const DEFAULT_IPFS_METADATA = + 'QmPWJBAvLqdv5oNv7WvEFaghiMkWtcThDRJGFKu6kennpF/metadata.json' + +// State of each dispute +const DISPUTES_DATA = [ + { + state: DisputeState.Ruled, + description: 'Dispute finished (First round, In favor)', + rounds: [{ ...ROUNDS.ENDED.IN_FAVOR }], + }, + { + state: DisputeState.Ruled, + description: + 'Dispute finished (First round, Refused to vote, Penalties Settled)', + rounds: [{ ...ROUNDS.ENDED.REFUSED }], + }, + { + state: DisputeState.Ruled, + description: 'Dispute finished (First round, No one voted)', + rounds: [{ ...ROUNDS.ENDED.NO_VOTES }], + }, + { + state: DisputeState.Ruled, + description: 'Dispute finished (Final round, Against)', + rounds: [ + ...populatePreviousRounds(courtConfig.maxRegularAppealRounds), + { ...ROUNDS.ENDED.FINAL_ROUND }, + ], + }, + { + state: DisputeState.Adjudicating, + description: 'Dispute finished (Execute ruling)', + rounds: [{ ...ROUNDS.ENDED.IN_FAVOR }], + }, + { + state: DisputeState.Drafting, + description: 'Dispute confirm appealed', + rounds: [populatePreviousRounds(1), { ...ROUNDS.CONFIRM_APPEALED }], + }, + { + state: DisputeState.Adjudicating, + description: 'Dispute appealed', + rounds: [{ ...ROUNDS.APPEALED }], + }, + { + state: DisputeState.Adjudicating, + description: 'Dispute appealing', + rounds: [{ ...ROUNDS.APPEALING }], + }, + { + state: DisputeState.Adjudicating, + description: 'Dispute revealing', + rounds: [{ ...ROUNDS.REVEALING }], + }, + { + state: DisputeState.Adjudicating, + description: 'Dispute comitting', + rounds: [{ ...ROUNDS.COMITTING }], + }, + { + state: DisputeState.Evidence, + description: 'Dispute in evidence submission', + rounds: [{ ...ROUNDS.NOT_DRAFTED }], + }, +] + +function populatePreviousRounds(numberOfRounds) { + return Array.from({ length: numberOfRounds }).map((_, index) => ({ + ...ROUNDS.PREVIOUS, + jurorsNumber: courtConfig.appealStepFactor ** (index + 1), + })) +} + +function generateDisputes() { + const disputes = [] + + for (let i = 0; i < DISPUTES_DATA.length; i++) { + const { description, rounds, state } = DISPUTES_DATA[i] + + const disputeId = String(i) + const dispute = { + id: disputeId, + txHash: hash256(i), + createTermId: courtConfig.currentTerm, + createdAt: dayjs().unix(), + possibleRulings: 2, + state, + metadata: JSON.stringify({ + description, + metadata: DEFAULT_IPFS_METADATA, + }), + lastRoundId: rounds.length - 1, + evidences: [ + { + submitter: DEFAULT_SUBMITTER, + data: DEFAULT_EVIDENCE, + createdAt: dayjs().unix(), + }, + ], + } + + dispute.rounds = rounds.map((round, index) => ({ + ...round, + number: String(index), + dispute, + })) + + disputes.unshift(dispute) + } + + return disputes +} + +export default generateDisputes() diff --git a/src/mock/data/Jurors.js b/src/mock/data/Jurors.js new file mode 100644 index 00000000..0ad824e5 --- /dev/null +++ b/src/mock/data/Jurors.js @@ -0,0 +1,50 @@ +import { accounts } from '../helper' +import courtConfig from './CourtConfig' +import { ANJMovementType } from '../types' +import { bigNum } from '../../lib/math-utils' + +const MIN_ACTIVE_BASE_MULTIPLIER = '10' +const numberOfJurors = 3 + +const JUROR_DEFAULT_DATA = { + // Wallet balance (from the subgraph we actually don't get this amount from the juror entity itself + // but in this case since we are mocking data doesn't really matter) + walletBalance: '0', + activeBalance: '0', + lockedBalance: '0', + availableBalance: '0', + deactivationBalance: '0', + treasuryTokens: [], +} + +function generateJurors() { + return accounts.slice(0, numberOfJurors).map((account, index) => { + const activeBalance = bigNum(courtConfig.minActiveBalance).mul( + MIN_ACTIVE_BASE_MULTIPLIER * (index + 1) + ) + + return { + ...JUROR_DEFAULT_DATA, + id: account, + activeBalance, + + // Mimicking ANJ activation from wallet + anjMovements: [ + { + amount: activeBalance, + effectiveTermId: 1, + type: ANJMovementType.Activation, + }, + { + amount: activeBalance, + effectiveTermId: 1, + type: ANJMovementType.Stake, + }, + ], + + drafts: [], + } + }) +} + +export default generateJurors() diff --git a/src/mock/data/Modules.js b/src/mock/data/Modules.js new file mode 100644 index 00000000..4c122de7 --- /dev/null +++ b/src/mock/data/Modules.js @@ -0,0 +1,6 @@ +import { bigExp } from '../helper' + +export const JurorRegistry = { + totalStaked: bigExp('0'), + totalActive: bigExp('0'), +} diff --git a/src/mock/data/Rounds.js b/src/mock/data/Rounds.js new file mode 100644 index 00000000..40815e06 --- /dev/null +++ b/src/mock/data/Rounds.js @@ -0,0 +1,131 @@ +import courtConfig from './CourtConfig' +import { AdjudicationState, RulingOptions } from '../types' + +const DEFAULT_ROUND_DATA = { + id: '0', + coherentJurors: 0, + collectedTokens: '0', + createdAt: 0, + delayedTerms: 0, + jurorFees: '0', + settledPenalties: false, +} + +const ROUNDS = { + // Mock round to use for multi-round disputes + PREVIOUS: { + state: AdjudicationState.Ended, + draftTermId: '50', // TODO: Find better calculation + + // Create mock votes and appeals for previous rounds + voteData: { + winningOutcome: RulingOptions.Against, + }, + + appealData: { + appealedRuling: RulingOptions.InFavor, + opposedRuling: RulingOptions.Against, + }, + ...DEFAULT_ROUND_DATA, + }, + + // Round not drafted + NOT_DRAFTED: { + state: AdjudicationState.Invalid, + jurorsNumber: 0, + ...DEFAULT_ROUND_DATA, + }, + + // Round in comitting phase + COMITTING: { + state: AdjudicationState.Committing, + jurorsNumber: courtConfig.firstRoundJurorsNumber, + ...DEFAULT_ROUND_DATA, + }, + + // Round in revealing phase + REVEALING: { + state: AdjudicationState.Revealing, + jurorsNumber: courtConfig.firstRoundJurorsNumber, + voteData: { + onlyCommit: true, + }, + ...DEFAULT_ROUND_DATA, + }, + + // Round in appealing phase + APPEALING: { + state: AdjudicationState.Appealing, + jurorsNumber: courtConfig.firstRoundJurorsNumber, + voteData: { + winningOutcome: RulingOptions.Against, + minority: RulingOptions.InFavor, + }, + ...DEFAULT_ROUND_DATA, + }, + + // Round has been appealed (in confirm appeal phase) + APPEALED: { + state: AdjudicationState.ConfirmingAppeal, + jurorsNumber: courtConfig.firstRoundJurorsNumber, + voteData: { + winningOutcome: RulingOptions.Against, + minority: RulingOptions.InFavor, + }, + appealData: { + appealedRuling: RulingOptions.InFavor, + }, + ...DEFAULT_ROUND_DATA, + }, + + // Round has been confirm appealed + CONFIRM_APPEALED: { + state: AdjudicationState.Invalid, + jurorsNumber: + courtConfig.firstRoundJurorsNumber * courtConfig.appealStepFactor, + ...DEFAULT_ROUND_DATA, + }, + + ENDED: { + // Round ended In favor + IN_FAVOR: { + state: AdjudicationState.Ended, + jurorsNumber: courtConfig.firstRoundJurorsNumber, + voteData: { + winningOutcome: RulingOptions.InFavor, + minority: RulingOptions.Against, + }, + ...DEFAULT_ROUND_DATA, + }, + + // Round ended and Refused + REFUSED: { + state: AdjudicationState.Ended, + jurorsNumber: courtConfig.firstRoundJurorsNumber, + voteData: { + winningOutcome: RulingOptions.Refused, + minority: RulingOptions.InFavor, + }, + settlePenalties: true, + ...DEFAULT_ROUND_DATA, + }, + + // Round ended no votes + NO_VOTES: { + state: AdjudicationState.Ended, + jurorsNumber: courtConfig.firstRoundJurorsNumber, + ...DEFAULT_ROUND_DATA, + }, + + FINAL_ROUND: { + state: AdjudicationState.Ended, + voteData: { + winningOutcome: RulingOptions.Against, + minority: RulingOptions.InFavor, + }, + ...DEFAULT_ROUND_DATA, + }, + }, +} + +export default ROUNDS diff --git a/src/mock/data/index.js b/src/mock/data/index.js new file mode 100644 index 00000000..bdaf5832 --- /dev/null +++ b/src/mock/data/index.js @@ -0,0 +1,166 @@ +import Court from '../models/Court' +import { ANJMovementType } from '../types' +import { + removeAppealCircularReferences, + removeJurorCircularReferences, + removeRoundCircularReferences, +} from '../helper' + +const court = new Court() + +// Each key of this Object is the name of the respective query that we have declared (see all query names at ./queries folder) +// And the value is the function that resolves said query (by resolving we mean returns the respective mocked data) +export default { + /** **** COURT CONFIG *****/ + + CourtConfig: () => ({ + courtConfig: court.config, + }), + + /** **** DASHBOARD STATE *****/ + + // Get Court JurorRegistry module + JurorsRegistryModule: () => ({ + jurorsRegistryModule: court.jurorsRegistryModule, + }), + + FeeMovements: () => ({ feeMovements: [] }), + + JurorFeesClaimed: ({ owner }) => ({ feeMovements: [] }), + + ActiveJurors: () => ({ + jurors: court.jurors.map(juror => ({ id: juror.id })), + }), + // Get first activation movements for juror with id `id` + JurorFirstANJActivationMovement: ({ id }) => { + const { anjMovements } = court.getJuror(id) || {} + const firstActivationMovement = anjMovements?.find( + movement => movement.type === ANJMovementType.Activation + ) + + return { + juror: { + anjMovements: firstActivationMovement ? [firstActivationMovement] : [], + }, + } + }, + + // Get active, inactive, locked and deactivation balances along with movements for juror with id `id` + JurorANJBalances: ({ id }) => { + const juror = court.getJuror(id) + const { + activeBalance, + lockedBalance, + availableBalance, + deactivationBalance, + treasuryTokens, + anjMovements, + } = juror || {} + + return { + juror: juror + ? { + activeBalance, + lockedBalance, + availableBalance, + deactivationBalance, + treasuryTokens, + anjMovements, + } + : null, + } + }, + + // Get wallet balance for juror with id `id` + JurorANJWalletBalance: ({ id }) => { + const { walletBalance } = court.getJuror(id) || {} + return { anjbalance: walletBalance } + }, + + JurorTreasuryBalances: ({ owner }) => [], + + AppealsByMaker: ({ maker }) => { + const appeals = court.getAppealsByMaker(maker) + + return { + appeals: appeals.map(({ taker, ...appeal }) => + removeAppealCircularReferences(appeal) + ), + } + }, + + AppealsByTaker: ({ taker }) => { + const appeals = court.getAppealsByTaker(taker) + + return { + appeals: appeals.map(({ maker, ...appeal }) => + removeAppealCircularReferences(appeal) + ), + } + }, + + JurorDraftsFrom: ({ id, from }) => { + const juror = removeJurorCircularReferences(court.getJuror(id)) + const { drafts = [] } = juror || {} + + return { + juror: { + id, + drafts: drafts.filter(draft => draft.createdAt >= from), + }, + } + }, + + JurorDraftsNotRewarded: ({ id }) => { + const juror = removeJurorCircularReferences(court.getJuror(id)) + const { drafts = [] } = juror || {} + + return { + juror: { + id, + drafts: drafts.filter(draft => !draft.rewarded), + }, + } + }, + + /** **** DISPUTES *****/ + + // Get all disputes + AllDisputes: () => ({ + disputes: court.disputes.map(dispute => ({ + ...dispute, + rounds: dispute.rounds.map(removeRoundCircularReferences), + })), + }), + + // Get dispute with id `id` + SingleDispute: ({ id }) => { + const dispute = court.getDispute(id) + return { + dispute: { + ...dispute, + rounds: dispute.rounds.map(removeRoundCircularReferences), + }, + } + }, + + // Get all juror drafts for juror with id `id` + JurorDrafts: ({ id }) => { + const juror = removeJurorCircularReferences(court.getJuror(id)) + const { drafts = [] } = juror || {} + + return { + juror: { + drafts, + }, + } + }, + + // Get all open tasks + OpenTasks: ({ state }) => { + const rounds = court.getRoundsByState(state) + return { + adjudicationRounds: rounds.map(removeRoundCircularReferences), + } + }, +} diff --git a/src/mock/exchanges/index.js b/src/mock/exchanges/index.js new file mode 100644 index 00000000..6f14b926 --- /dev/null +++ b/src/mock/exchanges/index.js @@ -0,0 +1,67 @@ +import { makeResult } from 'urql' +import { filter, make, merge, mergeMap, pipe, share } from 'wonka' +import mockData from '../data' + +const OPERATION_DEFINITION = 'OperationDefinition' + +// Fetch exchange +export const mockFetchExchange = ({ forward }) => { + return handleOperation('query', forward) +} + +// Subscription exchange +export const mockSubscriptionExchange = ({ forward }) => { + return handleOperation('subscription', forward) +} + +function handleOperation(operationType, forward) { + const isOperationType = operation => { + const { operationName } = operation + return operationName === operationType + } + + return ops$ => { + const sharedOps$ = share(ops$) + const results$ = pipe( + sharedOps$, + filter(isOperationType), + mergeMap(convertMockedData) + ) + + const forward$ = pipe( + sharedOps$, + filter(op => !isOperationType(op)), + forward + ) + + return merge([results$, forward$]) + } +} + +const convertMockedData = operation => { + return make(({ next, complete }) => { + const { name: queryName } = operation.query.definitions.find( + node => node.kind === OPERATION_DEFINITION && node.name + ) + + // Get the desired mocked data + const convertedData = mockData[queryName.value](operation.variables) + + Promise.resolve() + .then(() => + makeResult( + operation, + { + data: convertedData, + }, + null + ) + ) + .then(result => { + next(result) + complete() + }) + + return () => {} + }) +} diff --git a/src/mock/helper.js b/src/mock/helper.js new file mode 100644 index 00000000..4f91d474 --- /dev/null +++ b/src/mock/helper.js @@ -0,0 +1,131 @@ +import { AdjudicationState } from './types' +import { FINAL_ROUND_WEIGHT_PRECISION, PCT_BASE } from '../utils/dispute-utils' + +export const bigExp = (x, y = 18) => `${x}${''.padEnd(y, '0')}` + +export const accounts = [ + '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + '0xffcf8fdee72ac11b5c542428b35eef5769c409f0', + '0x22d491bde2303f2f43325b2108d26f1eaba1e32b', + '0xe11ba2b4d45eaed5996cd0823791e0c93114882d', + '0xd03ea8624c8c5987235048901fb614fdca89b117', + '0x95cED938F7991cd0dFcb48F0a06a40FA1aF46EBC', +] + +export const getRandomNumber = (min, max) => + Math.floor(Math.random() * (max - min + 1) + min) + +export const getDraftTermId = (state, courtConfig) => { + const { + currentTerm, + commitTerms, + revealTerms, + appealTerms, + appealConfirmationTerms, + } = courtConfig + + // When round has not yet started + if (state === AdjudicationState.Invalid) { + return String(currentTerm + 3) + } + + if (state === AdjudicationState.Committing) { + return currentTerm + } + + if (state === AdjudicationState.Revealing) { + return String(currentTerm - commitTerms) + } + + if (state === AdjudicationState.Appealing) { + return String(currentTerm - commitTerms - revealTerms) + } + + if (state === AdjudicationState.ConfirmingAppeal) { + return String(currentTerm - commitTerms - revealTerms - appealTerms) + } + + return String( + currentTerm - + commitTerms - + revealTerms - + appealTerms - + appealConfirmationTerms + ) +} +export const getTermStartTime = (termId, courtConfig) => { + const { termDuration, terms } = courtConfig + const secondsFromFirstTerm = termId * termDuration + + const firstTermStartTime = terms[0].startTime + return firstTermStartTime + secondsFromFirstTerm +} + +export const getMinActiveBalanceMultiple = ( + activeBalance, + minActiveBalance +) => { + if (activeBalance.lt(minActiveBalance)) { + return '0' + } + + return FINAL_ROUND_WEIGHT_PRECISION.mul(activeBalance).div(minActiveBalance) +} + +export const pct = (self, pct) => self.mul(pct).div(PCT_BASE) + +export const removeAppealCircularReferences = appeal => { + return { + ...appeal, + round: { + ...removeRoundCircularReferences(appeal.round), + dispute: { + ...appeal.round.dispute, + rounds: appeal.round.dispute.rounds.map(({ number, jurorsNumber }) => ({ + number, + jurorsNumber, + })), + }, + }, + } +} + +export const removeJurorCircularReferences = juror => { + if (!juror) { + return + } + + return { + ...juror, + drafts: juror.drafts.map(jurorDraft => ({ + ...jurorDraft, + round: removeRoundCircularReferences(jurorDraft.round), + juror: { id: jurorDraft.id }, + })), + } +} + +export const removeRoundCircularReferences = round => { + const { jurors = [], dispute } = round || {} + + return { + ...round, + appeal: round.appeal + ? { + ...round.appeal, + round: { + id: round.id, + }, + } + : null, + dispute: { + ...dispute, + rounds: dispute.rounds.map(round => ({ id: round.id })), + }, + jurors: jurors.map(jurorDraft => ({ + ...jurorDraft, + juror: { id: jurorDraft.juror.id }, + round: { id: jurorDraft.round.id }, + })), + } +} diff --git a/src/mock/models/Court.js b/src/mock/models/Court.js new file mode 100644 index 00000000..d04e3803 --- /dev/null +++ b/src/mock/models/Court.js @@ -0,0 +1,405 @@ +import Jurors from '../data/Jurors' +import Disputes from '../data/Disputes' +import courtConfig from '../data/CourtConfig' +import { JurorRegistry } from '../data/Modules' + +import { bigNum } from '../../lib/math-utils' +import { dayjs } from '../../utils/date-utils' +import { addressesEqual } from '../../lib/web3-utils' +import { + accounts, + bigExp, + getDraftTermId, + getMinActiveBalanceMultiple, + getRandomNumber, + getTermStartTime, + pct, +} from '../helper' +import { + ANJMovementType, + AdjudicationState, + DisputeState, + RulingOptions, + getAdjudicationStateNumber, + getRulingOptionNumber, +} from '../types' + +const DEFAULT_APPEAL_MAKER = accounts[3] +const DEFAULT_APPEAL_TAKER = accounts[4] +const DEFAULT_APPEAL_DEPOSIT = bigExp('185') +const DEFAULT_CONFIRM_APPEAL_DEPOSIT = bigExp('225') + +export default class Court { + constructor() { + this.config = courtConfig + this.jurorsRegistryModule = JurorRegistry + + this.disputes = Disputes + this.jurors = Jurors + this.appeals = [] + this.adjudicationRounds = [] + this.updateTotalActiveBalance() + + this.draftJurors() + this.createVotes() + this.createAppeals() + this.settlePenalties() + } + + getDispute(id) { + return this.disputes.find(dispute => dispute.id === id) + } + + getJuror(id) { + return this.jurors.find(juror => addressesEqual(juror.id, id)) + } + + getAppealsByMaker(maker) { + return this.appeals.filter(appeal => addressesEqual(appeal.maker, maker)) + } + + getAppealsByTaker(taker) { + return this.appeals.filter(appeal => addressesEqual(appeal.taker, taker)) + } + + getRoundsByState(state) { + return this.adjudicationRounds.filter(round => { + const adjudicationState = getAdjudicationStateNumber(round.state) + return state[0] <= adjudicationState && adjudicationState <= state[1] + }) + } + + // Randomly draft the 3 possible jurors. + draftJurors() { + for (let i = 0; i < this.disputes.length; i++) { + const dispute = this.disputes[i] + + // Draft jurors for each round + for (let roundId = 0; roundId < dispute.rounds.length; roundId++) { + const round = dispute.rounds[roundId] + + round.draftTermId = getDraftTermId(round.state, this.config) + round.jurors = [] + + this.adjudicationRounds.push(round) + + if (round.state === AdjudicationState.Invalid) { + continue + } + + const maxRegularAppealRoundsReached = + this.config.maxRegularAppealRounds <= roundId + + // If we reached the last possible round, juror's weight is relative to the number of times the min active balance the juror has + if (maxRegularAppealRoundsReached) { + // final round + + round.jurorsNumber = getMinActiveBalanceMultiple( + bigNum(this.jurorsRegistryModule.totalActive), + bigNum(this.config.minActiveBalance) + ) + } else { + // normal round + let selectedJurors = 0 + const jurorsNumber = round.jurorsNumber + while (selectedJurors < jurorsNumber) { + // Select a juror + const selectedJurorIndex = getRandomNumber( + 0, + this.jurors.length - 1 + ) + const selectedJuror = this.jurors[selectedJurorIndex] + + const draftLockAmount = pct( + bigNum(this.config.minActiveBalance), + this.config.penaltyPct + ) + + const jurorDraft = round.jurors.find(jurorDraft => + addressesEqual(jurorDraft.juror.id, selectedJuror.id) + ) + + if (!jurorDraft) { + this.createDraft(selectedJuror, round, 1, draftLockAmount) + } else { + jurorDraft.weight += 1 + + this.lockAnjAmount(selectedJuror, draftLockAmount) + } + + selectedJurors += 1 + } + + round.jurorFees = bigNum(this.config.jurorFee).mul(selectedJurors) + round.selectedJurors = selectedJurors + } + } + } + } + + createDraft(juror, round, weight, draftLockAmount) { + const jurorDraft = { + id: '0x', + round, + juror, + weight, + createdAt: getTermStartTime(round.draftTermId, this.config), + } + + this.lockAnjAmount(juror, draftLockAmount) + + // Associate respective entities. + juror.drafts.push(jurorDraft) + round.jurors.push(jurorDraft) + } + + lockAnjAmount(juror, amount) { + if (amount.gt(0)) { + // Create lock anjMovement + juror.anjMovements = [ + { + amount: amount.toString(), + effectiveTermId: null, + createdAt: dayjs().unix(), + type: ANJMovementType.Lock, + }, + ...juror.anjMovements, + ] + + // Update juror locked balance + juror.lockedBalance = bigNum(juror.lockedBalance) + .add(amount) + .toString() + } + } + + // Create votes for each round flagged with `voteData` + createVotes() { + for (let i = 0; i < this.disputes.length; i++) { + const dispute = this.disputes[i] + + if (dispute.state === DisputeState.Ruled) { + dispute.finalRuling = getRulingOptionNumber(RulingOptions.Refused) + } + + // Create votes for each round + for (let roundId = 0; roundId < dispute.rounds.length; roundId++) { + const round = dispute.rounds[roundId] + + // voteData tells whether we should create votes for the dispute in question + if (!round.voteData) { + continue + } + + const maxRegularAppealRoundsReached = + this.config.maxRegularAppealRounds <= roundId + + // If we are at the final round, we preslash jurors + if (maxRegularAppealRoundsReached) { + const selectedJurors = this.jurors.filter(juror => + bigNum(juror.activeBalance).gt(this.config.minActiveBalance) + ) + + selectedJurors.forEach(juror => { + const jurorWeight = getMinActiveBalanceMultiple( + bigNum(juror.activeBalance), + bigNum(this.config.minActiveBalance) + ) + + this.createDraft(juror, round, jurorWeight, bigNum(0)) + + const weightedPenalty = pct( + bigNum(juror.activeBalance), + bigNum(this.config.penaltyPct) + ) + + this.collectTokens(juror, round, weightedPenalty, false) + }) + } + + const { voteData } = round + + let processedWeight = 0 + const jurorsNumber = round.jurorsNumber + for (let j = 0; j < round.jurors.length; j++) { + const draft = round.jurors[j] + draft.commitment = '0x' + draft.commitmentDate = dayjs().unix() + + if (voteData.onlyCommit) { + continue + } + + const mayorityEnsured = processedWeight > jurorsNumber / 2 + + // If possible we'll try to ditribute the total votes + // `winningOutcome` has the ruling option that should have the mayority of votes + // `minority` has the ruling option that should have the minority + // TODO: Add cases to distribute votes among the 3 ruling options + draft.revealDate = dayjs().unix() + draft.outcome = getRulingOptionNumber( + voteData[ + mayorityEnsured && voteData.minority + ? 'minority' + : 'winningOutcome' + ] + ) + + processedWeight += draft.weight + } + + round.vote = { + winningOutcome: voteData.winningOutcome, + } + + if (round.state === AdjudicationState.Ended) { + dispute.finalRuling = getRulingOptionNumber(voteData.winningOutcome) + } + } + } + } + + // Create appeals for each round flagged with `appealData` + createAppeals() { + for (let i = 0; i < this.disputes.length; i++) { + const dispute = this.disputes[i] + + for (let roundId = 0; roundId < dispute.rounds.length; roundId++) { + const round = dispute.rounds[roundId] + + // `appealData` tells whether we should create appeals for the dispute in question + if (!round.appealData) { + continue + } + + const { appealData } = round + + const appeal = { + round, + appealedRuling: getRulingOptionNumber(appealData.appealedRuling), + maker: DEFAULT_APPEAL_MAKER, + appealDeposit: DEFAULT_APPEAL_DEPOSIT, + createdAt: dayjs().unix(), + + ...(appealData.opposedRuling + ? { + opposedRuling: getRulingOptionNumber(appealData.opposedRuling), + taker: DEFAULT_APPEAL_TAKER, + confirmAppealDeposit: DEFAULT_CONFIRM_APPEAL_DEPOSIT, + confirmedAt: dayjs().unix(), + } + : { + confirmAppealDeposit: '0', + }), + } + + // Save appeal round reference + round.appeal = appeal + + // Save appeal to collection + this.appeals.push(appeal) + } + } + } + + // Settle penalties for each round that is flagged with `settlePenalties` prop + settlePenalties() { + for (let i = 0; i < this.disputes.length; i++) { + const dispute = this.disputes[i] + + for (let roundId = 0; roundId < dispute.rounds.length; roundId++) { + const round = dispute.rounds[roundId] + + if (!round.settlePenalties) { + continue + } + + const maxRegularAppealRoundsReached = + this.config.maxRegularAppealRounds <= roundId + + if (!maxRegularAppealRoundsReached) { + for (let i = 0; i < round.jurors.length; i++) { + const jurorDraft = round.jurors[i] + + const isCoherentJuror = jurorDraft.outcome === dispute.finalRuling + + const slashOrUnlockAmount = pct( + bigNum(this.config.minActiveBalance), + this.config.penaltyPct + ) + const draftLockAmountTotal = slashOrUnlockAmount.mul( + jurorDraft.weight + ) + + if (isCoherentJuror) { + // unlock juror ANJ if juror is coherent + round.coherentJurors += jurorDraft.weight + this.unlockJurorANJ(jurorDraft.juror, draftLockAmountTotal) + } else { + this.collectTokens( + jurorDraft.juror, + round, + draftLockAmountTotal, + true + ) + } + } + } + + round.settledPenalties = true + } + } + } + + slashJuror(juror, amount) { + juror.activeBalance = bigNum(juror.activeBalance).sub(amount) + + this.jurorsRegistryModule.totalActive = bigNum( + this.jurorsRegistryModule.totalActive + ).sub(amount) + + juror.anjMovements.unshift({ + amount: amount.toString(), + effectiveTermId: null, + createdAt: dayjs().unix(), + type: ANJMovementType.Slash, + }) + } + + unlockJurorANJ(juror, amount) { + this.updateJurorLockedBalance(juror, amount) + + juror.anjMovements.unshift({ + amount: amount.toString(), + effectiveTermId: null, + createdAt: dayjs().unix(), + type: ANJMovementType.Unlock, + }) + } + + collectTokens(juror, round, amount, unlockBalance) { + // collect tokens if juror didn't vote for the winning outcome + round.collectedTokens = bigNum(round.collectedTokens) + .add(amount) + .toString() + + this.slashJuror(juror, amount) + + if (unlockBalance) { + this.updateJurorLockedBalance(juror, amount) + } + } + + updateJurorLockedBalance(juror, amount) { + juror.lockedBalance = bigNum(juror.lockedBalance).sub(amount) + } + + updateTotalActiveBalance() { + this.jurorsRegistryModule.totalActive = String( + this.jurors.reduce( + (acc, juror) => acc.add(juror.activeBalance), + bigNum(0) + ) + ) + } +} diff --git a/src/mock/types.js b/src/mock/types.js new file mode 100644 index 00000000..de044c79 --- /dev/null +++ b/src/mock/types.js @@ -0,0 +1,46 @@ +export const ANJMovementType = { + Stake: 'Stake', + Unstake: 'Unstake', + Activation: 'Activation', + Deactivation: 'Deactivation', + Lock: 'Lock', + Unlock: 'Unlock', + Reward: 'Reward', + Slash: 'Slash', +} + +export const DisputeState = { + Evidence: 'Evidence', + Drafting: 'Drafting', + Adjudicating: 'Adjudicating', + Ruled: 'Ruled', +} + +export const AdjudicationState = { + Invalid: 'Invalid', + Committing: 'Committing', + Revealing: 'Revealing', + Appealing: 'Appealing', + ConfirmingAppeal: 'ConfirmingAppeal', + Ended: 'Ended', +} + +export const getAdjudicationStateNumber = state => { + return Object.values(AdjudicationState).findIndex( + adjudicationState => adjudicationState === state + ) +} + +export const RulingOptions = { + Missing: 'Missing', + Leaked: 'Leaked', + Refused: 'Refused', + Against: 'Against', + InFavor: 'InFavor', +} + +export const getRulingOptionNumber = option => { + return Object.values(RulingOptions).findIndex( + rulingOption => rulingOption === option + ) +} diff --git a/src/queries/jurorDrafts.js b/src/queries/jurorDrafts.js index 851c2b95..07a0fae8 100644 --- a/src/queries/jurorDrafts.js +++ b/src/queries/jurorDrafts.js @@ -26,18 +26,6 @@ export const JurorDraftsNotRewarded = gql` } ` -// First juror draft already rewarded -export const JurorDraftsRewarded = gql` - query JurorDraftsRewarded($id: ID!) { - juror(id: $id) { - id - drafts(where: { rewarded: true }, first: 1) { - id - } - } - } -` - // Jurors drafts for juror with id `$id` created since `$from` export const JurorDraftsFrom = gql` subscription JurorDraftsFrom($id: ID!, $from: BigInt!) { diff --git a/src/utils/juror-draft-utils.js b/src/utils/juror-draft-utils.js index 6cb9c850..fa8965d2 100644 --- a/src/utils/juror-draft-utils.js +++ b/src/utils/juror-draft-utils.js @@ -29,7 +29,7 @@ export function isJurorCoherent(jurorDraft) { } export function transformJurorDataAttributes(jurorDraft) { - const { weight, round } = jurorDraft + const { round, weight } = jurorDraft return { ...jurorDraft,