From e093dbcc31d9d6c51761af69f036db9f0c35dd29 Mon Sep 17 00:00:00 2001 From: Crisgarner <@crisgarner> Date: Thu, 8 Dec 2022 14:23:37 -0600 Subject: [PATCH 01/55] updated texts --- vue-app/src/locales/en.json | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/vue-app/src/locales/en.json b/vue-app/src/locales/en.json index f28f2b3f9..05b12c9c9 100644 --- a/vue-app/src/locales/en.json +++ b/vue-app/src/locales/en.json @@ -495,7 +495,7 @@ "brightIDGuide": { "heading": { "h2": "Get verified", - "p": "BrightID verification helps prove that you’re a unique human. To get verified, you need enough people to confirm they've met you and you're a real person. There are a couple of ways to do this:" + "p": "BrightID verification helps prove that you’re a unique human. To get verified, you need to join a BrightId party where other members can verify you're \"not a bot\"." }, "accordion": { "fastest": { @@ -721,7 +721,7 @@ "title": "Every donation is amplified by the matching pool.", "paragraph": "This fundraiser rewards projects with the most unique demand, not just those with the wealthiest backers.", "subtitle": "How it works", - "list-1": "The {operator} and other donors send funds to the matching pool smart contract.", + "list-1": "{operator} and other donors send funds to the matching pool smart contract.", "list-2": "The round begins and you can donate to your favorite projects.", "list-3": "Once the round finishes, the smart contract distributes the matching pool funds to projects weighted primarily by number of contributions,", "list-3-strong": "not contribution value", @@ -743,7 +743,7 @@ "paragraph-2": "Using MACI, a zero-knowledge technology, it's impossible to prove how you contributed. This drives bribers insane because they have no idea whether you actually did what they bribed you to do!", "link-2": "About MACI", "subtitle-3": "Built using the CLR protocol", - "paragraph-3": "clr.fund is a protocol for efficiently allocating funds to public goods that benefit the Ethereum Network according to the prefences of the Ethereum Community.", + "paragraph-3": "clr.fund is a protocol for efficiently allocating funds to public goods that benefit the Ethereum Network according to the preferences of the Ethereum Community.", "link-3": "About clr.fund" }, "footer": { @@ -836,7 +836,7 @@ "p3": "Once this app is linked in your BrightID app, we'll automatically transition you to the next step. It may take a minute for us to verify the connection - please wait.", "p4": "Scan this QR code with your BrightID app", "p5": "Follow this link to connect your wallet to your BrightID app", - "p6": "This link might look scary but it just makes a connection between your connected wallet address, our app, and BrightID. Make sure your address looks correct.", + "p6": "This link might look scary but it just makes a connection between your connected wallet address, our app, and BrightID. If clicking the link does not open the BrightId app, try manually copying the link to a browser.", "h2_2": "Register", "p7": "To protect the round from bribery and fraud, you need to add your wallet address to a smart contract registry. Once you’re done, you can join the funding round!", "btn": " Become a contributor" @@ -998,17 +998,12 @@ "link2": "@Twitter" }, "matchigFundsModal": { - "h3_1_t1": "Contribute { tokenSymbol } to the", - "h3_1_if1": "next", - "h3_1_if2": "current", - "h3_1_t2": "round", + "h3_1_t1": "Contribute { tokenSymbol } to the matching pool", + "h3_div": "The funds will be distributed to all projects based on the contributions they receive from the community", "div1": " ⚠️ You only have { renderBalance } { tokenSymbol }", "button1": "Cancel", "button2": "Contribute", - "h3_2_t1": " Contribute { renderContributionAmount } { tokenSymbol } to the", - "h3_2_if1": "next", - "h3_2_if2": "current", - "h3_2_t2": "round", + "h3_2_t1": " Contribute { renderContributionAmount } { tokenSymbol } to the matching pool", "h3_3": " You just topped up the pool by { renderContributionAmount } { tokenSymbol }!", "div2": "Thanks for helping out all our projects.", "button3": "Done" From 6330683bf095c95e9f1bd4235b86cdad2fd5a107 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Thu, 25 May 2023 00:09:55 -0400 Subject: [PATCH 02/55] fix undefined buffer error while sponsoring user --- vue-app/src/api/bright-id.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vue-app/src/api/bright-id.ts b/vue-app/src/api/bright-id.ts index 1fa51238e..6e051150e 100644 --- a/vue-app/src/api/bright-id.ts +++ b/vue-app/src/api/bright-id.ts @@ -221,7 +221,7 @@ export async function brightIdSponsor(userAddress: string): Promise } const message = JSON.stringify(op) - const arrayedMessage = Buffer.from(message) + const arrayedMessage = utils.toUtf8Bytes(message) const arrayedKey = utils.base64.decode(brightIdSponsorKey) const signature = nacl.sign.detached(arrayedMessage, arrayedKey) op.sig = utils.base64.encode(signature) From bcda6eac8b00cceba16370e9114e5b257b1354ea Mon Sep 17 00:00:00 2001 From: yuetloo Date: Thu, 1 Jun 2023 12:22:32 -0400 Subject: [PATCH 03/55] remove the check to avoid throwing due to precision loss --- vue-app/src/components/ContributionModal.vue | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/vue-app/src/components/ContributionModal.vue b/vue-app/src/components/ContributionModal.vue index 74b1935c4..f628d076f 100644 --- a/vue-app/src/components/ContributionModal.vue +++ b/vue-app/src/components/ContributionModal.vue @@ -265,13 +265,9 @@ async function contribute() { contributionTxError.value = error.message return } - // Get state index and amount of voice credits + // Get state index const maci = new Contract(maciAddress, MACI, userStore.signer) const stateIndex = getEventArg(contributionTxReceipt, maci, 'SignUp', '_stateIndex') - const voiceCredits = getEventArg(contributionTxReceipt, maci, 'SignUp', '_voiceCreditBalance') - if (!voiceCredits.mul(voiceCreditFactor).eq(total.value)) { - throw new Error('Incorrect amount of voice credits') - } const contributor = { keypair: contributorKeypair, stateIndex: stateIndex.toNumber(), From 90b25fdd5be763f00fb12da47437064e9f6f6b13 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Thu, 1 Jun 2023 12:52:28 -0400 Subject: [PATCH 04/55] fix missing round error asking for user signature without active round --- vue-app/src/App.vue | 3 ++- vue-app/src/components/CallToActionCard.vue | 3 ++- vue-app/src/components/Cart.vue | 18 +++++++++--------- vue-app/src/stores/app.ts | 2 ++ vue-app/src/views/Profile.vue | 2 +- vue-app/src/views/VerifyLanding.vue | 2 +- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/vue-app/src/App.vue b/vue-app/src/App.vue index 332b6715e..c8e06bfeb 100644 --- a/vue-app/src/App.vue +++ b/vue-app/src/App.vue @@ -85,7 +85,7 @@ const routeName = computed(() => route.name?.toString() || '') const isUserAndRoundLoaded = computed(() => !!currentUser.value && !!currentRound.value) const isInApp = computed(() => routeName.value !== 'landing') const isVerifyStep = computed(() => routeName.value === 'verify-step') -const isSideCartShown = computed(() => !!currentUser.value && isSidebarShown.value && routeName.value !== 'cart') +const isSideCartShown = computed(() => isUserAndRoundLoaded.value && isSidebarShown.value && routeName.value !== 'cart') const isCartPadding = computed(() => { const routes = ['cart'] return routes.includes(routeName.value) @@ -169,6 +169,7 @@ onMounted(async () => { await appStore.loadMACIFactoryInfo() await appStore.loadRoundInfo() await recipientStore.loadRecipientRegistryInfo() + appStore.isAppReady = true setupLoadIntervals() }) diff --git a/vue-app/src/components/CallToActionCard.vue b/vue-app/src/components/CallToActionCard.vue index 64f0d8f45..46e3d3512 100644 --- a/vue-app/src/components/CallToActionCard.vue +++ b/vue-app/src/components/CallToActionCard.vue @@ -48,7 +48,7 @@ import { useAppStore, useUserStore } from '@/stores' import { storeToRefs } from 'pinia' const appStore = useAppStore() -const { canUserReallocate, hasContributionPhaseEnded } = storeToRefs(appStore) +const { canUserReallocate, hasContributionPhaseEnded, currentRound } = storeToRefs(appStore) const userStore = useUserStore() const { currentUser } = storeToRefs(userStore) @@ -58,6 +58,7 @@ const hasStartedVerification = computed( const showUserVerification = computed(() => { return ( userRegistryType === UserRegistryType.BRIGHT_ID && + currentRound.value && currentUser.value?.isRegistered !== undefined && !currentUser.value.isRegistered ) diff --git a/vue-app/src/components/Cart.vue b/vue-app/src/components/Cart.vue index bbcd68336..a9d5e2a2a 100644 --- a/vue-app/src/components/Cart.vue +++ b/vue-app/src/components/Cart.vue @@ -1,6 +1,11 @@ From 89bfc21295a0c080931fe761e2f9f2bd614f1825 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Fri, 2 Jun 2023 02:34:35 +0000 Subject: [PATCH 06/55] v4.1.2 --- contracts/package.json | 2 +- subgraph/package.json | 2 +- vue-app/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/package.json b/contracts/package.json index 8104f9acb..1988ea3ed 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@clrfund/contracts", - "version": "4.1.1", + "version": "4.1.2", "license": "GPL-3.0", "scripts": { "hardhat": "hardhat", diff --git a/subgraph/package.json b/subgraph/package.json index 3f27453d4..71dbb03e4 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@clrfund/subgraph", - "version": "4.1.1", + "version": "4.1.2", "repository": "https://github.com/clrfund/monorepo/subgraph", "keywords": [ "clr.fund", diff --git a/vue-app/package.json b/vue-app/package.json index 2500a6c7a..1a5c2364a 100644 --- a/vue-app/package.json +++ b/vue-app/package.json @@ -1,6 +1,6 @@ { "name": "@clrfund/vue-app", - "version": "4.1.1", + "version": "4.1.2", "private": true, "license": "GPL-3.0", "scripts": { From d25964b4b151336ff586993420cfa8ee51a342d2 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Fri, 2 Jun 2023 22:49:38 -0400 Subject: [PATCH 07/55] show leaderboard view if available --- vue-app/src/api/core.ts | 7 +- vue-app/src/api/leaderboard.ts | 9 ++ vue-app/src/api/projects.ts | 35 ++++- vue-app/src/api/round.ts | 76 +++++++++- vue-app/src/api/rounds.ts | 11 +- .../src/components/LeaderboardDetailView.vue | 16 +-- .../src/components/LeaderboardSimpleView.vue | 14 +- vue-app/src/components/RoundInformation.vue | 21 ++- vue-app/src/locales/cn.json | 4 + vue-app/src/locales/en.json | 4 + vue-app/src/router/index.ts | 16 ++- vue-app/src/stores/app.ts | 15 +- vue-app/src/utils/url.ts | 23 ---- vue-app/src/views/Landing.vue | 53 +++---- vue-app/src/views/Leaderboard.vue | 19 ++- vue-app/src/views/LeaderboardProject.vue | 130 ++++++++++++++++++ vue-app/src/views/ProjectList.vue | 12 +- vue-app/src/views/RoundList.vue | 11 ++ 18 files changed, 358 insertions(+), 118 deletions(-) create mode 100644 vue-app/src/api/leaderboard.ts create mode 100644 vue-app/src/views/LeaderboardProject.vue diff --git a/vue-app/src/api/core.ts b/vue-app/src/api/core.ts index 02718132b..4258b060a 100644 --- a/vue-app/src/api/core.ts +++ b/vue-app/src/api/core.ts @@ -3,6 +3,8 @@ import { ethers } from 'ethers' import { FundingRoundFactory } from './abi' import { CHAIN_INFO } from '@/utils/chains' +import leaderboardRounds from '@/rounds/rounds.json' + export const rpcUrl = import.meta.env.VITE_ETHEREUM_API_URL if (!rpcUrl) { throw new Error('Please provide ethereum rpc url for connecting to blockchain') @@ -48,9 +50,6 @@ if (!['simple', 'optimistic', 'kleros'].includes(recipientRegistryType as string } export const recipientRegistryPolicy = import.meta.env.VITE_RECIPIENT_REGISTRY_POLICY export const operator: string = import.meta.env.VITE_OPERATOR || 'Clr.fund' -export const extraRounds: string[] = import.meta.env.VITE_EXTRA_ROUNDS - ? import.meta.env.VITE_EXTRA_ROUNDS.split(',') - : [] export const SUBGRAPH_ENDPOINT = import.meta.env.VITE_SUBGRAPH_URL || 'https://api.thegraph.com/subgraphs/name/clrfund/clrfund' @@ -74,3 +73,5 @@ export const brightIdSponsorUrl = import.meta.env.VITE_BRIGHTID_SPONSOR_API_URL // wait for data to sync with the subgraph export const MAX_WAIT_DEPTH = Number(import.meta.env.VITE_MAX_WAIT_DEPTH) || 15 + +export { leaderboardRounds } diff --git a/vue-app/src/api/leaderboard.ts b/vue-app/src/api/leaderboard.ts new file mode 100644 index 000000000..8102e23dc --- /dev/null +++ b/vue-app/src/api/leaderboard.ts @@ -0,0 +1,9 @@ +import leaderboardRounds from '@/rounds/rounds.json' + +export async function getLeaderboardData(roundAddress: string) { + const found = leaderboardRounds.find(record => { + return record.address === roundAddress + }) + + return found ? import(`../rounds/${found.network}/${found.address}`) : null +} diff --git a/vue-app/src/api/projects.ts b/vue-app/src/api/projects.ts index 204c16c96..d0c57eb09 100644 --- a/vue-app/src/api/projects.ts +++ b/vue-app/src/api/projects.ts @@ -7,6 +7,7 @@ import SimpleRegistry from './recipient-registry-simple' import OptimisticRegistry from './recipient-registry-optimistic' import KlerosRegistry from './recipient-registry-kleros' import sdk from '@/graphql/sdk' +import { getLeaderboardData } from '@/api/leaderboard' export interface LeaderboardProject { id: string // Address or another ID depending on registry implementation @@ -111,7 +112,7 @@ export async function getProjectByIndex( recipientIndex, }) - if (!result.recipients?.length) { + if (!result.recipients.length) { return null } @@ -175,3 +176,35 @@ export function toLeaderboardProject(project: any): LeaderboardProject { donation: BigNumber.from(project.spentVoiceCredits || '0'), } } + +export async function getLeaderboardProject( + roundAddress: string, + projectId: string, + network: string, +): Promise { + const data = await getLeaderboardData(roundAddress, network) + if (!data) { + return null + } + + const project = data.projects.find(project => project.id === projectId) + + const metadata = project.metadata + const thumbnailHash = metadata.imageHash + const thumbnailImageUrl = thumbnailHash ? `${ipfsGatewayUrl}/ipfs/${thumbnailHash}` : undefined + const bannerHash = metadata.imageHash + const bannerImageUrl = bannerHash ? `${ipfsGatewayUrl}/ipfs/${bannerHash}` : undefined + + return { + id: project.id, + address: project.recipientAddress || '', + name: project.name, + description: metadata.description, + tagline: metadata.tagline, + thumbnailImageUrl, + bannerImageUrl, + index: project.recipientIndex, + isHidden: false, // always show leaderboard project + isLocked: true, // Visible, but contributions are not allowed + } +} diff --git a/vue-app/src/api/round.ts b/vue-app/src/api/round.ts index 82edac8da..852f8b057 100644 --- a/vue-app/src/api/round.ts +++ b/vue-app/src/api/round.ts @@ -7,9 +7,10 @@ import { provider, factory } from './core' import { getTotalContributed } from './contributions' import { getRounds } from './rounds' import sdk from '@/graphql/sdk' -import { assert, ASSERT_MISSING_ROUND } from '@/utils/assert' import { isSameAddress } from '@/utils/accounts' +import { Keypair } from '@clrfund/maci-utils' +import { getLeaderboardData } from '@/api/leaderboard' export interface RoundInfo { fundingRoundAddress: string @@ -34,6 +35,8 @@ export interface RoundInfo { contributions: FixedNumber contributors: number messages: number + blogUrl?: string + network?: string } export interface TimeLeft { @@ -65,8 +68,73 @@ export async function getCurrentRound(): Promise { return null } +function toRoundInfo(data: any, network: string): RoundInfo { + const nativeTokenDecimals = Number(data.nativeTokenDecimals) + // leaderboard does not need coordinator key, generate a dummy number + const keypair = Keypair.createFromSeed(utils.hexlify(utils.randomBytes(32))) + const coordinatorPubKey = keypair.pubKey + + const voiceCreditFactor = BigNumber.from(data.voiceCreditFactor) + const contributions = BigNumber.from(data.totalSpent).mul(voiceCreditFactor) + const matchingPool = BigNumber.from(data.matchingPoolSize) + let status = RoundStatus.Cancelled + if (data.isCancelled) { + status = RoundStatus.Cancelled + } else if (data.isFinalized) { + status = RoundStatus.Finalized + } + const totalFunds = contributions.add(matchingPool) + + return { + fundingRoundAddress: data.address, + recipientRegistryAddress: utils.getAddress(data.recipientRegistryAddress), + userRegistryAddress: utils.getAddress(data.userRegistryAddress), + maciAddress: utils.getAddress(data.maciAddress), + recipientTreeDepth: 0, + maxContributors: 0, + maxRecipients: data.maxRecipients, + maxMessages: data.maxMessages, + coordinatorPubKey, + nativeTokenAddress: utils.getAddress(data.nativeTokenAddress), + nativeTokenSymbol: data.nativeTokenSymbol, + nativeTokenDecimals, + voiceCreditFactor, + status, + startTime: DateTime.fromSeconds(data.startTime), + signUpDeadline: DateTime.fromSeconds(Number(data.startTime) + Number(data.signUpDuration)), + votingDeadline: DateTime.fromSeconds(Number(data.startTime) + Number(data.votingDuration)), + totalFunds: FixedNumber.fromValue(totalFunds, nativeTokenDecimals), + matchingPool: FixedNumber.fromValue(matchingPool, nativeTokenDecimals), + contributions: FixedNumber.fromValue(contributions, nativeTokenDecimals), + contributors: data.contributorCount, + messages: Number(data.messages), + blogUrl: data.blogUrl, + network, + } +} + +export async function getLeaderboardRoundInfo(fundingRoundAddress: string, network: string): Promise { + const data = await getLeaderboardData(fundingRoundAddress, network) + if (!data) { + return null + } + + let round: RoundInfo | null = null + try { + round = toRoundInfo(data.round, network) + } catch (err) { + /* eslint-disable-next-line no-console */ + console.warn(`Failed map leaderboard round info`, err) + } + + return round +} + //TODO: update to take factory address as a parameter, default to env. variable -export async function getRoundInfo(fundingRoundAddress: string, cachedRound?: RoundInfo | null): Promise { +export async function getRoundInfo( + fundingRoundAddress: string, + cachedRound?: RoundInfo | null, +): Promise { if (cachedRound && isSameAddress(fundingRoundAddress, cachedRound.fundingRoundAddress)) { // the requested round matches the cached round, quick return return cachedRound @@ -77,7 +145,9 @@ export async function getRoundInfo(fundingRoundAddress: string, cachedRound?: Ro fundingRoundAddress: fundingRoundAddress.toLowerCase(), }) - assert(data.fundingRound, ASSERT_MISSING_ROUND) + if (!data.fundingRound) { + return null + } const { maci: maciAddress, diff --git a/vue-app/src/api/rounds.ts b/vue-app/src/api/rounds.ts index 608786df7..e68ebd2de 100644 --- a/vue-app/src/api/rounds.ts +++ b/vue-app/src/api/rounds.ts @@ -1,25 +1,28 @@ import sdk from '@/graphql/sdk' -import { ipfsGatewayUrl, extraRounds } from './core' +import extraRounds from '@/rounds/rounds.json' export interface Round { index: number address: string - url?: string + network?: string + hasLeaderboard: boolean } + //TODO: update to take factory address as a parameter export async function getRounds(): Promise { //TODO: updateto pass factory address as a parameter, default to env. variable //NOTE: why not instantiate the sdk here? const data = await sdk.GetRounds() - const rounds: Round[] = extraRounds.map((ipfsHash: string, index): Round => { - return { index, address: '', url: `${ipfsGatewayUrl}/ipfs/${ipfsHash}` } + const rounds: Round[] = extraRounds.map(({ address, network }, index): Round => { + return { index, address, network, hasLeaderboard: true } }) for (const fundingRound of data.fundingRounds) { rounds.push({ index: rounds.length, address: fundingRound.id, + hasLeaderboard: false, }) } return rounds diff --git a/vue-app/src/components/LeaderboardDetailView.vue b/vue-app/src/components/LeaderboardDetailView.vue index 861fdf05a..25f86c78e 100644 --- a/vue-app/src/components/LeaderboardDetailView.vue +++ b/vue-app/src/components/LeaderboardDetailView.vue @@ -77,23 +77,11 @@ const props = defineProps() function projectRoute(id: string): LocationAsRelativeRaw { return { - name: 'round-project', - params: { id, address: props.round.fundingRoundAddress }, + name: 'leaderboard-project', + params: { id, address: props.round.fundingRoundAddress, network: props.round.network }, } } -function isFirst(index: number) { - return index === 0 -} - -function isSecond(index: number) { - return index === 1 -} - -function isThird(index: number) { - return index === 2 -} - function formatVotes(votes: BigNumber): string { // pass votes as string to skip formatting by tokenDecimal return formatAmount(votes.toString(), tokenDecimals.value, null, 0) diff --git a/vue-app/src/components/LeaderboardSimpleView.vue b/vue-app/src/components/LeaderboardSimpleView.vue index 027c3aec2..047d37741 100644 --- a/vue-app/src/components/LeaderboardSimpleView.vue +++ b/vue-app/src/components/LeaderboardSimpleView.vue @@ -1,6 +1,11 @@ diff --git a/vue-app/src/views/ProjectList.vue b/vue-app/src/views/ProjectList.vue index f2e0a4bb2..de926a697 100644 --- a/vue-app/src/views/ProjectList.vue +++ b/vue-app/src/views/ProjectList.vue @@ -119,6 +119,10 @@ onMounted(async () => { async function loadProjects(roundAddress: string) { const round = await getRoundInfo(roundAddress, currentRound.value) + if (!round) { + return + } + const _projects = await getProjects( round.recipientRegistryAddress, round.startTime.toSeconds(), @@ -131,14 +135,6 @@ async function loadProjects(roundAddress: string) { projects.value = visibleProjects } -function formatIntegerPart(value: FixedNumber): string { - if (value._value === '0.0') { - return '0' - } - const integerPart = value.toString().split('.')[0] - return integerPart + (value.round(0) === value ? '' : '.') -} - const filteredProjects = computed(() => { return projectsByCategoriesSelected.value.filter((project: Project) => { if (!search.value) { diff --git a/vue-app/src/views/RoundList.vue b/vue-app/src/views/RoundList.vue index 5e9b35079..36a91d461 100644 --- a/vue-app/src/views/RoundList.vue +++ b/vue-app/src/views/RoundList.vue @@ -3,6 +3,17 @@

{{ $t('roundList.h1') }}

+ {{ $t('roundList.link1', { index: round.index }) }} + + Date: Fri, 2 Jun 2023 23:24:44 -0400 Subject: [PATCH 09/55] fix missing argument error --- vue-app/src/api/leaderboard.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/vue-app/src/api/leaderboard.ts b/vue-app/src/api/leaderboard.ts index 8102e23dc..a17b31b34 100644 --- a/vue-app/src/api/leaderboard.ts +++ b/vue-app/src/api/leaderboard.ts @@ -1,9 +1,18 @@ import leaderboardRounds from '@/rounds/rounds.json' -export async function getLeaderboardData(roundAddress: string) { - const found = leaderboardRounds.find(record => { - return record.address === roundAddress +type LeaderboardRecord = { + address: string + network: string +} + +export async function getLeaderboardData(roundAddress: string, network: string) { + const rounds = leaderboardRounds as LeaderboardRecord[] + + const lowercaseRoundAddress = roundAddress.toLocaleLowerCase() + const lowercaseNetwork = network.toLowerCase() + const found = rounds.find((r: LeaderboardRecord) => { + return r.address.toLowerCase() === lowercaseRoundAddress && r.network.toLowerCase() === lowercaseNetwork }) - return found ? import(`../rounds/${found.network}/${found.address}`) : null + return found ? import(`../rounds/${found.network}/${found.address}.json`) : null } From 6a1e6f8957bb821513e7b321cc032209879ee07a Mon Sep 17 00:00:00 2001 From: yuetloo Date: Sat, 3 Jun 2023 01:29:40 -0400 Subject: [PATCH 10/55] split build output manually to reduce single output size --- vue-app/package.json | 5 ++--- vue-app/src/api/round.ts | 2 +- vue-app/src/components/ReallocationModal.vue | 2 +- vue-app/src/views/LeaderboardProject.vue | 4 ---- vue-app/src/views/ProjectList.vue | 1 - vue-app/vite.config.ts | 13 ++++++++++++- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/vue-app/package.json b/vue-app/package.json index 1a5c2364a..3018caf54 100644 --- a/vue-app/package.json +++ b/vue-app/package.json @@ -39,8 +39,6 @@ "humanize-duration": "^3.27.3", "is-ipfs": "^7.0.3", "luxon": "^3.1.1", - "maci-crypto": "npm:@clrfund/maci-crypto@0.10.2", - "maci-domainobjs": "npm:@clrfund/maci-domainobjs@0.10.2", "markdown-it": "^13.0.1", "markdown-it-link-attributes": "^4.0.1", "node-stdlib-browser": "^1.2.0", @@ -50,7 +48,8 @@ "vue-final-modal": "^4.3.0", "vue-i18n": "9", "vue-meta": "3.0.0-alpha.8", - "vue-router": "4" + "vue-router": "4", + "@clrfund/maci-utils": "^0.0.1" }, "devDependencies": { "@graphql-codegen/cli": "^3.3.0", diff --git a/vue-app/src/api/round.ts b/vue-app/src/api/round.ts index 852f8b057..61f6b115f 100644 --- a/vue-app/src/api/round.ts +++ b/vue-app/src/api/round.ts @@ -1,6 +1,6 @@ import { BigNumber, Contract, utils, FixedNumber } from 'ethers' import { DateTime } from 'luxon' -import { PubKey } from 'maci-domainobjs' +import { PubKey } from '@clrfund/maci-utils' import { FundingRound, MACI } from './abi' import { provider, factory } from './core' diff --git a/vue-app/src/components/ReallocationModal.vue b/vue-app/src/components/ReallocationModal.vue index 55e068bf2..2fba4b54b 100644 --- a/vue-app/src/components/ReallocationModal.vue +++ b/vue-app/src/components/ReallocationModal.vue @@ -22,7 +22,7 @@ From 476294548e325c4a15ea72a088f829e1d873d7c1 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Thu, 8 Jun 2023 16:52:21 +0000 Subject: [PATCH 25/55] v4.2.0 --- contracts/package.json | 2 +- subgraph/package.json | 2 +- vue-app/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/package.json b/contracts/package.json index 1988ea3ed..ea8d43e71 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@clrfund/contracts", - "version": "4.1.2", + "version": "4.2.0", "license": "GPL-3.0", "scripts": { "hardhat": "hardhat", diff --git a/subgraph/package.json b/subgraph/package.json index 71dbb03e4..e232ff6da 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@clrfund/subgraph", - "version": "4.1.2", + "version": "4.2.0", "repository": "https://github.com/clrfund/monorepo/subgraph", "keywords": [ "clr.fund", diff --git a/vue-app/package.json b/vue-app/package.json index 3018caf54..51f68c3c7 100644 --- a/vue-app/package.json +++ b/vue-app/package.json @@ -1,6 +1,6 @@ { "name": "@clrfund/vue-app", - "version": "4.1.2", + "version": "4.2.0", "private": true, "license": "GPL-3.0", "scripts": { From 8afab424fda3bf1f8eb1853012a43239b2196c5b Mon Sep 17 00:00:00 2001 From: yuetloo Date: Thu, 8 Jun 2023 15:08:28 -0400 Subject: [PATCH 26/55] script to bulkload users into the the simple user registry --- contracts/tasks/index.ts | 1 + contracts/tasks/loadUsers.ts | 86 ++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 contracts/tasks/loadUsers.ts diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts index 37d8529c8..6f4bfc5e2 100644 --- a/contracts/tasks/index.ts +++ b/contracts/tasks/index.ts @@ -12,3 +12,4 @@ import './fetchRound' import './mergeAllocations' import './setDurations' import './deploySponsor' +import './loadUsers' diff --git a/contracts/tasks/loadUsers.ts b/contracts/tasks/loadUsers.ts new file mode 100644 index 000000000..6fb185d2f --- /dev/null +++ b/contracts/tasks/loadUsers.ts @@ -0,0 +1,86 @@ +import { task } from 'hardhat/config' +import { Contract, utils, ContractReceipt } from 'ethers' +import fs from 'fs' + +/* + * Script to bulkload users into the simple user registry. + * File path can be relative or absolute path. + * The script can only be run by the owner of the simple user registry + * + * Sample usage: + * + * yarn hardhat load-users --file-path addresses.txt --user-registry
--network goerli + */ + +/** + * Add a user to the Simple user registry + * + * @param registry Simple user registry contract + * @param address User wallet address + * @returns transaction receipt + */ +async function addUser( + registry: Contract, + address: string +): Promise { + const tx = await registry.addUser(address) + const receipt = await tx.wait() + return receipt +} + +/** + * Load users in the file into the simple user registry + * + * @param registry Simple user registry contract + * @param filePath The path of the file containing the addresses + */ +async function loadFile(registry: Contract, filePath: string) { + let content: string | null = null + try { + content = fs.readFileSync(filePath, 'utf8') + } catch (err) { + console.error('Failed to read file', filePath, err) + return + } + + const addresses: string[] = [] + content.split(/\r?\n/).forEach(async (address) => { + addresses.push(address) + }) + + for (let i = 0; i < addresses.length; i++) { + const address = addresses[i] + const isValidAddress = Boolean(address) && utils.isAddress(address) + if (isValidAddress) { + console.log('Adding address', address) + try { + const result = await addUser(registry, address) + if (result.status !== 1) { + throw new Error( + `Transaction ${result.transactionHash} failed with status ${result.status}` + ) + } + } catch (err) { + console.error('Failed to add address', address, err) + break + } + } else { + console.warn('Skipping invalid address', address) + } + } +} + +task('load-users', 'Bulkload recipients into the simple user registry') + .addParam('userRegistry', 'The simple user registry contract address') + .addParam( + 'filePath', + 'The path of the file containing addresses separated by newline' + ) + .setAction(async ({ userRegistry, filePath }, { ethers }) => { + const registry = await ethers.getContractAt( + 'SimpleUserRegistry', + userRegistry + ) + + await loadFile(registry, filePath) + }) From 88627fa02a075c7c08b1f43c0b909c0d62fc6442 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Fri, 9 Jun 2023 03:53:23 -0400 Subject: [PATCH 27/55] update UI for simple user and recipient registry --- vue-app/src/api/core.ts | 3 + vue-app/src/api/recipient-registry-kleros.ts | 31 +++++- .../src/api/recipient-registry-optimistic.ts | 97 ++----------------- vue-app/src/api/recipient-registry-simple.ts | 58 ++++++++++- vue-app/src/api/recipient-registry.ts | 36 +++++++ vue-app/src/api/recipient.ts | 45 +++++++++ vue-app/src/api/types.ts | 43 ++++++++ vue-app/src/components/Cart.vue | 3 +- vue-app/src/components/NavBar.vue | 4 +- vue-app/src/locales/cn.json | 2 + vue-app/src/locales/en.json | 4 +- vue-app/src/locales/es.json | 4 +- vue-app/src/router/index.ts | 73 ++++++++------ vue-app/src/stores/recipient.ts | 4 +- vue-app/src/views/JoinView.vue | 9 +- vue-app/src/views/Landing.vue | 15 +-- vue-app/src/views/ProjectAdded.vue | 6 +- 17 files changed, 291 insertions(+), 146 deletions(-) create mode 100644 vue-app/src/api/recipient-registry.ts create mode 100644 vue-app/src/api/recipient.ts create mode 100644 vue-app/src/api/types.ts diff --git a/vue-app/src/api/core.ts b/vue-app/src/api/core.ts index b5bc0f49b..99ceb13ba 100644 --- a/vue-app/src/api/core.ts +++ b/vue-app/src/api/core.ts @@ -84,3 +84,6 @@ export { leaderboardRounds } export const useHumanbound = /^yes$/i.test(import.meta.env.VITE_USE_HUMANBOUND) export const humanboundWebsiteUrl = 'https://app.humanbound.xyz' + +export const isBrightIdRequired = userRegistryType === 'brightid' +export const isOptimisticRecipientRegistry = recipientRegistryType === 'optimistic' diff --git a/vue-app/src/api/recipient-registry-kleros.ts b/vue-app/src/api/recipient-registry-kleros.ts index e9f94b8e5..2a260ebf8 100644 --- a/vue-app/src/api/recipient-registry-kleros.ts +++ b/vue-app/src/api/recipient-registry-kleros.ts @@ -1,10 +1,11 @@ -import { Contract, type Event, Signer } from 'ethers' +import { Contract, type Event, Signer, BigNumber } from 'ethers' import type { TransactionResponse } from '@ethersproject/abstract-provider' import { gtcrDecode } from '@kleros/gtcr-encoder' import { KlerosGTCR, KlerosGTCRAdapter } from './abi' import { provider, ipfsGatewayUrl } from './core' import type { Project } from './projects' +import type { RegistryInfo } from './types' const KLEROS_CURATE_URL = 'https://curate.kleros.io/tcr/0x2E3B10aBf091cdc53cC892A50daBDb432e220398' @@ -179,4 +180,30 @@ export async function registerProject( return transaction } -export default { getProjects, getProject, registerProject } +async function getRegistryInfo(registryAddress: string): Promise { + const registry = new Contract(registryAddress, KlerosGTCRAdapter, provider) + + let recipientCount + try { + recipientCount = await registry.getRecipientCount() + } catch { + // older BaseRecipientRegistry contract did not have recipientCount + // set it to zero as this information is only + // used during current round for space calculation + recipientCount = BigNumber.from(0) + } + + // Kleros registry does not have owner + const owner = '' + + // deposit, depositToken and challengePeriodDuration are only relevant to the optimistic registry + return { + deposit: BigNumber.from(0), + depositToken: '', + challengePeriodDuration: 0, + recipientCount: recipientCount.toNumber(), + owner, + } +} + +export default { getProjects, getProject, registerProject, getRegistryInfo } diff --git a/vue-app/src/api/recipient-registry-optimistic.ts b/vue-app/src/api/recipient-registry-optimistic.ts index 59686c312..de049e58c 100644 --- a/vue-app/src/api/recipient-registry-optimistic.ts +++ b/vue-app/src/api/recipient-registry-optimistic.ts @@ -6,22 +6,15 @@ import { getEventArg } from '@/utils/contracts' import { chain } from '@/api/core' import { OptimisticRecipientRegistry } from './abi' -import { provider, ipfsGatewayUrl, recipientRegistryPolicy } from './core' +import { provider, ipfsGatewayUrl } from './core' import type { Project } from './projects' import sdk from '@/graphql/sdk' import type { Recipient } from '@/graphql/API' import { hasDateElapsed } from '@/utils/dates' +import type { RegistryInfo, RecipientApplicationData } from './types' +import { formToRecipientData } from './recipient' -export interface RegistryInfo { - deposit: BigNumber - depositToken: string - challengePeriodDuration: number - listingPolicyUrl: string - recipientCount: number - owner: string -} - -export async function getRegistryInfo(registryAddress: string): Promise { +async function getRegistryInfo(registryAddress: string): Promise { const registry = new Contract(registryAddress, OptimisticRecipientRegistry, provider) const deposit = await registry.baseDeposit() const challengePeriodDuration = await registry.challengePeriodDuration() @@ -39,7 +32,6 @@ export async function getRegistryInfo(registryAddress: string): Promise requests[recipientId]) } -// TODO merge this with `Project` inteface -export interface RecipientData { - name: string - description: string - imageHash?: string // TODO remove - old flow - address: string - tagline?: string - category?: string - problemSpace?: string - plans?: string - teamName?: string - teamDescription?: string - githubUrl?: string - radicleUrl?: string - websiteUrl?: string - twitterUrl?: string - discordUrl?: string - // fields different vs. Project - bannerImageHash?: string - thumbnailImageHash?: string -} - -export function formToRecipientData(data: RecipientApplicationData): RecipientData { - const { project, fund, team, links, image } = data - return { - address: fund.resolvedAddress, - name: project.name, - tagline: project.tagline, - description: project.description, - category: project.category, - problemSpace: project.problemSpace, - plans: fund.plans, - teamName: team.name, - teamDescription: team.description, - githubUrl: links.github, - radicleUrl: links.radicle, - websiteUrl: links.website, - twitterUrl: links.twitter, - discordUrl: links.discord, - bannerImageHash: image.bannerHash, - thumbnailImageHash: image.thumbnailHash, - } -} - -export async function addRecipient( +async function addRecipient( registryAddress: string, recipientApplicationData: RecipientApplicationData, deposit: BigNumber, @@ -444,4 +359,4 @@ export async function removeProject(registryAddress: string, recipientId: string return transaction } -export default { getProjects, getProject, registerProject, decodeProject } +export default { getProjects, getProject, registerProject, decodeProject, getRegistryInfo, addRecipient } diff --git a/vue-app/src/api/recipient-registry-simple.ts b/vue-app/src/api/recipient-registry-simple.ts index 59777de0e..568814c93 100644 --- a/vue-app/src/api/recipient-registry-simple.ts +++ b/vue-app/src/api/recipient-registry-simple.ts @@ -1,20 +1,36 @@ -import { Contract } from 'ethers' +import { Contract, BigNumber, Signer } from 'ethers' import type { Event } from 'ethers' import { isHexString } from '@ethersproject/bytes' +import type { TransactionResponse } from '@ethersproject/abstract-provider' import { SimpleRecipientRegistry } from './abi' import { provider, ipfsGatewayUrl } from './core' import type { Project } from './projects' +import type { RegistryInfo, RecipientApplicationData } from './types' +import { formToRecipientData } from './recipient' function decodeRecipientAdded(event: Event): Project { const args = event.args as any const metadata = JSON.parse(args._metadata) + console.log('metata', metadata) return { id: args._recipientId, address: args._recipient, name: metadata.name, description: metadata.description, - imageUrl: `${ipfsGatewayUrl}/ipfs/${metadata.imageHash}`, + tagline: metadata.tagline, + category: metadata.category, + problemSpace: metadata.problemSpace, + plans: metadata.plans, + teamName: metadata.teamName, + teamDescription: metadata.teamDescription, + githubUrl: metadata.githubUrl, + radicleUrl: metadata.radicleUrl, + websiteUrl: metadata.websiteUrl, + twitterUrl: metadata.twitterUrl, + discordUrl: metadata.discordUrl, + bannerImageUrl: `${ipfsGatewayUrl}/ipfs/${metadata.bannerImageHash}`, + thumbnailImageUrl: `${ipfsGatewayUrl}/ipfs/${metadata.thumbnailImageHash}`, index: args._index.toNumber(), isHidden: false, isLocked: false, @@ -90,4 +106,40 @@ export async function getProject(registryAddress: string, recipientId: string): return project } -export default { getProjects, getProject } +async function getRegistryInfo(registryAddress: string): Promise { + const registry = new Contract(registryAddress, SimpleRecipientRegistry, provider) + + let recipientCount + try { + recipientCount = await registry.getRecipientCount() + } catch { + // older BaseRecipientRegistry contract did not have recipientCount + // set it to zero as this information is only + // used during current round for space calculation + recipientCount = BigNumber.from(0) + } + const owner = await registry.owner() + + // deposit, depositToken and challengePeriodDuration are only relevant to the optimistic registry + return { + deposit: BigNumber.from(0), + depositToken: '', + challengePeriodDuration: 0, + recipientCount: recipientCount.toNumber(), + owner, + } +} + +async function addRecipient( + registryAddress: string, + recipientApplicationData: RecipientApplicationData, + signer: Signer, +): Promise { + const registry = new Contract(registryAddress, SimpleRecipientRegistry, signer) + const recipientData = formToRecipientData(recipientApplicationData) + const { address, ...metadata } = recipientData + const transaction = await registry.addRecipient(address, JSON.stringify(metadata)) + return transaction +} + +export default { getProjects, getProject, getRegistryInfo, addRecipient } diff --git a/vue-app/src/api/recipient-registry.ts b/vue-app/src/api/recipient-registry.ts new file mode 100644 index 000000000..dd26dea8e --- /dev/null +++ b/vue-app/src/api/recipient-registry.ts @@ -0,0 +1,36 @@ +import type { RegistryInfo, RecipientApplicationData } from './types' +import { recipientRegistryType } from './core' +import SimpleRegistry from './recipient-registry-simple' +import OptimisticRegistry from './recipient-registry-optimistic' +import KlerosRegistry from './recipient-registry-kleros' +import type { BigNumber, Signer } from 'ethers' +import type { TransactionResponse } from '@ethersproject/abstract-provider' + +export async function getRegistryInfo(registryAddress: string): Promise { + if (recipientRegistryType === 'simple') { + return await SimpleRegistry.getRegistryInfo(registryAddress) + } else if (recipientRegistryType === 'optimistic') { + return await OptimisticRegistry.getRegistryInfo(registryAddress) + } else if (recipientRegistryType === 'kleros') { + return await KlerosRegistry.getRegistryInfo(registryAddress) + } else { + throw new Error('Invalid recipient registry type: ' + recipientRegistryType) + } +} + +export async function addRecipient( + registryAddress: string, + recipientApplicationData: RecipientApplicationData, + deposit: BigNumber, + signer: Signer, +): Promise { + if (recipientRegistryType === 'simple') { + return await SimpleRegistry.addRecipient(registryAddress, recipientApplicationData, signer) + } else if (recipientRegistryType === 'optimistic') { + return await OptimisticRegistry.addRecipient(registryAddress, recipientApplicationData, deposit, signer) + } else if (recipientRegistryType === 'kleros') { + throw new Error('Kleros recipient registry is not supported') + } else { + throw new Error('Invalid recipient registry type: ' + recipientRegistryType) + } +} diff --git a/vue-app/src/api/recipient.ts b/vue-app/src/api/recipient.ts new file mode 100644 index 000000000..3b9fd4ef3 --- /dev/null +++ b/vue-app/src/api/recipient.ts @@ -0,0 +1,45 @@ +import type { RecipientApplicationData } from './types' + +// TODO merge this with `Project` inteface +export interface RecipientData { + name: string + description: string + imageHash?: string // TODO remove - old flow + address: string + tagline?: string + category?: string + problemSpace?: string + plans?: string + teamName?: string + teamDescription?: string + githubUrl?: string + radicleUrl?: string + websiteUrl?: string + twitterUrl?: string + discordUrl?: string + // fields different vs. Project + bannerImageHash?: string + thumbnailImageHash?: string +} + +export function formToRecipientData(data: RecipientApplicationData): RecipientData { + const { project, fund, team, links, image } = data + return { + address: fund.resolvedAddress, + name: project.name, + tagline: project.tagline, + description: project.description, + category: project.category, + problemSpace: project.problemSpace, + plans: fund.plans, + teamName: team.name, + teamDescription: team.description, + githubUrl: links.github, + radicleUrl: links.radicle, + websiteUrl: links.website, + twitterUrl: links.twitter, + discordUrl: links.discord, + bannerImageHash: image.bannerHash, + thumbnailImageHash: image.thumbnailHash, + } +} diff --git a/vue-app/src/api/types.ts b/vue-app/src/api/types.ts new file mode 100644 index 000000000..96d3897b5 --- /dev/null +++ b/vue-app/src/api/types.ts @@ -0,0 +1,43 @@ +import type { BigNumber } from 'ethers' + +// Recipient registry info +export interface RegistryInfo { + deposit: BigNumber + depositToken: string + challengePeriodDuration: number + recipientCount: number + owner: string +} + +export interface RecipientApplicationData { + project: { + name: string + tagline: string + description: string + category: string + problemSpace: string + } + fund: { + addressName: string + resolvedAddress: string + plans: string + } + team: { + name: string + description: string + email: string + } + links: { + github: string + radicle: string + website: string + twitter: string + discord: string + } + image: { + bannerHash: string + thumbnailHash: string + } + furthestStep: number + hasEns: boolean +} diff --git a/vue-app/src/components/Cart.vue b/vue-app/src/components/Cart.vue index a9d5e2a2a..4346e814b 100644 --- a/vue-app/src/components/Cart.vue +++ b/vue-app/src/components/Cart.vue @@ -235,7 +235,7 @@ import CartItems from '@/components/CartItems.vue' import Links from '@/components/Links.vue' import TimeLeft from '@/components/TimeLeft.vue' import { MAX_CONTRIBUTION_AMOUNT, MAX_CART_SIZE, type CartItem, isContributionAmountValid } from '@/api/contributions' -import { userRegistryType, UserRegistryType } from '@/api/core' +import { userRegistryType, UserRegistryType, operator } from '@/api/core' import { RoundStatus } from '@/api/round' import { formatAmount as _formatAmount } from '@/utils/amounts' import FundsNeededWarning from '@/components/FundsNeededWarning.vue' @@ -478,6 +478,7 @@ const errorMessage = computed(() => { if (isMessageLimitReached.value) return t('dynamic.cart.error.reached_contribution_limit') if (!currentUser.value) return t('dynamic.cart.error.connect_wallet') if (isBrightIdRequired.value) return t('dynamic.cart.error.need_to_setup_brightid') + if (!currentUser.value.isRegistered) return t('dynamic.cart.error.user_not_registered', { operator }) if (!isFormValid()) return t('dynamic.cart.error.invalid_contribution_amount') if (cart.value.length > MAX_CART_SIZE) return t('dynamic.cart.error.exceeded_max_cart_size', { diff --git a/vue-app/src/components/NavBar.vue b/vue-app/src/components/NavBar.vue index 687cb2727..82b50b3f6 100644 --- a/vue-app/src/components/NavBar.vue +++ b/vue-app/src/components/NavBar.vue @@ -53,7 +53,7 @@

{{ $t('navBar.dropdown.rounds') }}

-