diff --git a/vue-app/src/api/core.ts b/vue-app/src/api/core.ts index b07bb6ef5..1d0e4010f 100644 --- a/vue-app/src/api/core.ts +++ b/vue-app/src/api/core.ts @@ -83,3 +83,6 @@ 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' diff --git a/vue-app/src/api/projects.ts b/vue-app/src/api/projects.ts index 8dea2823d..c9cc36b71 100644 --- a/vue-app/src/api/projects.ts +++ b/vue-app/src/api/projects.ts @@ -8,6 +8,7 @@ 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 @@ -231,3 +232,29 @@ export async function getLeaderboardProject( 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 3b2e83d23..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, @@ -455,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..568814c93 100644 --- a/vue-app/src/api/recipient-registry-simple.ts +++ b/vue-app/src/api/recipient-registry-simple.ts @@ -1,20 +1,36 @@ -import { Contract } from 'ethers' +import { Contract, BigNumber, Signer } from 'ethers' import type { Event } from 'ethers' import { isHexString } from '@ethersproject/bytes' +import type { TransactionResponse } from '@ethersproject/abstract-provider' import { SimpleRecipientRegistry } from './abi' import { provider, ipfsGatewayUrl } from './core' import type { Project } from './projects' +import type { RegistryInfo, RecipientApplicationData } from './types' +import { formToRecipientData } from './recipient' function decodeRecipientAdded(event: Event): Project { const args = event.args as any const metadata = JSON.parse(args._metadata) + console.log('metata', metadata) return { id: args._recipientId, address: args._recipient, name: metadata.name, description: metadata.description, - imageUrl: `${ipfsGatewayUrl}/ipfs/${metadata.imageHash}`, + tagline: metadata.tagline, + category: metadata.category, + problemSpace: metadata.problemSpace, + plans: metadata.plans, + teamName: metadata.teamName, + teamDescription: metadata.teamDescription, + githubUrl: metadata.githubUrl, + radicleUrl: metadata.radicleUrl, + websiteUrl: metadata.websiteUrl, + twitterUrl: metadata.twitterUrl, + discordUrl: metadata.discordUrl, + bannerImageUrl: `${ipfsGatewayUrl}/ipfs/${metadata.bannerImageHash}`, + thumbnailImageUrl: `${ipfsGatewayUrl}/ipfs/${metadata.thumbnailImageHash}`, index: args._index.toNumber(), isHidden: false, isLocked: false, @@ -90,4 +106,40 @@ export async function getProject(registryAddress: string, recipientId: string): return project } -export default { getProjects, getProject } +async function getRegistryInfo(registryAddress: string): Promise { + const registry = new Contract(registryAddress, SimpleRecipientRegistry, provider) + + let recipientCount + try { + recipientCount = await registry.getRecipientCount() + } catch { + // older BaseRecipientRegistry contract did not have recipientCount + // set it to zero as this information is only + // used during current round for space calculation + recipientCount = BigNumber.from(0) + } + const owner = await registry.owner() + + // deposit, depositToken and challengePeriodDuration are only relevant to the optimistic registry + return { + deposit: BigNumber.from(0), + depositToken: '', + challengePeriodDuration: 0, + recipientCount: recipientCount.toNumber(), + owner, + } +} + +async function addRecipient( + registryAddress: string, + recipientApplicationData: RecipientApplicationData, + signer: Signer, +): Promise { + const registry = new Contract(registryAddress, SimpleRecipientRegistry, signer) + const recipientData = formToRecipientData(recipientApplicationData) + const { address, ...metadata } = recipientData + const transaction = await registry.addRecipient(address, JSON.stringify(metadata)) + return transaction +} + +export default { getProjects, getProject, getRegistryInfo, addRecipient } diff --git a/vue-app/src/api/recipient-registry.ts b/vue-app/src/api/recipient-registry.ts new file mode 100644 index 000000000..dd26dea8e --- /dev/null +++ b/vue-app/src/api/recipient-registry.ts @@ -0,0 +1,36 @@ +import type { RegistryInfo, RecipientApplicationData } from './types' +import { recipientRegistryType } from './core' +import SimpleRegistry from './recipient-registry-simple' +import OptimisticRegistry from './recipient-registry-optimistic' +import KlerosRegistry from './recipient-registry-kleros' +import type { BigNumber, Signer } from 'ethers' +import type { TransactionResponse } from '@ethersproject/abstract-provider' + +export async function getRegistryInfo(registryAddress: string): Promise { + if (recipientRegistryType === 'simple') { + return await SimpleRegistry.getRegistryInfo(registryAddress) + } else if (recipientRegistryType === 'optimistic') { + return await OptimisticRegistry.getRegistryInfo(registryAddress) + } else if (recipientRegistryType === 'kleros') { + return await KlerosRegistry.getRegistryInfo(registryAddress) + } else { + throw new Error('Invalid recipient registry type: ' + recipientRegistryType) + } +} + +export async function addRecipient( + registryAddress: string, + recipientApplicationData: RecipientApplicationData, + deposit: BigNumber, + signer: Signer, +): Promise { + if (recipientRegistryType === 'simple') { + return await SimpleRegistry.addRecipient(registryAddress, recipientApplicationData, signer) + } else if (recipientRegistryType === 'optimistic') { + return await OptimisticRegistry.addRecipient(registryAddress, recipientApplicationData, deposit, signer) + } else if (recipientRegistryType === 'kleros') { + throw new Error('Kleros recipient registry is not supported') + } else { + throw new Error('Invalid recipient registry type: ' + recipientRegistryType) + } +} diff --git a/vue-app/src/api/recipient.ts b/vue-app/src/api/recipient.ts new file mode 100644 index 000000000..3b9fd4ef3 --- /dev/null +++ b/vue-app/src/api/recipient.ts @@ -0,0 +1,45 @@ +import type { RecipientApplicationData } from './types' + +// TODO merge this with `Project` inteface +export interface RecipientData { + name: string + description: string + imageHash?: string // TODO remove - old flow + address: string + tagline?: string + category?: string + problemSpace?: string + plans?: string + teamName?: string + teamDescription?: string + githubUrl?: string + radicleUrl?: string + websiteUrl?: string + twitterUrl?: string + discordUrl?: string + // fields different vs. Project + bannerImageHash?: string + thumbnailImageHash?: string +} + +export function formToRecipientData(data: RecipientApplicationData): RecipientData { + const { project, fund, team, links, image } = data + return { + address: fund.resolvedAddress, + name: project.name, + tagline: project.tagline, + description: project.description, + category: project.category, + problemSpace: project.problemSpace, + plans: fund.plans, + teamName: team.name, + teamDescription: team.description, + githubUrl: links.github, + radicleUrl: links.radicle, + websiteUrl: links.website, + twitterUrl: links.twitter, + discordUrl: links.discord, + bannerImageHash: image.bannerHash, + thumbnailImageHash: image.thumbnailHash, + } +} diff --git a/vue-app/src/api/types.ts b/vue-app/src/api/types.ts new file mode 100644 index 000000000..96d3897b5 --- /dev/null +++ b/vue-app/src/api/types.ts @@ -0,0 +1,43 @@ +import type { BigNumber } from 'ethers' + +// Recipient registry info +export interface RegistryInfo { + deposit: BigNumber + depositToken: string + challengePeriodDuration: number + recipientCount: number + owner: string +} + +export interface RecipientApplicationData { + project: { + name: string + tagline: string + description: string + category: string + problemSpace: string + } + fund: { + addressName: string + resolvedAddress: string + plans: string + } + team: { + name: string + description: string + email: string + } + links: { + github: string + radicle: string + website: string + twitter: string + discord: string + } + image: { + bannerHash: string + thumbnailHash: string + } + furthestStep: number + hasEns: boolean +} diff --git a/vue-app/src/components/Cart.vue b/vue-app/src/components/Cart.vue index a9d5e2a2a..4346e814b 100644 --- a/vue-app/src/components/Cart.vue +++ b/vue-app/src/components/Cart.vue @@ -235,7 +235,7 @@ import CartItems from '@/components/CartItems.vue' import Links from '@/components/Links.vue' import TimeLeft from '@/components/TimeLeft.vue' import { MAX_CONTRIBUTION_AMOUNT, MAX_CART_SIZE, type CartItem, isContributionAmountValid } from '@/api/contributions' -import { userRegistryType, UserRegistryType } from '@/api/core' +import { userRegistryType, UserRegistryType, operator } from '@/api/core' import { RoundStatus } from '@/api/round' import { formatAmount as _formatAmount } from '@/utils/amounts' import FundsNeededWarning from '@/components/FundsNeededWarning.vue' @@ -478,6 +478,7 @@ const errorMessage = computed(() => { if (isMessageLimitReached.value) return t('dynamic.cart.error.reached_contribution_limit') if (!currentUser.value) return t('dynamic.cart.error.connect_wallet') if (isBrightIdRequired.value) return t('dynamic.cart.error.need_to_setup_brightid') + if (!currentUser.value.isRegistered) return t('dynamic.cart.error.user_not_registered', { operator }) if (!isFormValid()) return t('dynamic.cart.error.invalid_contribution_amount') if (cart.value.length > MAX_CART_SIZE) return t('dynamic.cart.error.exceeded_max_cart_size', { diff --git a/vue-app/src/components/NavBar.vue b/vue-app/src/components/NavBar.vue index 687cb2727..82b50b3f6 100644 --- a/vue-app/src/components/NavBar.vue +++ b/vue-app/src/components/NavBar.vue @@ -53,7 +53,7 @@

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

-