diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 000000000..d9f880069
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+16.14.2
diff --git a/README.md b/README.md
index 8fcac22d2..6ee604039 100644
--- a/README.md
+++ b/README.md
@@ -175,7 +175,6 @@ to run `yarn test` or `yarn test:contracts`.
- [Vuelidate](https://vuelidate-next.netlify.app/)
- [vue-final-modal](https://vue-final-modal.org)
- [Ethers](https://docs.ethers.io/v5/)
- - [Gun](https://gun.eco/docs/)
### Visual Studio Code
diff --git a/contracts/.gitignore b/contracts/.gitignore
index f4bb331fe..67e19cff2 100644
--- a/contracts/.gitignore
+++ b/contracts/.gitignore
@@ -7,3 +7,4 @@ proofs.json
tally.json
.env
.DS_Store
+tasks/addresses.txt
diff --git a/contracts/package.json b/contracts/package.json
index c7b09883b..52c728195 100644
--- a/contracts/package.json
+++ b/contracts/package.json
@@ -1,6 +1,6 @@
{
"name": "@clrfund/contracts",
- "version": "4.1.1",
+ "version": "4.2.3",
"license": "GPL-3.0",
"scripts": {
"hardhat": "hardhat",
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..3a0515d95
--- /dev/null
+++ b/contracts/tasks/loadUsers.ts
@@ -0,0 +1,89 @@
+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: any) {
+ if (err.reason) {
+ console.error('Failed to add address', address, err.reason)
+ } else {
+ console.error('Failed to add address', address, err)
+ }
+ }
+ } 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)
+ })
diff --git a/docs/deployment.md b/docs/deployment.md
index 89a69a7f9..05bea95fe 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -44,11 +44,13 @@ BRIGHTID_SPONSOR=
1. Adjust the `/contracts/scripts/deploy.ts` as you wish.
2. Run `yarn hardhat run --network {network} scripts/deploy.ts` or use one of the `yarn deploy:{network}` available in `/contracts/package.json`.
-3. To deploy a new funding round, update the .env file with the funding round factory address deployed in the previous step and run the `newRound.ts` script:
+3. Make sure to save in a safe place the serializedCoordinatorPrivKey and the serializedCoordinatorPubKey, you are going to need them for the website and tallying the votes in future steps.
+4. To deploy a new funding round, update the .env file with the funding round factory address and the COORDINATOR_PK (with serializedCoordinatorPrivKey) deployed in the previous step and run the `newRound.ts` script:
```
# .env
FACTORY_ADDRESS=
+COORDINATOR_PK=
```
```
diff --git a/subgraph/package.json b/subgraph/package.json
index 3f27453d4..e56d96d15 100644
--- a/subgraph/package.json
+++ b/subgraph/package.json
@@ -1,6 +1,6 @@
{
"name": "@clrfund/subgraph",
- "version": "4.1.1",
+ "version": "4.2.3",
"repository": "https://github.com/clrfund/monorepo/subgraph",
"keywords": [
"clr.fund",
@@ -17,6 +17,7 @@
"prepare:hardhat": "mustache config/hardhat.json subgraph.template.yaml > subgraph.yaml",
"prepare:arbitrum": "mustache config/arbitrum.json subgraph.template.yaml > subgraph.yaml",
"prepare:arbitrum-rinkeby": "mustache config/arbitrum-rinkeby.json subgraph.template.yaml > subgraph.yaml",
+ "prepare:arbitrum-goerli": "mustache config/arbitrum-goerli.json subgraph.template.yaml > subgraph.yaml",
"prepare:xdai": "mustache config/xdai.json subgraph.template.yaml > subgraph.yaml",
"codegen": "graph codegen",
"lint:js": "eslint 'src/*.ts'",
diff --git a/subgraph/src/MACIMapping.ts b/subgraph/src/MACIMapping.ts
index 211b98a6e..fb9876957 100644
--- a/subgraph/src/MACIMapping.ts
+++ b/subgraph/src/MACIMapping.ts
@@ -91,24 +91,24 @@ export function handleSignUp(event: SignUp): void {
//NOTE: If the public keys aren't being tracked initialize them
if (publicKey == null) {
- let publicKey = new PublicKey(publicKeyId)
- publicKey.x = event.params._userPubKey.x
- publicKey.y = event.params._userPubKey.y
- publicKey.stateIndex = event.params._stateIndex
-
- publicKey.voiceCreditBalance = event.params._voiceCreditBalance
+ publicKey = new PublicKey(publicKeyId)
+ }
+ publicKey.x = event.params._userPubKey.x
+ publicKey.y = event.params._userPubKey.y
+ publicKey.stateIndex = event.params._stateIndex
- let fundingRoundAddress = event.transaction.to!
- let fundingRoundId = fundingRoundAddress.toHex()
- let fundingRound = FundingRound.load(fundingRoundId)
- if (fundingRound == null) {
- log.error('Error: handleSignUp failed, fundingRound not registered', [])
- return
- }
+ publicKey.voiceCreditBalance = event.params._voiceCreditBalance
- publicKey.fundingRound = fundingRoundId
- publicKey.save()
+ let fundingRoundAddress = event.transaction.to!
+ let fundingRoundId = fundingRoundAddress.toHex()
+ let fundingRound = FundingRound.load(fundingRoundId)
+ if (fundingRound == null) {
+ log.error('Error: handleSignUp failed, fundingRound not registered', [])
+ return
}
+ publicKey.fundingRound = fundingRoundId
+ publicKey.save()
+
log.info('SignUp', [])
}
diff --git a/vue-app/.env.example b/vue-app/.env.example
index 5e52ba294..29076daca 100644
--- a/vue-app/.env.example
+++ b/vue-app/.env.example
@@ -76,3 +76,7 @@ VITE_EXPORT_BATCH_SIZE=
# This is only used for netlify function, sponsor.js, to avoid getting the 'fetch not found' error
AWS_LAMBDA_JS_RUNTIME=nodejs18.x
+
+# This date will be used in the verify landing page. If not set or bad date format, a generic message will be displayed
+# format: yyyy-MM-dd e.g. 2023-06-26
+VITE_NEXT_ROUND_START_DATE=
diff --git a/vue-app/README.md b/vue-app/README.md
index 1cf302359..b97e4a8c0 100644
--- a/vue-app/README.md
+++ b/vue-app/README.md
@@ -1,20 +1,21 @@
-# [WIP] clrfund vue3-app
+# Clrfund Frontend
## Development
-### GCP VM using VSCode Remote - SSH
-- /graph-node/docker
- - `docker compose up -d` or `docker compose restart`
-- /clrfund
- - `yarn start:subgraph`
- - `yarn start:node`
- - `yarn deploy:local`
+- Start a development instance
+```
+yarn serve
+```
-### Local
-- /clrfund
- - `yarn start:gun`
-- /vue-app
- - `yarn serve`
-- /vue3-app
- - `yarn dev`
\ No newline at end of file
+- Runs static analysis on translation texts for missing or unused keys
+
+```
+yarn test:lint-i18n`
+```
+
+- Generate graphql API's
+
+```
+yarn codegen
+```
diff --git a/vue-app/package.json b/vue-app/package.json
index 2500a6c7a..953f565cd 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.2.3",
"private": true,
"license": "GPL-3.0",
"scripts": {
@@ -14,7 +14,7 @@
"type-check": "yarn vue-tsc --noEmit",
"codegen": "graphql-codegen --config codegen.yml",
"build-map": "NODE_OPTIONS=--max-old-space-size=4096 vite build --sourcemap",
- "test:lint-i18n": "vue-i18n-extract report --vueFiles 'src/**/*.?(vue)' --languageFiles 'src/locales/*.?(json)' --exclude dynamic"
+ "test:lint-i18n": "vue-i18n-extract report --vueFiles 'src/**/*.?(vue)' --languageFiles 'src/locales/*.?(json)' --exclude dynamic --exclude join.step1.address_requirement"
},
"lint-staged": {
"**/*.{js,ts,json,scss,css,vue}": [
@@ -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/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/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)
diff --git a/vue-app/src/api/claims.ts b/vue-app/src/api/claims.ts
index 45e306865..82da4d757 100644
--- a/vue-app/src/api/claims.ts
+++ b/vue-app/src/api/claims.ts
@@ -1,4 +1,4 @@
-import { Contract, FixedNumber } from 'ethers'
+import { Contract, BigNumber } from 'ethers'
import sdk from '@/graphql/sdk'
import { FundingRound } from './abi'
@@ -9,10 +9,10 @@ export async function getAllocatedAmount(
tokenDecimals: number,
result: string,
spent: string,
-): Promise {
+): Promise {
const fundingRound = new Contract(fundingRoundAddress, FundingRound, provider)
const allocatedAmount = await fundingRound.getAllocatedAmount(result, spent)
- return FixedNumber.fromValue(allocatedAmount, tokenDecimals)
+ return allocatedAmount
}
export async function isFundsClaimed(
diff --git a/vue-app/src/api/contributions.ts b/vue-app/src/api/contributions.ts
index d8bd2caf2..51c954012 100644
--- a/vue-app/src/api/contributions.ts
+++ b/vue-app/src/api/contributions.ts
@@ -162,7 +162,11 @@ export async function getContributorIndex(fundingRoundAddress: string, pubKey: P
publicKeyId: id,
})
- return data.publicKey?.stateIndex ? Number(data.publicKey.stateIndex) : null
+ if (data.publicKeys.length === 0) {
+ return null
+ }
+
+ return Number(data.publicKeys[0].stateIndex)
}
/**
diff --git a/vue-app/src/api/core.ts b/vue-app/src/api/core.ts
index 02718132b..54383e83a 100644
--- a/vue-app/src/api/core.ts
+++ b/vue-app/src/api/core.ts
@@ -3,6 +3,9 @@ import { ethers } from 'ethers'
import { FundingRoundFactory } from './abi'
import { CHAIN_INFO } from '@/utils/chains'
+import historicalRounds from '@/rounds/rounds.json'
+import { DateTime } from 'luxon'
+
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 +51,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 +74,23 @@ 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 type LeaderboardRound = {
+ address: string
+ network: string
+}
+
+const leaderboardRounds = historicalRounds as LeaderboardRound[]
+export { leaderboardRounds }
+
+export const showComplianceRequirement = /^yes$/i.test(import.meta.env.VITE_SHOW_COMPLIANCE_REQUIREMENT)
+
+export const isBrightIdRequired = userRegistryType === 'brightid'
+export const isOptimisticRecipientRegistry = recipientRegistryType === 'optimistic'
+
+// Try to get the next scheduled start date
+const nextStartDate = import.meta.env.VITE_NEXT_ROUND_START_DATE
+ ? DateTime.fromFormat(import.meta.env.VITE_NEXT_ROUND_START_DATE, 'yyyy-MM-dd')
+ : null
+
+export const nextRoundStartDate = nextStartDate?.isValid ? nextStartDate : null
diff --git a/vue-app/src/api/factory.ts b/vue-app/src/api/factory.ts
index 933774569..32ea5795d 100644
--- a/vue-app/src/api/factory.ts
+++ b/vue-app/src/api/factory.ts
@@ -1,6 +1,6 @@
-import { Contract } from 'ethers'
-import { ERC20 } from './abi'
-import { factory, provider } from './core'
+import { BigNumber } from 'ethers'
+import { factory } from './core'
+import sdk from '@/graphql/sdk'
export interface Factory {
fundingRoundAddress: string
@@ -8,16 +8,42 @@ export interface Factory {
nativeTokenSymbol: string
nativeTokenDecimals: number
userRegistryAddress: string
+ matchingPool: BigNumber
}
export async function getFactoryInfo() {
- const nativeTokenAddress = await factory.nativeToken()
+ let nativeTokenAddress = ''
+ let nativeTokenSymbol = ''
+ let nativeTokenDecimals = 0
+ let matchingPool = BigNumber.from(0)
+ let userRegistryAddress = ''
+ let recipientRegistryAddress = ''
- const nativeToken = new Contract(nativeTokenAddress, ERC20, provider)
- const nativeTokenSymbol = await nativeToken.symbol()
- const nativeTokenDecimals = await nativeToken.decimals()
+ try {
+ const data = await sdk.GetFactoryInfo({
+ factoryAddress: factory.address.toLowerCase(),
+ })
- const userRegistryAddress = await factory.userRegistry()
+ const nativeTokenInfo = data.fundingRoundFactory?.nativeTokenInfo
+ if (nativeTokenInfo) {
+ nativeTokenAddress = nativeTokenInfo.tokenAddress || ''
+ nativeTokenSymbol = nativeTokenInfo.symbol || ''
+ nativeTokenDecimals = Number(nativeTokenInfo.decimals) || 0
+ }
+
+ userRegistryAddress = data.fundingRoundFactory?.contributorRegistryAddress || ''
+ recipientRegistryAddress = data.fundingRoundFactory?.recipientRegistryAddress || ''
+ } catch (err) {
+ /* eslint-disable-next-line no-console */
+ console.error('Failed GetFactoryInfo', err)
+ }
+
+ try {
+ matchingPool = await getMatchingFunds(nativeTokenAddress)
+ } catch (err) {
+ /* eslint-disable-next-line no-console */
+ console.error('Failed to get matching pool', err)
+ }
return {
fundingRoundAddress: factory.address,
@@ -25,5 +51,12 @@ export async function getFactoryInfo() {
nativeTokenSymbol,
nativeTokenDecimals,
userRegistryAddress,
+ recipientRegistryAddress,
+ matchingPool,
}
}
+
+export async function getMatchingFunds(nativeTokenAddress: string): Promise {
+ const matchingFunds = await factory.getMatchingFunds(nativeTokenAddress)
+ return matchingFunds
+}
diff --git a/vue-app/src/api/leaderboard.ts b/vue-app/src/api/leaderboard.ts
new file mode 100644
index 000000000..dff8ac18d
--- /dev/null
+++ b/vue-app/src/api/leaderboard.ts
@@ -0,0 +1,18 @@
+import leaderboardRounds from '@/rounds/rounds.json'
+
+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}.json`) : null
+}
diff --git a/vue-app/src/api/projects.ts b/vue-app/src/api/projects.ts
index 204c16c96..c9cc36b71 100644
--- a/vue-app/src/api/projects.ts
+++ b/vue-app/src/api/projects.ts
@@ -7,6 +7,8 @@ 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'
+import type { RecipientApplicationData } from '@/api/types'
export interface LeaderboardProject {
id: string // Address or another ID depending on registry implementation
@@ -58,6 +60,19 @@ export async function getRecipientRegistryAddress(roundAddress: string | null):
}
}
+export async function getCurrentRecipientRegistryAddress(): Promise {
+ const data = await sdk.GetRecipientRegistryInfo({
+ factoryAddress: factory.address.toLowerCase(),
+ })
+
+ const registryAddress =
+ data.fundingRoundFactory?.currentRound?.recipientRegistry?.id ||
+ data.fundingRoundFactory?.recipientRegistry?.id ||
+ ''
+
+ return registryAddress
+}
+
export async function getProjects(registryAddress: string, startTime?: number, endTime?: number): Promise {
if (recipientRegistryType === 'simple') {
return await SimpleRegistry.getProjects(registryAddress, startTime, endTime)
@@ -70,11 +85,21 @@ export async function getProjects(registryAddress: string, startTime?: number, e
}
}
-export async function getProject(registryAddress: string, recipientId: string): Promise {
+/**
+ * Get project information
+ *
+ * TODO: add subgraph event listener to track recipients from simple and kleros registries
+ *
+ * @param registryAddress recipient registry address
+ * @param recipientId recipient id
+ * @param filter filter result by locked or verified status
+ * @returns project information
+ */
+export async function getProject(registryAddress: string, recipientId: string, filter = true): Promise {
if (recipientRegistryType === 'simple') {
return await SimpleRegistry.getProject(registryAddress, recipientId)
} else if (recipientRegistryType === 'optimistic') {
- return await OptimisticRegistry.getProject(recipientId)
+ return await OptimisticRegistry.getProject(recipientId, filter)
} else if (recipientRegistryType === 'kleros') {
return await KlerosRegistry.getProject(registryAddress, recipientId)
} else {
@@ -111,7 +136,7 @@ export async function getProjectByIndex(
recipientIndex,
})
- if (!result.recipients?.length) {
+ if (!result.recipients.length) {
return null
}
@@ -175,3 +200,61 @@ 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
+ }
+}
+
+export function formToProjectInterface(data: RecipientApplicationData): Project {
+ const { project, fund, team, links, image } = data
+ return {
+ id: fund.resolvedAddress,
+ 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,
+ bannerImageUrl: `${ipfsGatewayUrl}/ipfs/${image.bannerHash}`,
+ thumbnailImageUrl: `${ipfsGatewayUrl}/ipfs/${image.thumbnailHash}`,
+ index: 0,
+ isHidden: false,
+ isLocked: true,
+ }
+}
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..fcfd04a2c 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,
@@ -374,7 +263,14 @@ export async function getProjects(registryAddress: string, startTime?: number, e
return projects
}
-export async function getProject(recipientId: string): Promise {
+/**
+ * Get project information
+ *
+ * @param recipientId recipient id
+ * @param filter default to always filter result by locked or verified status
+ * @returns project
+ */
+export async function getProject(recipientId: string, filter = true): Promise {
if (!isHexString(recipientId, 32)) {
return null
}
@@ -398,6 +294,10 @@ export async function getProject(recipientId: string): Promise {
return null
}
+ if (!filter) {
+ return project
+ }
+
const requestType = Number(recipient.requestType)
if (requestType === RequestTypeCode.Registration) {
if (recipient.verified) {
@@ -444,4 +344,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..8fda10a91 100644
--- a/vue-app/src/api/recipient-registry-simple.ts
+++ b/vue-app/src/api/recipient-registry-simple.ts
@@ -1,10 +1,13 @@
-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
@@ -14,7 +17,19 @@ function decodeRecipientAdded(event: Event): Project {
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 +105,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/round.ts b/vue-app/src/api/round.ts
index 82edac8da..ba6fc3e5c 100644
--- a/vue-app/src/api/round.ts
+++ b/vue-app/src/api/round.ts
@@ -1,15 +1,16 @@
-import { BigNumber, Contract, utils, FixedNumber } from 'ethers'
+import { BigNumber, Contract, utils } 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'
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
@@ -29,11 +30,13 @@ export interface RoundInfo {
startTime: DateTime
signUpDeadline: DateTime
votingDeadline: DateTime
- totalFunds: FixedNumber
- matchingPool: FixedNumber
- contributions: FixedNumber
+ totalFunds: BigNumber
+ matchingPool: BigNumber
+ contributions: BigNumber
contributors: number
messages: number
+ blogUrl?: string
+ network?: string
}
export interface TimeLeft {
@@ -65,19 +68,87 @@ 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,
+ matchingPool,
+ contributions,
+ 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 {
- if (cachedRound && isSameAddress(fundingRoundAddress, cachedRound.fundingRoundAddress)) {
+export async function getRoundInfo(
+ fundingRoundAddress: string,
+ cachedRound?: RoundInfo | null,
+): Promise {
+ const roundAddress = fundingRoundAddress || ''
+ if (cachedRound && isSameAddress(roundAddress, cachedRound.fundingRoundAddress)) {
// the requested round matches the cached round, quick return
return cachedRound
}
const fundingRound = new Contract(fundingRoundAddress, FundingRound, provider)
const data = await sdk.GetRoundInfo({
- fundingRoundAddress: fundingRoundAddress.toLowerCase(),
+ fundingRoundAddress: roundAddress.toLowerCase(),
})
- assert(data.fundingRound, ASSERT_MISSING_ROUND)
+ if (!data.fundingRound) {
+ return null
+ }
const {
maci: maciAddress,
@@ -169,9 +240,9 @@ export async function getRoundInfo(fundingRoundAddress: string, cachedRound?: Ro
startTime,
signUpDeadline,
votingDeadline,
- totalFunds: FixedNumber.fromValue(totalFunds, nativeTokenDecimals),
- matchingPool: FixedNumber.fromValue(matchingPool, nativeTokenDecimals),
- contributions: FixedNumber.fromValue(contributions, nativeTokenDecimals),
+ totalFunds,
+ matchingPool,
+ contributions,
contributors,
messages: messages.toNumber(),
}
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/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.d.ts b/vue-app/src/components.d.ts
index 22c624b03..7ccfb0c87 100644
--- a/vue-app/src/components.d.ts
+++ b/vue-app/src/components.d.ts
@@ -19,6 +19,7 @@ declare module '@vue/runtime-core' {
CartWidget: typeof import('./components/CartWidget.vue')['default']
ClaimButton: typeof import('./components/ClaimButton.vue')['default']
ClaimModal: typeof import('./components/ClaimModal.vue')['default']
+ ComplianceInfo: typeof import('./components/ComplianceInfo.vue')['default']
ContributionModal: typeof import('./components/ContributionModal.vue')['default']
CopyButton: typeof import('./components/CopyButton.vue')['default']
CriteriaModal: typeof import('./components/CriteriaModal.vue')['default']
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..e2cf8e7a8 100644
--- a/vue-app/src/components/Cart.vue
+++ b/vue-app/src/components/Cart.vue
@@ -1,6 +1,11 @@
-
+
+
+
🌚
+
{{ $t('roundInfo.div21') }}
+
+
🌚
{{ $t('cart.h3_1') }}
@@ -217,7 +222,7 @@
diff --git a/vue-app/src/components/ClaimButton.vue b/vue-app/src/components/ClaimButton.vue
index 12f554ec2..6eff26bb5 100644
--- a/vue-app/src/components/ClaimButton.vue
+++ b/vue-app/src/components/ClaimButton.vue
@@ -27,13 +27,12 @@
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(),
diff --git a/vue-app/src/components/CopyButton.vue b/vue-app/src/components/CopyButton.vue
index e174ec67a..a486ed06a 100644
--- a/vue-app/src/components/CopyButton.vue
+++ b/vue-app/src/components/CopyButton.vue
@@ -2,7 +2,6 @@
+
+
🛡️
+
+
+ {{ $t('dynamic.criteria.compliance.tagline') }}
+
+
+
+
{{ $t('criterialModal.link2') }}
@@ -38,6 +47,7 @@
diff --git a/vue-app/src/views/Leaderboard.vue b/vue-app/src/views/Leaderboard.vue
index 610e95b7f..1725cf57d 100644
--- a/vue-app/src/views/Leaderboard.vue
+++ b/vue-app/src/views/Leaderboard.vue
@@ -42,6 +42,8 @@ import { useRouter, useRoute } from 'vue-router'
import type { RoundInfo } from '@/api/round'
import type { LeaderboardProject } from '@/api/projects'
import { toLeaderboardProject } from '@/api/projects'
+import { getLeaderboardData } from '@/api/leaderboard'
+import { getRouteParamValue } from '@/utils/route'
const router = useRouter()
const route = useRoute()
@@ -53,19 +55,24 @@ const projects = ref(null)
const appStore = useAppStore()
const { showSimpleLeaderboard } = storeToRefs(appStore)
-async function loadLeaderboard(address: string) {
- const data = await appStore.getLeaderboardData(address)
+async function loadLeaderboard(address: string, network: string) {
+ const data = await getLeaderboardData(address, network)
return data
}
onMounted(async () => {
- const { address } = route.params
+ if (!route.params.address || !route.params.network) {
+ router.push({ name: 'rounds' })
+ return
+ }
- const data = await loadLeaderboard(address as string)
+ const address = getRouteParamValue(route.params.address)
+ const network = getRouteParamValue(route.params.network)
+ const data = await loadLeaderboard(address, network)
// redirect to projects view if not finalized or no static round data for leaderboard
- if (!data.projects) {
- router.push({ name: 'round', params: route.params })
+ if (!data?.projects) {
+ router.push({ name: 'round' })
return
}
@@ -74,7 +81,7 @@ onMounted(async () => {
.map(project => toLeaderboardProject(project))
.sort((p1: LeaderboardProject, p2: LeaderboardProject) => p2.allocatedAmount.sub(p1.allocatedAmount))
- round.value = { ...data.round, fundingRoundAddress: data.round.address }
+ round.value = { ...data.round, fundingRoundAddress: data.round.address, network }
isLoading.value = false
})
diff --git a/vue-app/src/views/LeaderboardProject.vue b/vue-app/src/views/LeaderboardProject.vue
new file mode 100644
index 000000000..e09423647
--- /dev/null
+++ b/vue-app/src/views/LeaderboardProject.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vue-app/src/views/Profile.vue b/vue-app/src/views/Profile.vue
index 573437c3b..768352a67 100644
--- a/vue-app/src/views/Profile.vue
+++ b/vue-app/src/views/Profile.vue
@@ -131,7 +131,7 @@ onMounted(async () => {
const walletProvider = computed(() => currentUser.value?.walletProvider)
const showBrightIdWidget = computed(
- () => userRegistryType === UserRegistryType.BRIGHT_ID && !hasContributionPhaseEnded.value,
+ () => userRegistryType === UserRegistryType.BRIGHT_ID && currentRound.value && !hasContributionPhaseEnded.value,
)
const tokenLogo = computed(() => getTokenLogo(nativeTokenSymbol.value))
const displayAddress = computed(() => {
diff --git a/vue-app/src/views/Project.vue b/vue-app/src/views/Project.vue
index 1e52fa986..a681aa2cb 100644
--- a/vue-app/src/views/Project.vue
+++ b/vue-app/src/views/Project.vue
@@ -60,7 +60,7 @@ onMounted(async () => {
roundAddress.value = (route.params.address as string) || currentRoundAddress || ''
- const registryAddress = await getRecipientRegistryAddress(roundAddress.value)
+ const registryAddress = await getRecipientRegistryAddress(roundAddress.value || null)
const _project = await getProject(registryAddress, route.params.id as string)
if (_project === null || _project.isHidden) {
// Project not found
diff --git a/vue-app/src/views/ProjectAdded.vue b/vue-app/src/views/ProjectAdded.vue
index a13419c5b..abfbbf3ce 100644
--- a/vue-app/src/views/ProjectAdded.vue
+++ b/vue-app/src/views/ProjectAdded.vue
@@ -23,7 +23,12 @@
- {{ $t('projectAdded.link2') }}
+
+ {{
+ $t('projectAdded.link2')
+ }}
+
+ {{ $t('projectAdded.linkProjects') }}
{{ $t('projectAdded.link3') }}
@@ -43,11 +48,22 @@ import ImageResponsive from '@/components/ImageResponsive.vue'
import { useAppStore } from '@/stores'
import { useRoute } from 'vue-router'
import { storeToRefs } from 'pinia'
+import { isOptimisticRecipientRegistry } from '@/api/core'
+import { getRecipientBySubmitHash } from '@/api/projects'
const route = useRoute()
const appStore = useAppStore()
const { currentRound } = storeToRefs(appStore)
const hash = computed(() => route.params.hash as string)
+
+const recipientId = ref('')
+
+onMounted(async () => {
+ const recipient = await getRecipientBySubmitHash(hash.value)
+ if (recipient) {
+ recipientId.value = recipient.id
+ }
+})