From b5a5f6dd99ba04a57b7062e8c6704457bf73c36f Mon Sep 17 00:00:00 2001 From: yuetloo Date: Mon, 22 Jan 2024 13:39:21 -0500 Subject: [PATCH] fix bigint issues and maci v1 interface change --- contracts/tasks/exportRound.ts | 156 ++++++++++++------ contracts/tasks/index.ts | 1 + contracts/tasks/mergeAllocations.ts | 21 ++- contracts/utils/JSONFile.ts | 23 +++ .../utils/RecipientRegistryLogProcessor.ts | 31 ++-- contracts/utils/file.ts | 12 -- .../utils/parsers/RequestSubmittedParser.ts | 4 +- .../utils/providers/EtherscanProvider.ts | 1 + contracts/utils/types.ts | 7 + vue-app/src/api/projects.ts | 4 +- vue-app/src/views/Leaderboard.vue | 23 ++- 11 files changed, 187 insertions(+), 96 deletions(-) delete mode 100644 contracts/utils/file.ts diff --git a/contracts/tasks/exportRound.ts b/contracts/tasks/exportRound.ts index a0feb4686..7f3f30337 100644 --- a/contracts/tasks/exportRound.ts +++ b/contracts/tasks/exportRound.ts @@ -3,18 +3,20 @@ * * Sample usage: * - * yarn hardhat export-round --round-address
--out-dir ../vue-app/src/rounds --operator --ipfs --start-block --network + * yarn hardhat export-round --round-address
--output-dir ../vue-app/src/rounds \ + * --operator --ipfs \ + * --start-block --network * * To generate the leaderboard view, deploy the clrfund website with the generated round data in the vue-app/src/rounds folder */ import { task, types } from 'hardhat/config' -import { Contract, formatUnits } from 'ethers' +import { Contract, formatUnits, getNumber } from 'ethers' import { Ipfs } from '../utils/ipfs' import { Project, Round, RoundFileContent } from '../utils/types' import { RecipientRegistryLogProcessor } from '../utils/RecipientRegistryLogProcessor' import { getRecipientAddressAbi } from '../utils/abi' -import { writeToFile } from '../utils/file' +import { JSONFile } from '../utils/JSONFile' import path from 'path' import fs from 'fs' @@ -74,7 +76,8 @@ async function updateRoundList(filePath: string, round: RoundListEntry) { // sort in ascending start time order rounds.sort((round1, round2) => round1.startTime - round2.startTime) - writeToFile(filePath, rounds) + JSONFile.write(filePath, rounds) + console.log('Finished writing to', filePath) } async function mergeRecipientTally({ @@ -126,13 +129,14 @@ async function mergeRecipientTally({ startTime, endTime ) - } catch { + } catch (err) { + console.log('err', err) // some older recipient registry contract does not have // the getRecipientAddress function, ignore error } const tallyResult = tally.results.tally[i] - const spentVoiceCredits = tally.totalVoiceCreditsPerVoteOption.tally[i] + const spentVoiceCredits = tally.perVOSpentVoiceCredits.tally[i] const formattedDonationAmount = formatUnits( BigInt(spentVoiceCredits) * BigInt(voiceCreditFactor), nativeTokenDecimals @@ -162,10 +166,14 @@ async function getRoundInfo( ethers: any, operator: string ): Promise { - console.log('Fetching round data for', roundContract.address) - const round: any = { address: roundContract.address } + console.log('Fetching round data for', roundContract.target) + const address = await roundContract.getAddress() + + let nativeTokenAddress: string + let nativeTokenDecimals = BigInt(0) + let nativeTokenSymbol = '' try { - round.nativeTokenAddress = await roundContract.nativeToken() + nativeTokenAddress = await roundContract.nativeToken() } catch (err) { const errorMessage = `Failed to get nativeToken. Make sure the environment variable JSONRPC_HTTP_URL is set properly: ${ (err as Error).message @@ -174,14 +182,14 @@ async function getRoundInfo( } try { - const token = await ethers.getContractAt('ERC20', round.nativeTokenAddress) - round.nativeTokenDecimals = await token.decimals().catch(toUndefined) - round.nativeTokenSymbol = await token.symbol().catch(toUndefined) + const token = await ethers.getContractAt('ERC20', nativeTokenAddress) + nativeTokenDecimals = await token.decimals().catch(toUndefined) + nativeTokenSymbol = await token.symbol().catch(toUndefined) console.log( 'Fetched token data', - round.nativeTokenAddress, - round.nativeTokenSymbol, - round.nativeTokenDecimals + nativeTokenAddress, + nativeTokenSymbol, + nativeTokenDecimals ) } catch (err) { const errorMessage = err instanceof Error ? err.message : '' @@ -189,67 +197,114 @@ async function getRoundInfo( } const contributorCount = await roundContract.contributorCount().catch(toZero) - round.contributorCount = contributorCount.toNumber() - const matchingPoolSize = await roundContract.matchingPoolSize().catch(toZero) - round.matchingPoolSize = matchingPoolSize.toString() - round.totalSpent = await roundContract + const totalSpent = await roundContract .totalSpent() .then(toString) .catch(toUndefined) const voiceCreditFactor = await roundContract.voiceCreditFactor() - round.voiceCreditFactor = voiceCreditFactor.toString() - - round.isFinalized = await roundContract.isFinalized() - round.isCancelled = await roundContract.isCancelled() - round.tallyHash = await roundContract.tallyHash() + const isFinalized = await roundContract.isFinalized() + const isCancelled = await roundContract.isCancelled() + const tallyHash = await roundContract.tallyHash() + const maciAddress = await roundContract.maci().catch(toUndefined) + const pollAddress = await roundContract.poll().catch(toUndefined) + let startTime = 0 + let endTime = 0 + let pollId: bigint | undefined + let messages: bigint + let maxMessages: bigint + let maxRecipients: bigint + let signUpDuration = BigInt(0) + let votingDuration = BigInt(0) try { - round.maciAddress = await roundContract.maci().catch(toUndefined) - const maci = await ethers.getContractAt('MACI', round.maciAddress) - const startTime = await maci.signUpTimestamp().catch(toZero) - round.startTime = startTime.toNumber() - const signUpDuration = await maci.signUpDurationSeconds().catch(toZero) - const votingDuration = await maci.votingDurationSeconds().catch(toZero) - const endTime = startTime.add(signUpDuration).add(votingDuration) - round.endTime = endTime.toNumber() - round.signUpDuration = signUpDuration.toNumber() - round.votingDuration = votingDuration.toNumber() - - const maciTreeDepths = await maci.treeDepths() - const messages = await maci.numMessages() - - round.messages = messages.toNumber() - round.maxMessages = 2 ** maciTreeDepths.messageTreeDepth - 1 - round.maxRecipients = 5 ** maciTreeDepths.voteOptionTreeDepth - 1 + if (pollAddress) { + const pollContract = await ethers.getContractAt('Poll', pollAddress) + const [roundStartTime, roundDuration] = + await pollContract.getDeployTimeAndDuration() + startTime = getNumber(roundStartTime) + signUpDuration = roundDuration + votingDuration = roundDuration + endTime = startTime + getNumber(roundDuration) + + pollId = await roundContract.pollId() + + messages = await pollContract.numMessages() + const maxValues = await pollContract.maxValues() + maxMessages = maxValues.maxMessages + maxRecipients = maxValues.maxVoteOptions + } else { + const maci = await ethers.getContractAt('MACI', maciAddress) + startTime = await maci.signUpTimestamp().catch(toZero) + signUpDuration = await maci.signUpDurationSeconds().catch(toZero) + votingDuration = await maci.votingDurationSeconds().catch(toZero) + endTime = + getNumber(startTime) + + getNumber(signUpDuration) + + getNumber(votingDuration) + + const treeDepths = await maci.treeDepths() + messages = await maci.numMessages() + maxMessages = BigInt(2) ** BigInt(treeDepths.messageTreeDepth) - BigInt(1) + maxRecipients = + BigInt(5) ** BigInt(treeDepths.voteOptionTreeDepth) - BigInt(1) + } } catch (err) { const errorMessage = err instanceof Error ? err.message : '' - throw new Error(`Failed to get MACI data ${errorMessage}`) + throw new Error(`Failed to get round duration ${errorMessage}`) } - round.userRegistryAddress = await roundContract + const userRegistryAddress = await roundContract .userRegistry() .catch(toUndefined) - round.recipientRegistryAddress = await roundContract.recipientRegistry() + const recipientRegistryAddress = await roundContract.recipientRegistry() + let recipientDepositAmount = '0' try { const recipientRegistry = await ethers.getContractAt( 'OptimisticRecipientRegistry', - round.recipientRegistryAddress + recipientRegistryAddress ) - round.recipientDepositAmount = await recipientRegistry + recipientDepositAmount = await recipientRegistry .baseDeposit() .then(toString) } catch { // ignore error - non optimistic recipient registry does not have deposit } - round.operator = operator const providerNetwork = await ethers.provider.getNetwork() - round.chainId = providerNetwork.chainId - + const chainId = getNumber(providerNetwork.chainId) + + const round: Round = { + chainId, + operator, + address, + userRegistryAddress, + recipientRegistryAddress, + recipientDepositAmount, + maciAddress, + pollAddress, + pollId, + contributorCount, + totalSpent: totalSpent || '', + matchingPoolSize, + voiceCreditFactor, + isFinalized, + isCancelled, + tallyHash, + nativeTokenAddress, + nativeTokenSymbol, + nativeTokenDecimals: getNumber(nativeTokenDecimals), + startTime, + endTime, + signUpDuration: getNumber(signUpDuration), + votingDuration: getNumber(votingDuration), + messages, + maxMessages, + maxRecipients, + } console.log('Round', round) return round } @@ -370,7 +425,8 @@ task('export-round', 'Export round data for the leaderboard') projects, tally, } - writeToFile(filename, roundData) + JSONFile.write(filename, roundData) + console.log('Finished writing to', filename) // update round list const listFilename = roundListFileName(outputDir) diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts index a22808bcb..8180a9282 100644 --- a/contracts/tasks/index.ts +++ b/contracts/tasks/index.ts @@ -6,3 +6,4 @@ import './verifyRecipientRegistry' import './verifyUserRegistry' import './pubkey' import './loadUsers' +import './exportRound' diff --git a/contracts/tasks/mergeAllocations.ts b/contracts/tasks/mergeAllocations.ts index d6beab4fb..100361019 100644 --- a/contracts/tasks/mergeAllocations.ts +++ b/contracts/tasks/mergeAllocations.ts @@ -8,10 +8,10 @@ */ import { task, types } from 'hardhat/config' -import { utils, BigNumber } from 'ethers' +import { formatUnits, parseUnits } from 'ethers' import fs from 'fs' import { Project, RoundFileContent, Tally } from '../utils/types' -import { writeToFile } from '../utils/file' +import { JSONFile } from '../utils/JSONFile' const COLUMN_PROJECT_NAME = 0 const COLUMN_RECIPIENT_ADDRESS = 1 @@ -153,7 +153,7 @@ task('merge-allocations', 'Merge the allocations data into the round JSON file') totalVoiceCreditsPerVoteOption: { tally: [] }, } - let totalPayout = BigNumber.from(0) + let totalPayout = BigInt(0) for (let index = 0; index < allocations.length; index++) { const { recipientAddress, projectName, payoutAmount, votes } = allocations[index] @@ -167,13 +167,13 @@ task('merge-allocations', 'Merge the allocations data into the round JSON file') continue } - const allocatedAmountBN = utils.parseUnits(payoutAmount, decimals) + const allocatedAmountBN = parseUnits(payoutAmount, decimals) const allocatedAmount = allocatedAmountBN.toString() const projectKey = makeProjectKey(recipientAddress) if (projects[projectKey]) { projects[projectKey].allocatedAmount = allocatedAmount console.log(index, projectName, '-', payoutAmount) - totalPayout = totalPayout.add(allocatedAmount) + totalPayout = totalPayout + BigInt(allocatedAmount) const { recipientIndex } = projects[projectKey] if (recipientIndex) { @@ -186,12 +186,17 @@ task('merge-allocations', 'Merge the allocations data into the round JSON file') } } - if (roundData && Object.keys(projects).length > 0 && totalPayout.gt(0)) { + if ( + roundData && + Object.keys(projects).length > 0 && + totalPayout > BigInt(0) + ) { roundData.projects = Object.values(projects) - console.log('totalPayout ', utils.formatUnits(totalPayout, decimals)) + console.log('totalPayout ', formatUnits(totalPayout, decimals)) roundData.round.matchingPoolSize = totalPayout.toString() roundData.tally = sanitizeTally(tally) - writeToFile(roundFile, roundData) + JSONFile.write(roundFile, roundData) + console.log('Finished writing to', roundFile) } }) diff --git a/contracts/utils/JSONFile.ts b/contracts/utils/JSONFile.ts index acd2817c1..384bd3d01 100644 --- a/contracts/utils/JSONFile.ts +++ b/contracts/utils/JSONFile.ts @@ -1,5 +1,18 @@ import fs from 'fs' +/** + * Used by JSON.stringify to convert bigint to string + * @param _key: key of the JSON entry to process + * @param value: value of the JSON entry to process + * @returns formated value + */ +function replacer(_key: string, value: any) { + if (typeof value === 'bigint') { + return value.toString() + } + return value +} + export class JSONFile { /** * Read the content of the JSON file @@ -24,4 +37,14 @@ export class JSONFile { const state = JSONFile.read(path) fs.writeFileSync(path, JSON.stringify({ ...state, ...data }, null, 2)) } + + /** + * Write the data to the JSON file + * @param path The path of the file + * @param data The data to write + */ + static write(path: string, data: any) { + const outputString = JSON.stringify(data, replacer, 2) + fs.writeFileSync(path, outputString + '\n') + } } diff --git a/contracts/utils/RecipientRegistryLogProcessor.ts b/contracts/utils/RecipientRegistryLogProcessor.ts index 26a17a742..615ab1cb0 100644 --- a/contracts/utils/RecipientRegistryLogProcessor.ts +++ b/contracts/utils/RecipientRegistryLogProcessor.ts @@ -1,12 +1,4 @@ -import { - Contract, - EventFilter, - Interface, - Fragment, - EventFragment, - Addressable, - ZeroAddress, -} from 'ethers' +import { Contract, EventFilter, Interface, ZeroAddress } from 'ethers' import { ProviderFactory } from './providers/ProviderFactory' import { Project } from './types' import { RecipientState } from './constants' @@ -14,13 +6,15 @@ import { ParserFactory } from './parsers/ParserFactory' import { Log } from './providers/BaseProvider' import { toDate } from './date' import { EVENT_ABIS } from './abi' +import { AbiInfo } from './types' -function getFilter(address: string | Addressable, abi: string): EventFilter { - const eventInterface = new Interface([abi]) - const events = eventInterface.fragments - .filter(Fragment.isEvent) - .map((evt) => evt as EventFragment) - const topic0 = events[0].topicHash +function getFilter(address: string, abiInfo: AbiInfo): EventFilter { + const eventInterface = new Interface([abiInfo.abi]) + const event = eventInterface.getEvent(abiInfo.name) + if (!event) { + throw new Error(`Event ${abiInfo.name} not found`) + } + const topic0 = event.topicHash return { address, topics: [topic0] } } @@ -62,9 +56,10 @@ export class RecipientRegistryLogProcessor { ? endBlock : await this.registry.runner?.provider?.getBlockNumber() + const registryAddress = await this.registry.getAddress() console.log( `Fetching event logs from the recipient registry`, - this.registry.address + registryAddress ) const logProvider = ProviderFactory.createProvider({ @@ -76,7 +71,7 @@ export class RecipientRegistryLogProcessor { for (let i = 0; i < EVENT_ABIS.length; i++) { const { add, remove } = EVENT_ABIS[i] - const filter = getFilter(this.registry.target, add.abi) + const filter = getFilter(registryAddress, add) const addLogs = await logProvider.fetchLogs({ filter, startBlock, @@ -85,7 +80,7 @@ export class RecipientRegistryLogProcessor { }) if (addLogs.length > 0) { - const filter = getFilter(this.registry.target, remove.abi) + const filter = getFilter(registryAddress, remove) const removeLogs = await logProvider.fetchLogs({ filter, startBlock, diff --git a/contracts/utils/file.ts b/contracts/utils/file.ts deleted file mode 100644 index 93c618039..000000000 --- a/contracts/utils/file.ts +++ /dev/null @@ -1,12 +0,0 @@ -import fs from 'fs' - -/** - * Write json data to the file - * @param filePath the path of the file to write to - * @param data json data - */ -export function writeToFile(filePath: string, data: any) { - const outputString = JSON.stringify(data, null, 2) - fs.writeFileSync(filePath, outputString + '\n') - console.log('Successfully written to ', filePath) -} diff --git a/contracts/utils/parsers/RequestSubmittedParser.ts b/contracts/utils/parsers/RequestSubmittedParser.ts index f431a0d73..777b38fbb 100644 --- a/contracts/utils/parsers/RequestSubmittedParser.ts +++ b/contracts/utils/parsers/RequestSubmittedParser.ts @@ -15,7 +15,9 @@ export class RequestSubmittedParser extends BaseParser { const recipientIndex = args._index const state = - args._type === 0 ? RecipientState.Registered : RecipientState.Removed + BigInt(args._type) === BigInt(0) + ? RecipientState.Registered + : RecipientState.Removed const timestamp = toDate(args._timestamp) const createdAt = diff --git a/contracts/utils/providers/EtherscanProvider.ts b/contracts/utils/providers/EtherscanProvider.ts index 384bf11ce..73a6205c0 100644 --- a/contracts/utils/providers/EtherscanProvider.ts +++ b/contracts/utils/providers/EtherscanProvider.ts @@ -5,6 +5,7 @@ const EtherscanApiUrl: Record = { xdai: 'https://api.gnosisscan.io', arbitrum: 'https://api.arbiscan.io', 'arbitrum-goerli': 'https://api-goerli.arbiscan.io', + 'arbitrum-sepolia': 'https://api-sepolia.arbiscan.io', } export class EtherscanProvider extends BaseProvider { diff --git a/contracts/utils/types.ts b/contracts/utils/types.ts index 61f6c48a5..473853ac1 100644 --- a/contracts/utils/types.ts +++ b/contracts/utils/types.ts @@ -39,6 +39,8 @@ export interface Round { recipientRegistryAddress: string recipientDepositAmount?: string maciAddress: string + pollAddress?: string + pollId?: bigint contributorCount: number totalSpent: string matchingPoolSize: string @@ -51,6 +53,11 @@ export interface Round { nativeTokenDecimals: number startTime: number endTime: number + signUpDuration: number + votingDuration: number + messages: bigint + maxMessages: bigint + maxRecipients: bigint blogUrl?: string } diff --git a/vue-app/src/api/projects.ts b/vue-app/src/api/projects.ts index ca483587e..22a2b1d46 100644 --- a/vue-app/src/api/projects.ts +++ b/vue-app/src/api/projects.ts @@ -1,4 +1,4 @@ -import { Contract, Interface } from 'ethers' +import { Contract, Interface, getNumber } from 'ethers' import type { TransactionResponse, Signer } from 'ethers' import { FundingRound, OptimisticRecipientRegistry } from './abi' import { clrFundContract, provider, recipientRegistryType, ipfsGatewayUrl } from './core' @@ -186,7 +186,7 @@ export function toLeaderboardProject(project: any): LeaderboardProject { return { id: project.id, name: project.name, - index: project.recipientIndex, + index: getNumber(project.recipientIndex), imageUrl, allocatedAmount: BigInt(project.allocatedAmount || '0'), votes: BigInt(project.tallyResult || '0'), diff --git a/vue-app/src/views/Leaderboard.vue b/vue-app/src/views/Leaderboard.vue index b98054da6..b5c08a706 100644 --- a/vue-app/src/views/Leaderboard.vue +++ b/vue-app/src/views/Leaderboard.vue @@ -83,12 +83,25 @@ onMounted(async () => { return } - projects.value = data.projects - .filter(project => project.state != 'Removed') - .map(project => toLeaderboardProject(project)) - .sort((p1: LeaderboardProject, p2: LeaderboardProject) => p2.allocatedAmount - p1.allocatedAmount) + try { + projects.value = data.projects + .filter(project => project.state != 'Removed') + .map(project => toLeaderboardProject(project)) + .sort((p1: LeaderboardProject, p2: LeaderboardProject) => { + const diff = p2.allocatedAmount - p1.allocatedAmount + if (diff === BigInt(0)) return 0 + if (diff > BigInt(0)) return 1 + return -1 + }) + } catch (err) { + console.log('Error sorting project information', err) + } - round.value = toRoundInfo(data.round, network) + try { + round.value = toRoundInfo(data.round, network) + } catch (e) { + console.log('Error converting to round info', e) + } isLoading.value = false })