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 @@ 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.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 + } +})