diff --git a/README.md b/README.md index 4e835db25..f1d077eb7 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,7 @@ to run `yarn test` or `yarn test:contracts`. - [Vue js modal](http://vue-js-modal.yev.io/) - [Ethers](https://docs.ethers.io/v5/) - [Gun](https://gun.eco/docs/) + - [GraphQL Code Generator](https://www.graphql-code-generator.com/docs/guides/vue) - used to generate the [/graphql/API.ts](https://github.com/clrfund/monorepo/blob/develop/vue-app/src/graphql/API.ts) ### Visual Studio Code diff --git a/codegen.yml b/codegen.yml index 2d22ef4ef..77c4d951b 100644 --- a/codegen.yml +++ b/codegen.yml @@ -1,5 +1,5 @@ overwrite: true -schema: 'http://localhost:8000/subgraphs/name/daodesigner/clrfund' +schema: 'https://api.thegraph.com/subgraphs/name/clrfund/clrfund' documents: 'vue-app/src/graphql/**/*.graphql' generates: vue-app/src/graphql/API.ts: diff --git a/contracts/.gitignore b/contracts/.gitignore index 8e5d5e754..63b8494e7 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -6,3 +6,4 @@ state.json proofs.json tally.json .env +.env.* diff --git a/contracts/scripts/deploy.ts b/contracts/scripts/deploy.ts index 9f7dfc93c..49f5ec56c 100644 --- a/contracts/scripts/deploy.ts +++ b/contracts/scripts/deploy.ts @@ -3,6 +3,7 @@ import { Contract, utils } from 'ethers' import { UNIT } from '../utils/constants' import { deployMaciFactory } from '../utils/deployment' +import { RecipientRegistryFactory } from '../utils/recipient-registry-factory' async function main() { const [deployer] = await ethers.getSigners() @@ -59,30 +60,18 @@ async function main() { await setUserRegistryTx.wait() const recipientRegistryType = process.env.RECIPIENT_REGISTRY_TYPE || 'simple' - let recipientRegistry: Contract - if (recipientRegistryType === 'simple') { - const SimpleRecipientRegistry = await ethers.getContractFactory( - 'SimpleRecipientRegistry', - deployer - ) - recipientRegistry = await SimpleRecipientRegistry.deploy( - fundingRoundFactory.address - ) - } else if (recipientRegistryType === 'optimistic') { - const OptimisticRecipientRegistry = await ethers.getContractFactory( - 'OptimisticRecipientRegistry', - deployer - ) - recipientRegistry = await OptimisticRecipientRegistry.deploy( - UNIT.div(1000), - 0, - fundingRoundFactory.address - ) - } else { - throw new Error('unsupported recipient registry type') - } - await recipientRegistry.deployTransaction.wait() - console.log(`Recipient registry deployed: ${recipientRegistry.address}`) + const recipientRegistry = await RecipientRegistryFactory.deploy( + recipientRegistryType, + { + controller: fundingRoundFactory.address, + baseDeposit: UNIT.div(1000), + challengePeriodDuration: 0, + }, + deployer + ) + console.log( + `${recipientRegistryType} recipient registry deployed: ${recipientRegistry.address}` + ) const setRecipientRegistryTx = await fundingRoundFactory.setRecipientRegistry( recipientRegistry.address diff --git a/contracts/scripts/deployRecipientRegistry.ts b/contracts/scripts/deployRecipientRegistry.ts new file mode 100644 index 000000000..582bdbaf2 --- /dev/null +++ b/contracts/scripts/deployRecipientRegistry.ts @@ -0,0 +1,72 @@ +import { ethers } from 'hardhat' +import { UNIT } from '../utils/constants' +import { RecipientRegistryFactory } from '../utils/recipient-registry-factory' + +/* + * Deploy a new recipient registry. + * The following environment variables must be set to run the script + * + * RECIPIENT_REGISTRY_TYPE - default is simple, values can be simple, optimistic + * FUNDING_ROUND_FACTORY_ADDRESS - address of the funding round factory + * WALLET_PRIVATE_KEY - private key of the account that will fund this transaction + * JSONRPC_HTTP_URL - URL to connect to the node + * + * For example, to run the script on rinkeby network: + * From the contracts folder: + * npx hardhat run --network rinkeby scripts/deployRecipientRegistry.ts + * + */ +async function main() { + const recipientRegistryType = process.env.RECIPIENT_REGISTRY_TYPE || 'simple' + const fundingRoundFactoryAddress = process.env.FUNDING_ROUND_FACTORY_ADDRESS + let challengePeriodDuration = '0' + let baseDeposit = '0' + + if (recipientRegistryType === 'optimistic') { + challengePeriodDuration = process.env.CHALLENGE_PERIOD_IN_SECONDS || '300' + baseDeposit = process.env.BASE_DEPOSIT || UNIT.div(10).toString() + } + + if (!fundingRoundFactoryAddress) { + console.log('Environment variable FUNDING_ROUND_FACTORY_ADDRESS not set') + return + } + const fundingRoundFactory = await ethers.getContractAt( + 'FundingRoundFactory', + fundingRoundFactoryAddress + ) + const factoryOwner = await fundingRoundFactory.owner() + + console.log('*******************') + console.log(`Deploying a new ${recipientRegistryType} recipient registry!`) + console.log(` challenge period in seconds: ${challengePeriodDuration}`) + console.log(` baseDeposit: ${baseDeposit}`) + console.log(` fundingRoundFactoryAddress: ${fundingRoundFactoryAddress}`) + console.log(` fundingRoundFactoryOwner: ${factoryOwner}`) + const [deployer] = await ethers.getSigners() + + const recipientRegistry = await RecipientRegistryFactory.deploy( + recipientRegistryType, + { + controller: fundingRoundFactory.address, + baseDeposit, + challengePeriodDuration, + }, + deployer + ) + console.log(` recipientRegistry address: ${recipientRegistry.address}`) + + const setRecipientRegistryTx = await fundingRoundFactory.setRecipientRegistry( + recipientRegistry.address + ) + + await setRecipientRegistryTx.wait() + console.log('*******************') +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/contracts/scripts/deployRound.ts b/contracts/scripts/deployRound.ts index ebd71e53d..a14745308 100644 --- a/contracts/scripts/deployRound.ts +++ b/contracts/scripts/deployRound.ts @@ -3,8 +3,9 @@ import { Contract, utils } from 'ethers' import { Keypair } from 'maci-domainobjs' import { deployMaciFactory } from '../utils/deployment' -import { getEventArg } from '../utils/contracts' import { MaciParameters } from '../utils/maci' +import { RecipientRegistryFactory } from '../utils/recipient-registry-factory' +import { RecipientRegistryLoader } from '../utils/recipient-registry-loader' async function main() { console.log('*******************') @@ -63,30 +64,20 @@ async function main() { await setUserRegistryTx.wait() const recipientRegistryType = process.env.RECIPIENT_REGISTRY_TYPE || 'simple' - let recipientRegistry: Contract - if (recipientRegistryType === 'simple') { - const SimpleRecipientRegistry = await ethers.getContractFactory( - 'SimpleRecipientRegistry', - deployer - ) - recipientRegistry = await SimpleRecipientRegistry.deploy( - fundingRoundFactory.address - ) - } else if (recipientRegistryType === 'optimistic') { - const OptimisticRecipientRegistry = await ethers.getContractFactory( - 'OptimisticRecipientRegistry', - deployer - ) - recipientRegistry = await OptimisticRecipientRegistry.deploy( - ethers.BigNumber.from(10).pow(ethers.BigNumber.from(18)).div(10), - 300, - fundingRoundFactory.address - ) - } else { - throw new Error('unsupported recipient registry type') - } - await recipientRegistry.deployTransaction.wait() - console.log('recipientRegistry.address: ', recipientRegistry.address) + const recipientRegistry = await RecipientRegistryFactory.deploy( + recipientRegistryType, + { + controller: fundingRoundFactory.address, + baseDeposit: ethers.BigNumber.from(10) + .pow(ethers.BigNumber.from(18)) + .div(10), + challengePeriodDuration: 300, + }, + deployer + ) + console.log( + `${recipientRegistryType} recipientRegistry address: ${recipientRegistry.address}` + ) const setRecipientRegistryTx = await fundingRoundFactory.setRecipientRegistry( recipientRegistry.address @@ -122,47 +113,6 @@ async function main() { ) await setMaciParametersTx.wait() - const metadataRecipient1 = { - name: 'Commons Simulator', - description: - 'Funding open-source projects & other public goods is the killer app of blockchain tech. Giveth & BlockScience are joining forces to build the Commons Stack: a modular library of well engineered components that can be used to create economic models for projects that are creating value, yet have trouble finding sustainable business models.', - imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', - tagline: 'Modeling Sustainable Funding for Public Good', - category: 'Data', - problemSpace: 'metadata.problemSpace', - plans: 'metadata.plans', - teamName: 'metadata.teamName', - teamDescription: 'metadata.teamDescription', - githubUrl: 'https://github.com/', - radicleUrl: 'https://radicle.xyz/', - websiteUrl: 'https://website.com/', - twitterUrl: 'https://twitter.com/', - discordUrl: 'https://discord.com/', - bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - } - - const metadataRecipient2 = { - name: 'Synthereum', - description: - 'The aim of our synthetic assets is to help creating fiat-based wallet and applications on any local currencies, and help to create stock, commodities portfolio in order to bring more traditional users within the DeFi ecosystem.', - imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', - tagline: - 'Synthetic assets with liquidity pools to bridge traditional and digital finance.', - category: 'Content', - problemSpace: 'metadata.problemSpace', - plans: 'metadata.plans', - teamName: 'metadata.teamName', - teamDescription: 'metadata.teamDescription', - githubUrl: 'https://github.com/', - radicleUrl: 'https://radicle.xyz/', - websiteUrl: 'https://website.com/', - twitterUrl: 'https://twitter.com/', - discordUrl: 'https://discord.com/', - bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - } - const addFundingSourceTx = await fundingRoundFactory.addFundingSource( deployer.address ) @@ -172,79 +122,43 @@ async function main() { const deployNewRoundTx = await fundingRoundFactory.deployNewRound() await deployNewRoundTx.wait() - if (recipientRegistryType === 'simple') { - const recipients = [ - { account: deployer.address, metadata: metadataRecipient1 }, - { account: deployer.address, metadata: metadataRecipient2 }, - ] - let addRecipientTx - for (const recipient of recipients) { - addRecipientTx = await recipientRegistry.addRecipient( - deployer.address, - JSON.stringify(recipient.metadata) - ) - addRecipientTx.wait() - } - } else if (recipientRegistryType === 'optimistic') { - const deposit = await recipientRegistry.baseDeposit() - const recipient1Added = await recipientRegistry.addRecipient( - deployer.address, - JSON.stringify(metadataRecipient1), - { value: deposit } - ) - await recipient1Added.wait() - - const recipient1Id = await getEventArg( - recipient1Added, - recipientRegistry, - 'RequestSubmitted', - '_recipientId' - ) - const executeRequest1 = await recipientRegistry.executeRequest(recipient1Id) - await executeRequest1.wait() - - const recipient2Added = await recipientRegistry.addRecipient( - deployer.address, - JSON.stringify(metadataRecipient2), - { value: deposit } - ) - await recipient2Added.wait() + const recipients = RecipientRegistryLoader.buildStubRecipients([ + deployer.address, + deployer.address, + ]) - const recipient2Id = await getEventArg( - recipient2Added, - recipientRegistry, - 'RequestSubmitted', - '_recipientId' - ) - const executeRequest2 = await recipientRegistry.executeRequest(recipient2Id) - await executeRequest2.wait() + // add recipients to registry + await RecipientRegistryLoader.load( + recipientRegistryType, + recipientRegistry, + recipients + ) - const fundingRoundAddress = await fundingRoundFactory.getCurrentRound() - console.log('fundingRound.address: ', fundingRoundAddress) + const fundingRoundAddress = await fundingRoundFactory.getCurrentRound() + console.log('fundingRound.address: ', fundingRoundAddress) - const fundingRound = await ethers.getContractAt( - 'FundingRound', - fundingRoundAddress + const fundingRound = await ethers.getContractAt( + 'FundingRound', + fundingRoundAddress + ) + const maciAddress = await fundingRound.maci() + console.log('maci.address: ', maciAddress) + + if (userRegistryType === 'brightid') { + const maci = await ethers.getContractAt('MACI', maciAddress) + const startTime = await maci.signUpTimestamp() + const endTime = await maci.calcSignUpDeadline() + const periodTx = await userRegistry.setRegistrationPeriod( + startTime, + endTime ) - const maciAddress = await fundingRound.maci() - console.log('maci.address: ', maciAddress) - - if (userRegistryType === 'brightid') { - const maci = await ethers.getContractAt('MACI', maciAddress) - const startTime = await maci.signUpTimestamp() - const endTime = await maci.calcSignUpDeadline() - const periodTx = await userRegistry.setRegistrationPeriod( - startTime, - endTime - ) - console.log('Set user registration period', periodTx.hash) - await periodTx.wait() - } - - console.log('*******************') - console.log('Deploy complete!') - console.log('*******************') + console.log('Set user registration period', periodTx.hash) + await periodTx.wait() } + + console.log('*******************') + console.log('Deploy complete!') + console.log('*******************') } main() diff --git a/contracts/scripts/deployTestRound.ts b/contracts/scripts/deployTestRound.ts index e3b05c4e7..39b3753c2 100644 --- a/contracts/scripts/deployTestRound.ts +++ b/contracts/scripts/deployTestRound.ts @@ -3,8 +3,8 @@ import { ethers } from 'hardhat' import { Keypair } from 'maci-domainobjs' import { UNIT } from '../utils/constants' -import { getEventArg } from '../utils/contracts' import { MaciParameters } from '../utils/maci' +import { RecipientRegistryLoader } from '../utils/recipient-registry-loader' async function main() { // We're hardcoding factory address due to a buidler limitation: @@ -57,6 +57,7 @@ async function main() { // Configure MACI factory const maciFactoryAddress = await factory.maciFactory() + console.log('MACIFactory', maciFactoryAddress) const maciFactory = await ethers.getContractAt( 'MACIFactory', maciFactoryAddress @@ -96,186 +97,6 @@ async function main() { } } - // Add dummy recipients - // TODO add better dummy data - const metadataRecipient1 = { - name: 'Commons Simulator', - description: - 'Funding open-source projects & other public goods is the killer app of blockchain tech. Giveth & BlockScience are joining forces to build the Commons Stack: a modular library of well engineered components that can be used to create economic models for projects that are creating value, yet have trouble finding sustainable business models.', - imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', - tagline: 'Modeling Sustainable Funding for Public Good', - category: 'Data', - problemSpace: 'metadata.problemSpace', - plans: 'metadata.plans', - teamName: 'metadata.teamName', - teamDescription: 'metadata.teamDescription', - githubUrl: 'https://github.com/', - radicleUrl: 'https://radicle.xyz/', - websiteUrl: 'https://website.com/', - twitterUrl: 'https://twitter.com/', - discordUrl: 'https://discord.com/', - bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - } - - const metadataRecipient2 = { - name: 'Synthereum', - description: - 'The aim of our synthetic assets is to help creating fiat-based wallet and applications on any local currencies, and help to create stock, commodities portfolio in order to bring more traditional users within the DeFi ecosystem.', - imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', - tagline: - 'Synthetic assets with liquidity pools to bridge traditional and digital finance.', - category: 'Content', - problemSpace: 'metadata.problemSpace', - plans: 'metadata.plans', - teamName: 'metadata.teamName', - teamDescription: 'metadata.teamDescription', - githubUrl: 'https://github.com/', - radicleUrl: 'https://radicle.xyz/', - websiteUrl: 'https://website.com/', - twitterUrl: 'https://twitter.com/', - discordUrl: 'https://discord.com/', - bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - } - - const metadataRecipient3 = { - name: 'Commons Simulator', - description: - 'Funding open-source projects & other public goods is the killer app of blockchain tech. Giveth & BlockScience are joining forces to build the Commons Stack: a modular library of well engineered components that can be used to create economic models for projects that are creating value, yet have trouble finding sustainable business models.', - imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', - tagline: 'Modeling Sustainable Funding for Public Good', - category: 'Data', - problemSpace: 'metadata.problemSpace', - plans: 'metadata.plans', - teamName: 'metadata.teamName', - teamDescription: 'metadata.teamDescription', - githubUrl: 'https://github.com/', - radicleUrl: 'https://radicle.xyz/', - websiteUrl: 'https://website.com/', - twitterUrl: 'https://twitter.com/', - discordUrl: 'https://discord.com/', - bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - } - const metadataRecipient4 = { - name: 'Synthereum', - description: - 'The aim of our synthetic assets is to help creating fiat-based wallet and applications on any local currencies, and help to create stock, commodities portfolio in order to bring more traditional users within the DeFi ecosystem.', - imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', - tagline: - 'Synthetic assets with liquidity pools to bridge traditional and digital finance.', - category: 'Content', - problemSpace: 'metadata.problemSpace', - plans: 'metadata.plans', - teamName: 'metadata.teamName', - teamDescription: 'metadata.teamDescription', - githubUrl: 'https://github.com/', - radicleUrl: 'https://radicle.xyz/', - websiteUrl: 'https://website.com/', - twitterUrl: 'https://twitter.com/', - discordUrl: 'https://discord.com/', - bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - } - const metadataRecipient5 = { - name: 'Commons Simulator', - description: - 'Funding open-source projects & other public goods is the killer app of blockchain tech. Giveth & BlockScience are joining forces to build the Commons Stack: a modular library of well engineered components that can be used to create economic models for projects that are creating value, yet have trouble finding sustainable business models.', - imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', - tagline: 'Modeling Sustainable Funding for Public Good', - category: 'Data', - problemSpace: 'metadata.problemSpace', - plans: 'metadata.plans', - teamName: 'metadata.teamName', - teamDescription: 'metadata.teamDescription', - githubUrl: 'https://github.com/', - radicleUrl: 'https://radicle.xyz/', - websiteUrl: 'https://website.com/', - twitterUrl: 'https://twitter.com/', - discordUrl: 'https://discord.com/', - bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - } - const metadataRecipient6 = { - name: 'Synthereum', - description: - 'The aim of our synthetic assets is to help creating fiat-based wallet and applications on any local currencies, and help to create stock, commodities portfolio in order to bring more traditional users within the DeFi ecosystem.', - imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', - tagline: - 'Synthetic assets with liquidity pools to bridge traditional and digital finance.', - category: 'Content', - problemSpace: 'metadata.problemSpace', - plans: 'metadata.plans', - teamName: 'metadata.teamName', - teamDescription: 'metadata.teamDescription', - githubUrl: 'https://github.com/', - radicleUrl: 'https://radicle.xyz/', - websiteUrl: 'https://website.com/', - twitterUrl: 'https://twitter.com/', - discordUrl: 'https://discord.com/', - bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - } - const metadataRecipient7 = { - name: 'Commons Simulator', - description: - 'Funding open-source projects & other public goods is the killer app of blockchain tech. Giveth & BlockScience are joining forces to build the Commons Stack: a modular library of well engineered components that can be used to create economic models for projects that are creating value, yet have trouble finding sustainable business models.', - imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', - tagline: 'Modeling Sustainable Funding for Public Good', - category: 'Data', - problemSpace: 'metadata.problemSpace', - plans: 'metadata.plans', - teamName: 'metadata.teamName', - teamDescription: 'metadata.teamDescription', - githubUrl: 'https://github.com/', - radicleUrl: 'https://radicle.xyz/', - websiteUrl: 'https://website.com/', - twitterUrl: 'https://twitter.com/', - discordUrl: 'https://discord.com/', - bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - } - const metadataRecipient8 = { - name: 'Synthereum', - description: - 'The aim of our synthetic assets is to help creating fiat-based wallet and applications on any local currencies, and help to create stock, commodities portfolio in order to bring more traditional users within the DeFi ecosystem.', - imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', - tagline: - 'Synthetic assets with liquidity pools to bridge traditional and digital finance.', - category: 'Content', - problemSpace: 'metadata.problemSpace', - plans: 'metadata.plans', - teamName: 'metadata.teamName', - teamDescription: 'metadata.teamDescription', - githubUrl: 'https://github.com/', - radicleUrl: 'https://radicle.xyz/', - websiteUrl: 'https://website.com/', - twitterUrl: 'https://twitter.com/', - discordUrl: 'https://discord.com/', - bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - } - const metadataRecipient9 = { - name: 'Commons Simulator', - description: - 'Funding open-source projects & other public goods is the killer app of blockchain tech. Giveth & BlockScience are joining forces to build the Commons Stack: a modular library of well engineered components that can be used to create economic models for projects that are creating value, yet have trouble finding sustainable business models.', - imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', - tagline: 'Modeling Sustainable Funding for Public Good', - category: 'Data', - problemSpace: 'metadata.problemSpace', - plans: 'metadata.plans', - teamName: 'metadata.teamName', - teamDescription: 'metadata.teamDescription', - githubUrl: 'https://github.com/', - radicleUrl: 'https://radicle.xyz/', - websiteUrl: 'https://website.com/', - twitterUrl: 'https://twitter.com/', - discordUrl: 'https://discord.com/', - bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', - } - // Deploy new funding round and MACI const deployNewRoundTx = await factory.deployNewRound() await deployNewRoundTx.wait() @@ -306,77 +127,51 @@ async function main() { } const recipientRegistryType = process.env.RECIPIENT_REGISTRY_TYPE || 'simple' + const RecipientRegistryName: Record = { + simple: 'SimpleRecipientRegistry', + optimistic: 'OptimisticRecipientRegistry', + } const recipientRegistryAddress = await factory.recipientRegistry() - if (recipientRegistryType === 'simple') { - const recipientRegistry = await ethers.getContractAt( - 'SimpleRecipientRegistry', - recipientRegistryAddress + const recipientRegistryName = RecipientRegistryName[recipientRegistryType] + if (!recipientRegistryName) { + throw new Error( + `unsupported recipient registry type: ${recipientRegistryType}` ) - const recipients = [ - { account: recipient1, metadata: metadataRecipient1 }, - { account: recipient2, metadata: metadataRecipient2 }, - { account: recipient3, metadata: metadataRecipient3 }, - { account: recipient4, metadata: metadataRecipient4 }, - { account: recipient5, metadata: metadataRecipient5 }, - { account: recipient6, metadata: metadataRecipient6 }, - { account: recipient7, metadata: metadataRecipient7 }, - { account: recipient8, metadata: metadataRecipient8 }, - { account: recipient9, metadata: metadataRecipient9 }, - ] - let addRecipientTx - for (const recipient of recipients) { - addRecipientTx = await recipientRegistry.addRecipient( - recipient.account.getAddress(), - JSON.stringify(recipient.metadata) - ) - addRecipientTx.wait() - } - } else if (recipientRegistryType === 'optimistic') { - const recipientRegistry = await ethers.getContractAt( - 'OptimisticRecipientRegistry', - recipientRegistryAddress - ) - const deposit = await recipientRegistry.baseDeposit() - const recipient1Added = await recipientRegistry.addRecipient( - recipient1.getAddress(), - JSON.stringify(metadataRecipient1), - { value: deposit } - ) - await recipient1Added.wait() - - const recipient1Id = await getEventArg( - recipient1Added, - recipientRegistry, - 'RequestSubmitted', - '_recipientId' - ) - const executeRequest1 = await recipientRegistry.executeRequest(recipient1Id) - await executeRequest1.wait() - - const recipient2Added = await recipientRegistry.addRecipient( - recipient2.getAddress(), - JSON.stringify(metadataRecipient2), - { value: deposit } - ) - await recipient2Added.wait() + } + const recipientRegistry = await ethers.getContractAt( + recipientRegistryName, + recipientRegistryAddress + ) - const recipient2Id = await getEventArg( - recipient2Added, - recipientRegistry, - 'RequestSubmitted', - '_recipientId' - ) - const executeRequest2 = await recipientRegistry.executeRequest(recipient2Id) - await executeRequest2.wait() + // Add dummy recipients + let recipients + if (recipientRegistryType === 'simple') { + recipients = RecipientRegistryLoader.buildStubRecipients([ + recipient1.address, + recipient2.address, + recipient3.address, + recipient4.address, + recipient5.address, + recipient6.address, + recipient7.address, + recipient8.address, + recipient9.address, + ]) + } else { + recipients = RecipientRegistryLoader.buildStubRecipients([ + recipient1.address, + recipient2.address, + recipient3.address, + ]) // Add recipient without executing - const recipient3Added = await recipientRegistry.addRecipient( - recipient3.getAddress(), - JSON.stringify(metadataRecipient3), - { value: deposit } - ) - recipient3Added.wait() + recipients[2].skipExecution = true } + await RecipientRegistryLoader.load( + recipientRegistryType, + recipientRegistry, + recipients + ) // Save the current state of the round fs.writeFileSync( diff --git a/contracts/scripts/deployToken.ts b/contracts/scripts/deployToken.ts new file mode 100644 index 000000000..b0f6e4dde --- /dev/null +++ b/contracts/scripts/deployToken.ts @@ -0,0 +1,20 @@ +import { ethers } from 'hardhat' +import { UNIT } from '../utils/constants' + +async function main() { + const [deployer] = await ethers.getSigners() + + // Deploy ERC20 token contract + const Token = await ethers.getContractFactory('AnyOldERC20Token', deployer) + const tokenInitialSupply = UNIT.mul(1000) + const token = await Token.deploy(tokenInitialSupply) + await token.deployTransaction.wait() + console.log(`Token deployed: ${token.address}`) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/contracts/utils/recipient-registry-factory/index.ts b/contracts/utils/recipient-registry-factory/index.ts new file mode 100644 index 000000000..d65704165 --- /dev/null +++ b/contracts/utils/recipient-registry-factory/index.ts @@ -0,0 +1,30 @@ +import { Contract, Signer } from 'ethers' +import { SimpleRecipientRegistryFactory } from './simple' +import { OptimisticRecipientRegistryFactory } from './optimistic' +import { RecipientRegistryConstructorArgs } from './types' + +// Map of recipient registry type to the deployment function +const RegistryFactoryMap: Record = { + simple: SimpleRecipientRegistryFactory.deploy, + optimistic: OptimisticRecipientRegistryFactory.deploy, +} + +/** + * Recipient Registry Factory to deploy + */ +export class RecipientRegistryFactory { + static async deploy( + registryType: string, + args: RecipientRegistryConstructorArgs, + signer: Signer + ): Promise { + const factory = RegistryFactoryMap[registryType] + if (!factory) { + throw new Error('unsupported recipient registry type') + } + + const registry = await factory(args, signer) + await registry.deployTransaction.wait() + return registry + } +} diff --git a/contracts/utils/recipient-registry-factory/optimistic.ts b/contracts/utils/recipient-registry-factory/optimistic.ts new file mode 100644 index 000000000..ef104a780 --- /dev/null +++ b/contracts/utils/recipient-registry-factory/optimistic.ts @@ -0,0 +1,30 @@ +import { ethers } from 'hardhat' +import { Contract, Signer } from 'ethers' +import { RecipientRegistryConstructorArgs } from './types' + +export class OptimisticRecipientRegistryFactory { + static async deploy( + args: RecipientRegistryConstructorArgs, + signer: Signer + ): Promise { + if (args.baseDeposit === undefined) { + throw new Error('missing base deposit ') + } + if (args.challengePeriodDuration === undefined) { + throw new Error('missing challenge period duration') + } + + const OptimisticRecipientRegistry = await ethers.getContractFactory( + 'OptimisticRecipientRegistry', + signer + ) + + const recipientRegistry = await OptimisticRecipientRegistry.deploy( + args.baseDeposit, + args.challengePeriodDuration, + args.controller + ) + + return recipientRegistry + } +} diff --git a/contracts/utils/recipient-registry-factory/simple.ts b/contracts/utils/recipient-registry-factory/simple.ts new file mode 100644 index 000000000..eb0b0b45f --- /dev/null +++ b/contracts/utils/recipient-registry-factory/simple.ts @@ -0,0 +1,21 @@ +import { ethers } from 'hardhat' +import { Contract, Signer } from 'ethers' +import { RecipientRegistryConstructorArgs } from './types' + +export class SimpleRecipientRegistryFactory { + static async deploy( + args: RecipientRegistryConstructorArgs, + signer: Signer + ): Promise { + const SimpleRecipientRegistry = await ethers.getContractFactory( + 'SimpleRecipientRegistry', + signer + ) + + const recipientRegistry = await SimpleRecipientRegistry.deploy( + args.controller + ) + + return recipientRegistry + } +} diff --git a/contracts/utils/recipient-registry-factory/types.ts b/contracts/utils/recipient-registry-factory/types.ts new file mode 100644 index 000000000..9c9137183 --- /dev/null +++ b/contracts/utils/recipient-registry-factory/types.ts @@ -0,0 +1,8 @@ +import { BigNumberish } from 'ethers' + +// Recipient Registry Contructor Arguments +export type RecipientRegistryConstructorArgs = { + controller: string + baseDeposit?: BigNumberish + challengePeriodDuration?: BigNumberish +} diff --git a/contracts/utils/recipient-registry-loader/index.ts b/contracts/utils/recipient-registry-loader/index.ts new file mode 100644 index 000000000..79457ddbf --- /dev/null +++ b/contracts/utils/recipient-registry-loader/index.ts @@ -0,0 +1,29 @@ +import { Contract } from 'ethers' +import { Recipient } from './types' +import { OptimisticRecipientRegistryLoader } from './optimistic' +import { SimpleRecipientRegistryLoader } from './simple' +import { TestMetadata } from '../test-metadata' + +const LoaderMap: Record = { + simple: SimpleRecipientRegistryLoader.load, + optimistic: OptimisticRecipientRegistryLoader.load, +} + +export class RecipientRegistryLoader { + static async load( + registryType: string, + registry: Contract, + recipients: Recipient[] + ) { + const loader = LoaderMap[registryType] + if (loader) { + await loader(registry, recipients) + } + } + + static buildStubRecipients(recipients: string[]): Recipient[] { + return recipients.map((recipient, i) => { + return { address: recipient, metadata: TestMetadata[i] } + }) + } +} diff --git a/contracts/utils/recipient-registry-loader/optimistic.ts b/contracts/utils/recipient-registry-loader/optimistic.ts new file mode 100644 index 000000000..ebc21dfd3 --- /dev/null +++ b/contracts/utils/recipient-registry-loader/optimistic.ts @@ -0,0 +1,32 @@ +import { Recipient } from './types' +import { Contract } from 'ethers' +import { getEventArg } from '../contracts' + +export class OptimisticRecipientRegistryLoader { + static async load(registry: Contract, recipients: Recipient[]) { + for (const recipient of recipients) { + if (!recipient.metadata) { + throw new Error('missing metadata') + } + + const deposit = await registry.baseDeposit() + const recipientAdded = await registry.addRecipient( + recipient.address, + JSON.stringify(recipient.metadata), + { value: deposit } + ) + await recipientAdded.wait() + + if (!!recipient.skipExecution) { + const recipientId = await getEventArg( + recipientAdded, + registry, + 'RequestSubmitted', + '_recipientId' + ) + const executeRequest = await registry.executeRequest(recipientId) + await executeRequest.wait() + } + } + } +} diff --git a/contracts/utils/recipient-registry-loader/simple.ts b/contracts/utils/recipient-registry-loader/simple.ts new file mode 100644 index 000000000..732ed056e --- /dev/null +++ b/contracts/utils/recipient-registry-loader/simple.ts @@ -0,0 +1,14 @@ +import { Recipient } from './types' +import { Contract } from 'ethers' + +export class SimpleRecipientRegistryLoader { + static async load(registry: Contract, recipients: Recipient[]) { + for (const recipient of recipients) { + const tx = await registry.addRecipient( + recipient.address, + JSON.stringify(recipient.metadata) + ) + await tx.wait() + } + } +} diff --git a/contracts/utils/recipient-registry-loader/types.ts b/contracts/utils/recipient-registry-loader/types.ts new file mode 100644 index 000000000..491c9128e --- /dev/null +++ b/contracts/utils/recipient-registry-loader/types.ts @@ -0,0 +1,6 @@ +// Recipient input for the recipient registry loader +export type Recipient = { + address: string + metadata?: any + skipExecution?: boolean +} diff --git a/contracts/utils/test-metadata.ts b/contracts/utils/test-metadata.ts new file mode 100644 index 000000000..d2fe3ddb8 --- /dev/null +++ b/contracts/utils/test-metadata.ts @@ -0,0 +1,177 @@ +export const TestMetadata = [ + { + name: 'Commons Simulator', + description: + 'Funding open-source projects & other public goods is the killer app of blockchain tech. Giveth & BlockScience are joining forces to build the Commons Stack: a modular library of well engineered components that can be used to create economic models for projects that are creating value, yet have trouble finding sustainable business models.', + imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', + tagline: 'Modeling Sustainable Funding for Public Good', + category: 'Data', + problemSpace: 'metadata.problemSpace', + plans: 'metadata.plans', + teamName: 'metadata.teamName', + teamDescription: 'metadata.teamDescription', + githubUrl: 'https://github.com/', + radicleUrl: 'https://radicle.xyz/', + websiteUrl: 'https://website.com/', + twitterUrl: 'https://twitter.com/', + discordUrl: 'https://discord.com/', + bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + }, + { + name: 'Synthereum', + description: + 'The aim of our synthetic assets is to help creating fiat-based wallet and applications on any local currencies, and help to create stock, commodities portfolio in order to bring more traditional users within the DeFi ecosystem.', + imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', + tagline: + 'Synthetic assets with liquidity pools to bridge traditional and digital finance.', + category: 'Content', + problemSpace: 'metadata.problemSpace', + plans: 'metadata.plans', + teamName: 'metadata.teamName', + teamDescription: 'metadata.teamDescription', + githubUrl: 'https://github.com/', + radicleUrl: 'https://radicle.xyz/', + websiteUrl: 'https://website.com/', + twitterUrl: 'https://twitter.com/', + discordUrl: 'https://discord.com/', + bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + }, + { + name: 'Commons Simulator', + description: + 'Funding open-source projects & other public goods is the killer app of blockchain tech. Giveth & BlockScience are joining forces to build the Commons Stack: a modular library of well engineered components that can be used to create economic models for projects that are creating value, yet have trouble finding sustainable business models.', + imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', + tagline: 'Modeling Sustainable Funding for Public Good', + category: 'Data', + problemSpace: 'metadata.problemSpace', + plans: 'metadata.plans', + teamName: 'metadata.teamName', + teamDescription: 'metadata.teamDescription', + githubUrl: 'https://github.com/', + radicleUrl: 'https://radicle.xyz/', + websiteUrl: 'https://website.com/', + twitterUrl: 'https://twitter.com/', + discordUrl: 'https://discord.com/', + bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + }, + { + name: 'Synthereum', + description: + 'The aim of our synthetic assets is to help creating fiat-based wallet and applications on any local currencies, and help to create stock, commodities portfolio in order to bring more traditional users within the DeFi ecosystem.', + imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', + tagline: + 'Synthetic assets with liquidity pools to bridge traditional and digital finance.', + category: 'Content', + problemSpace: 'metadata.problemSpace', + plans: 'metadata.plans', + teamName: 'metadata.teamName', + teamDescription: 'metadata.teamDescription', + githubUrl: 'https://github.com/', + radicleUrl: 'https://radicle.xyz/', + websiteUrl: 'https://website.com/', + twitterUrl: 'https://twitter.com/', + discordUrl: 'https://discord.com/', + bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + }, + { + name: 'Commons Simulator', + description: + 'Funding open-source projects & other public goods is the killer app of blockchain tech. Giveth & BlockScience are joining forces to build the Commons Stack: a modular library of well engineered components that can be used to create economic models for projects that are creating value, yet have trouble finding sustainable business models.', + imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', + tagline: 'Modeling Sustainable Funding for Public Good', + category: 'Data', + problemSpace: 'metadata.problemSpace', + plans: 'metadata.plans', + teamName: 'metadata.teamName', + teamDescription: 'metadata.teamDescription', + githubUrl: 'https://github.com/', + radicleUrl: 'https://radicle.xyz/', + websiteUrl: 'https://website.com/', + twitterUrl: 'https://twitter.com/', + discordUrl: 'https://discord.com/', + bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + }, + { + name: 'Synthereum', + description: + 'The aim of our synthetic assets is to help creating fiat-based wallet and applications on any local currencies, and help to create stock, commodities portfolio in order to bring more traditional users within the DeFi ecosystem.', + imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', + tagline: + 'Synthetic assets with liquidity pools to bridge traditional and digital finance.', + category: 'Content', + problemSpace: 'metadata.problemSpace', + plans: 'metadata.plans', + teamName: 'metadata.teamName', + teamDescription: 'metadata.teamDescription', + githubUrl: 'https://github.com/', + radicleUrl: 'https://radicle.xyz/', + websiteUrl: 'https://website.com/', + twitterUrl: 'https://twitter.com/', + discordUrl: 'https://discord.com/', + bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + }, + { + name: 'Commons Simulator', + description: + 'Funding open-source projects & other public goods is the killer app of blockchain tech. Giveth & BlockScience are joining forces to build the Commons Stack: a modular library of well engineered components that can be used to create economic models for projects that are creating value, yet have trouble finding sustainable business models.', + imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', + tagline: 'Modeling Sustainable Funding for Public Good', + category: 'Data', + problemSpace: 'metadata.problemSpace', + plans: 'metadata.plans', + teamName: 'metadata.teamName', + teamDescription: 'metadata.teamDescription', + githubUrl: 'https://github.com/', + radicleUrl: 'https://radicle.xyz/', + websiteUrl: 'https://website.com/', + twitterUrl: 'https://twitter.com/', + discordUrl: 'https://discord.com/', + bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + }, + { + name: 'Synthereum', + description: + 'The aim of our synthetic assets is to help creating fiat-based wallet and applications on any local currencies, and help to create stock, commodities portfolio in order to bring more traditional users within the DeFi ecosystem.', + imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', + tagline: + 'Synthetic assets with liquidity pools to bridge traditional and digital finance.', + category: 'Content', + problemSpace: 'metadata.problemSpace', + plans: 'metadata.plans', + teamName: 'metadata.teamName', + teamDescription: 'metadata.teamDescription', + githubUrl: 'https://github.com/', + radicleUrl: 'https://radicle.xyz/', + websiteUrl: 'https://website.com/', + twitterUrl: 'https://twitter.com/', + discordUrl: 'https://discord.com/', + bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + }, + { + name: 'Commons Simulator', + description: + 'Funding open-source projects & other public goods is the killer app of blockchain tech. Giveth & BlockScience are joining forces to build the Commons Stack: a modular library of well engineered components that can be used to create economic models for projects that are creating value, yet have trouble finding sustainable business models.', + imageHash: 'QmbMP2fMiy6ek5uQZaxG3bzT9gSqMWxpdCUcQg1iSeEFMU', + tagline: 'Modeling Sustainable Funding for Public Good', + category: 'Data', + problemSpace: 'metadata.problemSpace', + plans: 'metadata.plans', + teamName: 'metadata.teamName', + teamDescription: 'metadata.teamDescription', + githubUrl: 'https://github.com/', + radicleUrl: 'https://radicle.xyz/', + websiteUrl: 'https://website.com/', + twitterUrl: 'https://twitter.com/', + discordUrl: 'https://discord.com/', + bannerImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + thumbnailImageHash: 'QmaDy75RkRVtZcbYeqMDLcCK8dDvahfik68zP7FbpxvD2F', + }, +] diff --git a/package.json b/package.json index e82c61575..990c93cff 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ ] }, "scripts": { - "build": "yarn workspaces run build", + "build": "yarn workspace @clrfund/subgraph run codegen && yarn workspaces run build", "build:contracts": "yarn workspace @clrfund/contracts run build", "build:web": "yarn workspace @clrfund/vue-app run build", "build:subgraph": "yarn workspace @clrfund/subgraph run codegen && yarn workspace @clrfund/subgraph run build", diff --git a/subgraph/.eslintrc.js b/subgraph/.eslintrc.js index a73e076db..e2c862510 100644 --- a/subgraph/.eslintrc.js +++ b/subgraph/.eslintrc.js @@ -14,6 +14,6 @@ module.exports = { rules: { '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-explicit-any': 'off', - 'prefer-const': 'warn', + 'prefer-const': 'off', }, } diff --git a/subgraph/.gitignore b/subgraph/.gitignore index c9eccc402..c56e0e8b2 100644 --- a/subgraph/.gitignore +++ b/subgraph/.gitignore @@ -1,4 +1,4 @@ /node_modules /build /packages/*/node_modules -.DS_Store \ No newline at end of file +.DS_Store diff --git a/subgraph/abis/KlerosRecipientRegistry.json b/subgraph/abis/KlerosRecipientRegistry.json new file mode 100644 index 000000000..a90e571c4 --- /dev/null +++ b/subgraph/abis/KlerosRecipientRegistry.json @@ -0,0 +1,194 @@ +[ + { + "inputs": [ + { + "internalType": "contract IKlerosGTCR", + "name": "_tcr", + "type": "address" + }, + { + "internalType": "address", + "name": "_controller", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "_tcrItemId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "_metadata", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_index", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_timestamp", + "type": "uint256" + } + ], + "name": "RecipientAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "_tcrItemId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_timestamp", + "type": "uint256" + } + ], + "name": "RecipientRemoved", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_tcrItemId", + "type": "bytes32" + } + ], + "name": "addRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "controller", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_index", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_startTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_endTime", + "type": "uint256" + } + ], + "name": "getRecipientAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getRecipientCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxRecipients", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_tcrItemId", + "type": "bytes32" + } + ], + "name": "removeRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_maxRecipients", + "type": "uint256" + } + ], + "name": "setMaxRecipients", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "tcr", + "outputs": [ + { + "internalType": "contract IKlerosGTCR", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/subgraph/abis/SimpleRecipientRegistry.json b/subgraph/abis/SimpleRecipientRegistry.json new file mode 100644 index 000000000..96a6d801c --- /dev/null +++ b/subgraph/abis/SimpleRecipientRegistry.json @@ -0,0 +1,239 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_controller", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "_recipientId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "_metadata", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_index", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_timestamp", + "type": "uint256" + } + ], + "name": "RecipientAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "_recipientId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_timestamp", + "type": "uint256" + } + ], + "name": "RecipientRemoved", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "string", + "name": "_metadata", + "type": "string" + } + ], + "name": "addRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "controller", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_index", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_startTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_endTime", + "type": "uint256" + } + ], + "name": "getRecipientAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getRecipientCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxRecipients", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_recipientId", + "type": "bytes32" + } + ], + "name": "removeRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_maxRecipients", + "type": "uint256" + } + ], + "name": "setMaxRecipients", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/subgraph/config/rinkeby.json b/subgraph/config/rinkeby.json new file mode 100644 index 000000000..434b744c9 --- /dev/null +++ b/subgraph/config/rinkeby.json @@ -0,0 +1,6 @@ +{ + "network": "rinkeby", + "address": "0x93A990D939Ca592cD8cCa47b7a0c3F590A598F9d", + "factoryStartBlock": 9969110, + "recipientRegistryStartBlock": 9969130 +} diff --git a/subgraph/config/xdai.json b/subgraph/config/xdai.json index b026df2a1..d3d408f85 100644 --- a/subgraph/config/xdai.json +++ b/subgraph/config/xdai.json @@ -2,5 +2,6 @@ "network": "xdai", "address": "0x4ede8f30d9c2dc96a9d6787e9c4a478424fb960a", "factoryStartBlock": 15217676, + "recipientRegistryStartBlock": 0, "recipientRegistryStartBlock": 15217676 } diff --git a/subgraph/generated/templates.ts b/subgraph/generated/templates.ts deleted file mode 100644 index 754dd9bd0..000000000 --- a/subgraph/generated/templates.ts +++ /dev/null @@ -1,59 +0,0 @@ -// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. - -import { - Address, - DataSourceTemplate, - DataSourceContext -} from "@graphprotocol/graph-ts"; - -export class FundingRound extends DataSourceTemplate { - static create(address: Address): void { - DataSourceTemplate.create("FundingRound", [address.toHex()]); - } - - static createWithContext(address: Address, context: DataSourceContext): void { - DataSourceTemplate.createWithContext( - "FundingRound", - [address.toHex()], - context - ); - } -} - -export class OptimisticRecipientRegistry extends DataSourceTemplate { - static create(address: Address): void { - DataSourceTemplate.create("OptimisticRecipientRegistry", [address.toHex()]); - } - - static createWithContext(address: Address, context: DataSourceContext): void { - DataSourceTemplate.createWithContext( - "OptimisticRecipientRegistry", - [address.toHex()], - context - ); - } -} - -export class BrightIdUserRegistry extends DataSourceTemplate { - static create(address: Address): void { - DataSourceTemplate.create("BrightIdUserRegistry", [address.toHex()]); - } - - static createWithContext(address: Address, context: DataSourceContext): void { - DataSourceTemplate.createWithContext( - "BrightIdUserRegistry", - [address.toHex()], - context - ); - } -} - -export class MACI extends DataSourceTemplate { - static create(address: Address): void { - DataSourceTemplate.create("MACI", [address.toHex()]); - } - - static createWithContext(address: Address, context: DataSourceContext): void { - DataSourceTemplate.createWithContext("MACI", [address.toHex()], context); - } -} diff --git a/subgraph/src/FundingRoundFactoryMapping.ts b/subgraph/src/FundingRoundFactoryMapping.ts index 66318bc5b..81f668c2f 100644 --- a/subgraph/src/FundingRoundFactoryMapping.ts +++ b/subgraph/src/FundingRoundFactoryMapping.ts @@ -15,9 +15,9 @@ import { FundingRound as FundingRoundContract } from '../generated/FundingRoundF import { OptimisticRecipientRegistry as RecipientRegistryContract } from '../generated/FundingRoundFactory/OptimisticRecipientRegistry' +import { RecipientRegistryTemplate } from './recipientRegistry/RecipientRegistryTemplate' import { FundingRound as FundingRoundTemplate, - OptimisticRecipientRegistry as recipientRegistryTemplate, MACI as MACITemplate, } from '../generated/templates' import { @@ -120,7 +120,7 @@ export function handleRoundStarted(event: RoundStarted): void { log.info('New recipientRegistry {}', [recipientRegistryAddress.toHex()]) let recipientRegistry = new RecipientRegistry(recipientRegistryId) - recipientRegistryTemplate.create(recipientRegistryAddress) + RecipientRegistryTemplate.create(recipientRegistryAddress) let recipientRegistryContract = RecipientRegistryContract.bind( recipientRegistryAddress ) diff --git a/subgraph/src/FundingRoundMapping.ts b/subgraph/src/FundingRoundMapping.ts index 8eb25f6a2..e1b1a41d3 100644 --- a/subgraph/src/FundingRoundMapping.ts +++ b/subgraph/src/FundingRoundMapping.ts @@ -9,7 +9,6 @@ import { Voted, FundingRound as FundingRoundContract, } from '../generated/templates/FundingRound/FundingRound' -import { OptimisticRecipientRegistry as RecipientRegistryContract } from '../generated/templates/FundingRound/OptimisticRecipientRegistry' import { Recipient, diff --git a/subgraph/src/MACIMapping.ts b/subgraph/src/MACIMapping.ts index fa76a8ef3..bf84663c3 100644 --- a/subgraph/src/MACIMapping.ts +++ b/subgraph/src/MACIMapping.ts @@ -20,7 +20,7 @@ import { FundingRound, Message, PublicKey } from '../generated/schema' // - contract.verifier(...) export function handlePublishMessage(event: PublishMessage): void { - const fundingRoundId = event.transaction.to.toHexString() + let fundingRoundId = event.transaction.to.toHexString() if (fundingRoundId == null) { log.error( 'Error: handlePublishMessage failed fundingRound not registered', @@ -28,7 +28,7 @@ export function handlePublishMessage(event: PublishMessage): void { ) return } - const fundingRound = FundingRound.load(fundingRoundId) + let fundingRound = FundingRound.load(fundingRoundId) if (fundingRound == null) { log.error( 'Error: handlePublishMessage failed fundingRound not registered', @@ -37,23 +37,23 @@ export function handlePublishMessage(event: PublishMessage): void { return } - const messageID = event.transaction.hash.toHexString() + let messageID = event.transaction.hash.toHexString() - const timestamp = event.block.timestamp.toString() - const message = new Message(messageID) + let timestamp = event.block.timestamp.toString() + let message = new Message(messageID) message.data = event.params._message.data message.iv = event.params._message.iv - const publicKeyId = event.transaction.from.toHexString() - const publicKey = PublicKey.load(publicKeyId) + let publicKeyId = event.transaction.from.toHexString() + let publicKey = PublicKey.load(publicKeyId) //NOTE: If the public keys aren't being tracked initialize them if (publicKey == null) { - const publicKey = new PublicKey(publicKeyId) + let publicKey = new PublicKey(publicKeyId) publicKey.x = event.params._encPubKey.x publicKey.y = event.params._encPubKey.y - const _messages = [messageID] as string[] + let _messages = [messageID] as string[] publicKey.messages = _messages publicKey.fundingRound = fundingRoundId @@ -68,12 +68,12 @@ export function handlePublishMessage(event: PublishMessage): void { } export function handleSignUp(event: SignUp): void { - const publicKeyId = event.transaction.from.toHexString() - const publicKey = PublicKey.load(publicKeyId) + let publicKeyId = event.transaction.from.toHexString() + let publicKey = PublicKey.load(publicKeyId) //NOTE: If the public keys aren't being tracked initialize them if (publicKey == null) { - const publicKey = new PublicKey(publicKeyId) + let publicKey = new PublicKey(publicKeyId) publicKey.x = event.params._userPubKey.x publicKey.y = event.params._userPubKey.y publicKey.stateIndex = event.params._stateIndex diff --git a/subgraph/src/RecipientMapping.ts b/subgraph/src/RecipientMapping.ts new file mode 100644 index 000000000..614bdb305 --- /dev/null +++ b/subgraph/src/RecipientMapping.ts @@ -0,0 +1,13 @@ +import { Recipient } from '../generated/schema' + +export const RECIPIENT_REQUEST_TYPE_REGISTRATION = '0' +export const RECIPIENT_REQUEST_TYPE_REMOVAL = '1' + +export function removeRecipient(id: string, timestamp: string): void { + let recipient = Recipient.load(id) + if (recipient) { + recipient.requestType = RECIPIENT_REQUEST_TYPE_REMOVAL + recipient.lastUpdatedAt = timestamp + recipient.save() + } +} diff --git a/subgraph/src/recipientRegistry/KlerosRecipientRegistryMapping.ts b/subgraph/src/recipientRegistry/KlerosRecipientRegistryMapping.ts new file mode 100644 index 000000000..b09b88964 --- /dev/null +++ b/subgraph/src/recipientRegistry/KlerosRecipientRegistryMapping.ts @@ -0,0 +1,35 @@ +import { + RecipientAdded, + RecipientRemoved, +} from '../../generated/templates/KlerosRecipientRegistry/KlerosRecipientRegistry' + +import { Recipient } from '../../generated/schema' +import { + removeRecipient, + RECIPIENT_REQUEST_TYPE_REGISTRATION, +} from '../RecipientMapping' + +export function handleRecipientAdded(event: RecipientAdded): void { + let recipientRegistryId = event.address.toHexString() + + let recipientId = event.params._tcrItemId.toHexString() + let recipient = new Recipient(recipientId) + recipient.requestType = RECIPIENT_REQUEST_TYPE_REGISTRATION + // recipient was verified by kleros + recipient.verified = true + recipient.recipientRegistry = recipientRegistryId + recipient.createdAt = event.block.timestamp.toString() + recipient.recipientIndex = event.params._index + recipient.recipientMetadata = event.params._metadata.toHexString() + + // TODO map recipient address from event.params._metadata + // recipient.recipientAddress = event.params._metadata + + recipient.save() +} + +export function handleRecipientRemoved(event: RecipientRemoved): void { + let id = event.params._tcrItemId.toHexString() + let timestamp = event.block.timestamp.toString() + removeRecipient(id, timestamp) +} diff --git a/subgraph/src/OptimisticRecipientRegistryMapping.ts b/subgraph/src/recipientRegistry/OptimisticRecipientRegistryMapping.ts similarity index 95% rename from subgraph/src/OptimisticRecipientRegistryMapping.ts rename to subgraph/src/recipientRegistry/OptimisticRecipientRegistryMapping.ts index feeb4e29f..281978163 100644 --- a/subgraph/src/OptimisticRecipientRegistryMapping.ts +++ b/subgraph/src/recipientRegistry/OptimisticRecipientRegistryMapping.ts @@ -3,9 +3,9 @@ import { OwnershipTransferred, RequestResolved, RequestSubmitted, -} from '../generated/OptimisticRecipientRegistry/OptimisticRecipientRegistry' +} from '../../generated/OptimisticRecipientRegistry/OptimisticRecipientRegistry' -import { Recipient } from '../generated/schema' +import { Recipient } from '../../generated/schema' // It is also possible to access smart contracts from mappings. For // example, the contract that has emitted the event can be connected to diff --git a/subgraph/src/recipientRegistry/RecipientRegistryFactory.ts b/subgraph/src/recipientRegistry/RecipientRegistryFactory.ts new file mode 100644 index 000000000..eb130d90e --- /dev/null +++ b/subgraph/src/recipientRegistry/RecipientRegistryFactory.ts @@ -0,0 +1,91 @@ +import { Address } from '@graphprotocol/graph-ts' +import { OptimisticRecipientRegistry as OptimisticRecipientRegistryContract } from '../../generated/FundingRoundFactory/OptimisticRecipientRegistry' +import { SimpleRecipientRegistry as SimpleRecipientRegistryContract } from '../../generated/FundingRoundFactory/SimpleRecipientRegistry' +import { KlerosRecipientRegistry as KlerosRecipientRegistryContract } from '../../generated/FundingRoundFactory/KlerosRecipientRegistry' + +import { RecipientRegistry } from '../../generated/schema' +import { + RecipientRegistryType, + getRecipientRegistryType, +} from './RecipientRegistryType' + +export class RecipientRegistryCreateParams { + recipientRegistryAddress: Address + fundingRoundFactoryId: string + createdAt: string +} + +class RecipientRegistryFactoryOptimistic { + static create(params: RecipientRegistryCreateParams): RecipientRegistry { + let recipientRegistryId = params.recipientRegistryAddress.toHexString() + let recipientRegistry = new RecipientRegistry(recipientRegistryId) + + let optimisticRegistry = OptimisticRecipientRegistryContract.bind( + params.recipientRegistryAddress + ) + recipientRegistry.baseDeposit = optimisticRegistry.baseDeposit() + recipientRegistry.challengePeriodDuration = + optimisticRegistry.challengePeriodDuration() + recipientRegistry.owner = optimisticRegistry.owner() + + recipientRegistry.controller = optimisticRegistry.controller() + recipientRegistry.maxRecipients = optimisticRegistry.maxRecipients() + recipientRegistry.fundingRoundFactory = params.fundingRoundFactoryId + recipientRegistry.createdAt = params.createdAt + + return recipientRegistry + } +} + +class RecipientRegistryFactorySimple { + static create(params: RecipientRegistryCreateParams): RecipientRegistry { + let recipientRegistryId = params.recipientRegistryAddress.toHexString() + let recipientRegistry = new RecipientRegistry(recipientRegistryId) + + let simpleRegistry = SimpleRecipientRegistryContract.bind( + params.recipientRegistryAddress + ) + recipientRegistry.owner = simpleRegistry.owner() + recipientRegistry.controller = simpleRegistry.controller() + recipientRegistry.maxRecipients = simpleRegistry.maxRecipients() + recipientRegistry.fundingRoundFactory = params.fundingRoundFactoryId + recipientRegistry.createdAt = params.createdAt + + return recipientRegistry + } +} + +class RecipientRegistryFactoryKleros { + static create(params: RecipientRegistryCreateParams): RecipientRegistry { + let recipientRegistryId = params.recipientRegistryAddress.toHexString() + let recipientRegistry = new RecipientRegistry(recipientRegistryId) + + let klerosRegistry = KlerosRecipientRegistryContract.bind( + params.recipientRegistryAddress + ) + recipientRegistry.controller = klerosRegistry.controller() + recipientRegistry.maxRecipients = klerosRegistry.maxRecipients() + recipientRegistry.fundingRoundFactory = params.fundingRoundFactoryId + recipientRegistry.createdAt = params.createdAt + + return recipientRegistry + } +} + +export class RecipientRegistryFactory { + static create( + params: RecipientRegistryCreateParams + ): RecipientRegistry | null { + let type = getRecipientRegistryType(params.recipientRegistryAddress) + switch (type) { + case RecipientRegistryType.Optimistic: + return RecipientRegistryFactoryOptimistic.create(params) + case RecipientRegistryType.Kleros: + return RecipientRegistryFactoryKleros.create(params) + case RecipientRegistryType.Simple: + return RecipientRegistryFactorySimple.create(params) + default: + return null + } + } +} diff --git a/subgraph/src/recipientRegistry/RecipientRegistryTemplate.ts b/subgraph/src/recipientRegistry/RecipientRegistryTemplate.ts new file mode 100644 index 000000000..0c8d4d0fc --- /dev/null +++ b/subgraph/src/recipientRegistry/RecipientRegistryTemplate.ts @@ -0,0 +1,29 @@ +import { + RecipientRegistryType, + getRecipientRegistryType, +} from './RecipientRegistryType' +import { Address, log } from '@graphprotocol/graph-ts' +import { + OptimisticRecipientRegistry as OptimisticRecipientRegistryTemplate, + SimpleRecipientRegistry as SimpleRecipientRegistryTemplate, + KlerosRecipientRegistry as KlerosRecipientRegistryTemplate, +} from '../../generated/templates' + +export class RecipientRegistryTemplate { + static create(registryAddress: Address): void { + let type = getRecipientRegistryType(registryAddress) + switch (type) { + case RecipientRegistryType.Kleros: + KlerosRecipientRegistryTemplate.create(registryAddress) + break + case RecipientRegistryType.Optimistic: + OptimisticRecipientRegistryTemplate.create(registryAddress) + break + case RecipientRegistryType.Simple: + SimpleRecipientRegistryTemplate.create(registryAddress) + break + default: + log.error('Unknown recipient registry type, not creating template', []) + } + } +} diff --git a/subgraph/src/recipientRegistry/RecipientRegistryType.ts b/subgraph/src/recipientRegistry/RecipientRegistryType.ts new file mode 100644 index 000000000..29cc86204 --- /dev/null +++ b/subgraph/src/recipientRegistry/RecipientRegistryType.ts @@ -0,0 +1,45 @@ +import { Address, TypedMap, log } from '@graphprotocol/graph-ts' +import { OptimisticRecipientRegistry as OptimisticRecipientRegistryContract } from '../../generated/FundingRoundFactory/OptimisticRecipientRegistry' +import { KlerosRecipientRegistry as KlerosRecipientRegistryContract } from '../../generated/FundingRoundFactory/KlerosRecipientRegistry' + +export enum RecipientRegistryType { + Unknown, + Simple, + Kleros, + Optimistic, +} + +let registryTypeMap = new TypedMap() +registryTypeMap.set('simple', RecipientRegistryType.Simple) +registryTypeMap.set('kleros', RecipientRegistryType.Kleros) +registryTypeMap.set('optimistic', RecipientRegistryType.Optimistic) + +/** + * Determine the type of the registry given the registry address + * + * For newer contracts, use the registryType method. + * + * For legacy contracts, check the availability of a method to + * determine the type + * + * @param registryAddress the recipient registry address + * @returns RecipientRegistryType of Simple, Kleros, Optimistic + */ +export function getRecipientRegistryType( + registryAddress: Address +): RecipientRegistryType { + let klerosRegistry = KlerosRecipientRegistryContract.bind(registryAddress) + let tcr = klerosRegistry.try_tcr() + if (!tcr.reverted) { + return RecipientRegistryType.Kleros + } + + let optimisticRegistry = + OptimisticRecipientRegistryContract.bind(registryAddress) + let challengePeriodDuration = optimisticRegistry.try_challengePeriodDuration() + if (!challengePeriodDuration.reverted) { + return RecipientRegistryType.Optimistic + } + + return RecipientRegistryType.Simple +} diff --git a/subgraph/src/recipientRegistry/SimpleRecipientRegistryMapping.ts b/subgraph/src/recipientRegistry/SimpleRecipientRegistryMapping.ts new file mode 100644 index 000000000..9bbc6e9e9 --- /dev/null +++ b/subgraph/src/recipientRegistry/SimpleRecipientRegistryMapping.ts @@ -0,0 +1,37 @@ +import { + RecipientAdded, + RecipientRemoved, +} from '../../generated/templates/SimpleRecipientRegistry/SimpleRecipientRegistry' + +import { Recipient } from '../../generated/schema' +import { + removeRecipient, + RECIPIENT_REQUEST_TYPE_REGISTRATION, +} from '../RecipientMapping' + +export function handleRecipientAdded(event: RecipientAdded): void { + let recipientRegistryId = event.address.toHexString() + + let recipientId = event.params._recipientId.toHexString() + let recipient = new Recipient(recipientId) + + recipient.requester = event.transaction.from.toHexString() + recipient.requestType = RECIPIENT_REQUEST_TYPE_REGISTRATION + recipient.recipientRegistry = recipientRegistryId + recipient.recipientMetadata = event.params._metadata + recipient.recipientIndex = event.params._index + recipient.recipientAddress = event.params._recipient + recipient.submissionTime = event.params._timestamp.toString() + recipient.requestResolvedHash = event.transaction.hash + // recipients are verified as they are added by the admin + recipient.verified = true + recipient.createdAt = event.block.timestamp.toString() + + recipient.save() +} + +export function handleRecipientRemoved(event: RecipientRemoved): void { + let id = event.params._recipientId.toHexString() + let timestamp = event.block.timestamp.toString() + removeRecipient(id, timestamp) +} diff --git a/subgraph/subgraph.template.yaml b/subgraph/subgraph.template.yaml index 451a6f7c2..bf1957fdb 100644 --- a/subgraph/subgraph.template.yaml +++ b/subgraph/subgraph.template.yaml @@ -29,6 +29,10 @@ dataSources: file: ./abis/MACIFactory.json - name: OptimisticRecipientRegistry file: ./abis/OptimisticRecipientRegistry.json + - name: SimpleRecipientRegistry + file: ./abis/SimpleRecipientRegistry.json + - name: KlerosRecipientRegistry + file: ./abis/KlerosRecipientRegistry.json - name: BrightIdUserRegistry file: ./abis/BrightIdUserRegistry.json eventHandlers: @@ -70,7 +74,7 @@ dataSources: handler: handleRequestResolved - event: RequestSubmitted(indexed bytes32,indexed uint8,address,string,uint256) handler: handleRequestSubmitted - file: ./src/OptimisticRecipientRegistryMapping.ts + file: ./src/recipientRegistry/OptimisticRecipientRegistryMapping.ts templates: - name: FundingRound kind: ethereum/contract @@ -130,7 +134,51 @@ templates: handler: handleRequestResolved - event: RequestSubmitted(indexed bytes32,indexed uint8,address,string,uint256) handler: handleRequestSubmitted - file: ./src/OptimisticRecipientRegistryMapping.ts + file: ./src/recipientRegistry/OptimisticRecipientRegistryMapping.ts + - name: SimpleRecipientRegistry + kind: ethereum/contract + network: {{network}} + source: + abi: SimpleRecipientRegistry + mapping: + kind: ethereum/events + apiVersion: 0.0.4 + language: wasm/assemblyscript + entities: + - RecipientRegistry + - Recipient + abis: + - name: SimpleRecipientRegistry + file: ./abis/SimpleRecipientRegistry.json + eventHandlers: + - event: OwnershipTransferred(indexed address,indexed address) + handler: handleOwnershipTransferred + - event: RecipientAdded(indexed bytes32,address,string,uint256,uint256) + handler: handleRecipientAdded + - event: RecipientRemoved(indexed bytes32,uint256) + handler: handleRecipientRemoved + file: ./src/recipientRegistry/SimpleRecipientRegistryMapping.ts + - name: KlerosRecipientRegistry + kind: ethereum/contract + network: {{network}} + source: + abi: KlerosRecipientRegistry + mapping: + kind: ethereum/events + apiVersion: 0.0.4 + language: wasm/assemblyscript + entities: + - RecipientRegistry + - Recipient + abis: + - name: KlerosRecipientRegistry + file: ./abis/KlerosRecipientRegistry.json + eventHandlers: + - event: RecipientAdded(indexed bytes32,bytes,uint256,uint256) + handler: handleRecipientAdded + - event: RecipientRemoved(indexed bytes32,uint256) + handler: handleRecipientRemoved + file: ./src/recipientRegistry/KlerosRecipientRegistryMapping.ts - name: BrightIdUserRegistry kind: ethereum/contract network: {{network}} diff --git a/subgraph/subgraph.yaml b/subgraph/subgraph.yaml index 91bdea694..7d8f1ba37 100644 --- a/subgraph/subgraph.yaml +++ b/subgraph/subgraph.yaml @@ -29,6 +29,10 @@ dataSources: file: ./abis/MACIFactory.json - name: OptimisticRecipientRegistry file: ./abis/OptimisticRecipientRegistry.json + - name: SimpleRecipientRegistry + file: ./abis/SimpleRecipientRegistry.json + - name: KlerosRecipientRegistry + file: ./abis/KlerosRecipientRegistry.json - name: BrightIdUserRegistry file: ./abis/BrightIdUserRegistry.json eventHandlers: @@ -70,7 +74,7 @@ dataSources: handler: handleRequestResolved - event: RequestSubmitted(indexed bytes32,indexed uint8,address,string,uint256) handler: handleRequestSubmitted - file: ./src/OptimisticRecipientRegistryMapping.ts + file: ./src/recipientRegistry/OptimisticRecipientRegistryMapping.ts templates: - name: FundingRound kind: ethereum/contract @@ -130,7 +134,51 @@ templates: handler: handleRequestResolved - event: RequestSubmitted(indexed bytes32,indexed uint8,address,string,uint256) handler: handleRequestSubmitted - file: ./src/OptimisticRecipientRegistryMapping.ts + file: ./src/recipientRegistry/OptimisticRecipientRegistryMapping.ts + - name: SimpleRecipientRegistry + kind: ethereum/contract + network: xdai + source: + abi: SimpleRecipientRegistry + mapping: + kind: ethereum/events + apiVersion: 0.0.4 + language: wasm/assemblyscript + entities: + - RecipientRegistry + - Recipient + abis: + - name: SimpleRecipientRegistry + file: ./abis/SimpleRecipientRegistry.json + eventHandlers: + - event: OwnershipTransferred(indexed address,indexed address) + handler: handleOwnershipTransferred + - event: RecipientAdded(indexed bytes32,address,string,uint256,uint256) + handler: handleRecipientAdded + - event: RecipientRemoved(indexed bytes32,uint256) + handler: handleRecipientRemoved + file: ./src/recipientRegistry/SimpleRecipientRegistryMapping.ts + - name: KlerosRecipientRegistry + kind: ethereum/contract + network: xdai + source: + abi: KlerosRecipientRegistry + mapping: + kind: ethereum/events + apiVersion: 0.0.4 + language: wasm/assemblyscript + entities: + - RecipientRegistry + - Recipient + abis: + - name: KlerosRecipientRegistry + file: ./abis/KlerosRecipientRegistry.json + eventHandlers: + - event: RecipientAdded(indexed bytes32,bytes,uint256,uint256) + handler: handleRecipientAdded + - event: RecipientRemoved(indexed bytes32,uint256) + handler: handleRecipientRemoved + file: ./src/recipientRegistry/KlerosRecipientRegistryMapping.ts - name: BrightIdUserRegistry kind: ethereum/contract network: xdai diff --git a/subgraph/tsconfig.json b/subgraph/tsconfig.json index b40085325..769d8a3b2 100644 --- a/subgraph/tsconfig.json +++ b/subgraph/tsconfig.json @@ -13,7 +13,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react" + "jsx": "react", + "strictPropertyInitialization": false }, "include": ["./src", "./generated", "/abis"] } diff --git a/vue-app/.env.example b/vue-app/.env.example index d765d0c3d..2d52c0be7 100644 --- a/vue-app/.env.example +++ b/vue-app/.env.example @@ -46,5 +46,19 @@ VUE_APP_FIRST_ROUND= GOOGLE_APPLICATION_CREDENTIALS= # Spreadsheet ID to send recipients data VUE_APP_GOOGLE_SPREADSHEET_ID= + +# metadata registry configurations +# The networks where metadata is stored; as comma separated strings +# i.e. arbitrum-rinkeby,ropsten,mainnet +VUE_APP_METADATA_NETWORKS= + +# metadata registry subgraph url prefix +# Add the network part (VUE_APP_METADATA_NETWORKS) to form the complete url +# i.e. https://api.thegraph.com/subgraphs/name/clrfund/metadata- +VUE_APP_METADATA_SUBGRAPH_URL_PREFIX= + +# subgraph query batch size, default to 30 +VUE_APP_QUERY_BATCH_SIZE= + # Select the sheet's name to write the data, by default 'Raw' GOOGLE_SHEET_NAME= diff --git a/vue-app/.env.xdai b/vue-app/.env.xdai index f92ce255e..06415ba8d 100644 --- a/vue-app/.env.xdai +++ b/vue-app/.env.xdai @@ -19,7 +19,7 @@ VUE_APP_USER_REGISTRY_TYPE=brightid # Learn more about BrightID and context in /docs/brightid.md VUE_APP_BRIGHTID_CONTEXT=clr.fund -# Supported values: simple, optimistic, kleros, universal +# Supported values: simple, optimistic, kleros VUE_APP_RECIPIENT_REGISTRY_TYPE=optimistic VUE_APP_RECIPIENT_REGISTRY_POLICY=QmeygKjvrpidJeFHv6ywjUrj718nwtFQgCCPPR4r5nL87R diff --git a/vue-app/package.json b/vue-app/package.json index 19192d195..05bd4b2c5 100644 --- a/vue-app/package.json +++ b/vue-app/package.json @@ -12,6 +12,7 @@ "test:lint": "vue-cli-service lint --no-fix" }, "dependencies": { + "@clrfund/metadata-composer": "^0.0.1", "@kleros/gtcr-encoder": "^1.4.0", "@openfonts/inter_all": "^1.0.2", "@walletconnect/web3-provider": "^1.5.1", diff --git a/vue-app/src/App.vue b/vue-app/src/App.vue index 211369ad5..1984de726 100644 --- a/vue-app/src/App.vue +++ b/vue-app/src/App.vue @@ -187,9 +187,15 @@ export default class App extends Vue { 'join-step', 'round-information', 'transaction-success', + 'metadata-success', 'verify', 'verify-step', 'verified', + 'metadata', + 'metadata-registry', + 'metadata-edit', + 'metadata-new', + 'not-found', 'sponsored', ] return !excludedRoutes.includes(this.$route.name || '') @@ -205,6 +211,11 @@ export default class App extends Vue { 'verify', 'verify-step', 'verified', + 'metadata', + 'metadata-registry', + 'metadata-edit', + 'metadata-new', + 'not-found', 'sponsored', ] return !excludedRoutes.includes(this.$route.name || '') @@ -230,6 +241,7 @@ export default class App extends Vue { 'verify', 'project-added', 'verified', + 'not-found', ] return !excludedRoutes.includes(this.$route.name || '') } diff --git a/vue-app/src/api/abi.ts b/vue-app/src/api/abi.ts index f8ffc89d8..528ad6edd 100644 --- a/vue-app/src/api/abi.ts +++ b/vue-app/src/api/abi.ts @@ -9,6 +9,7 @@ import { abi as SimpleRecipientRegistry } from '../../../contracts/build/contrac import { abi as OptimisticRecipientRegistry } from '../../../contracts/build/contracts/contracts/recipientRegistry/OptimisticRecipientRegistry.sol/OptimisticRecipientRegistry.json' import { abi as KlerosGTCR } from '../../../contracts/build/contracts/contracts/recipientRegistry/IKlerosGTCR.sol/IKlerosGTCR.json' import { abi as KlerosGTCRAdapter } from '../../../contracts/build/contracts/contracts/recipientRegistry/KlerosGTCRAdapter.sol/KlerosGTCRAdapter.json' +import { abi as BaseRecipientRegistry } from '../../../contracts/build/contracts/contracts/recipientRegistry/BaseRecipientRegistry.sol/BaseRecipientRegistry.json' export { ERC20, @@ -22,4 +23,5 @@ export { OptimisticRecipientRegistry, KlerosGTCR, KlerosGTCRAdapter, + BaseRecipientRegistry, } diff --git a/vue-app/src/api/core.ts b/vue-app/src/api/core.ts index fc9ddbac0..9f681d69a 100644 --- a/vue-app/src/api/core.ts +++ b/vue-app/src/api/core.ts @@ -41,9 +41,17 @@ if ( ) { throw new Error('invalid user registry type') } -export const recipientRegistryType = process.env.VUE_APP_RECIPIENT_REGISTRY_TYPE +export enum RecipientRegistryType { + SIMPLE = 'simple', + OPTIMISTIC = 'optimistic', + KLEROS = 'kleros', +} +export const recipientRegistryType = + process.env.VUE_APP_RECIPIENT_REGISTRY_TYPE || '' if ( - !['simple', 'optimistic', 'kleros'].includes(recipientRegistryType as string) + !Object.values(RecipientRegistryType).includes( + recipientRegistryType as RecipientRegistryType + ) ) { throw new Error('invalid recipient registry type') } @@ -58,8 +66,29 @@ export const SUBGRAPH_ENDPOINT = process.env.VUE_APP_SUBGRAPH_URL || 'https://api.thegraph.com/subgraphs/name/clrfund/clrfund' +export const METADATA_SUBGRAPH_URL_PREFIX = process.env + .VUE_APP_METADATA_SUBGRAPH_URL_PREFIX + ? process.env.VUE_APP_METADATA_SUBGRAPH_URL_PREFIX + : 'https://api.thegraph.com/subgraphs/name/yuetloo/metadata-' + +export const METADATA_NETWORKS = process.env.VUE_APP_METADATA_NETWORKS + ? process.env.VUE_APP_METADATA_NETWORKS.split(',') + : ['rinkeby'] + +export const QUERY_BATCH_SIZE = + Number(process.env.VUE_APP_QUERY_BATCH_SIZE) || 30 + +export const MAX_RETRIES = Number(process.env.VUE_APP_MAX_RETRIES) || 10 + // application theme export enum ThemeMode { LIGHT = 'light', DARK = 'dark', } + +// transaction progress reported as the current block seen +// and the last block the transaction is expected to be in +export type TransactionProgress = { + current: number + last: number +} diff --git a/vue-app/src/api/ipfs.ts b/vue-app/src/api/ipfs.ts index afb4d4342..66bee1534 100644 --- a/vue-app/src/api/ipfs.ts +++ b/vue-app/src/api/ipfs.ts @@ -1,3 +1,4 @@ +import { ipfsGatewayUrl } from './core' import { ipfsPinningUrl, ipfsPinningJwt } from './core' export class IPFS { @@ -25,4 +26,13 @@ export class IPFS { const json = await result.json() return json.IpfsHash } + + static toUrl(hash: string | undefined): string | undefined { + if (!hash) { + return + } + + const base = ipfsGatewayUrl || '' + return new URL(`/ipfs/${hash}`, base).toString() + } } diff --git a/vue-app/src/api/metadata.ts b/vue-app/src/api/metadata.ts new file mode 100644 index 000000000..65b6b0b5b --- /dev/null +++ b/vue-app/src/api/metadata.ts @@ -0,0 +1,466 @@ +import { MetadataComposer, SearchOptions } from '@clrfund/metadata-composer' +import { ContractTransaction, providers, utils } from 'ethers' +import { METADATA_NETWORKS, METADATA_SUBGRAPH_URL_PREFIX, chain } from './core' +import { Project } from './projects' +import { IPFS } from './ipfs' +import { MAX_RETRIES } from './core' +import { required, url, maxLength } from 'vuelidate/lib/validators' +import * as isIPFS from 'is-ipfs' +import { ReceivingAddress } from '@/api/receiving-address' + +const subgraphUrl = (network: string): string => + `${METADATA_SUBGRAPH_URL_PREFIX}${network}` + +const urls = METADATA_NETWORKS.map((network) => subgraphUrl(network)) + +const GET_METADATA_BATCH_QUERY = ` + query($ids: [String]) { + metadataEntries(where: { id_in: $ids }) { + id + metadata + } + } +` + +const GET_LATEST_BLOCK_QUERY = ` +{ + _meta { + block { + number + } + } +} +` + +export interface MetadataFormData { + project: { + name: string + tagline: string + description: string + category: string + problemSpace: string + } + fund: { + receivingAddresses: string[] + currentChainReceivingAddress: 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 + } + dirtyFields: Set + furthestStep: number + id?: string + network?: string + owner?: string +} + +export const MetadataFormValidations = { + project: { + name: { required }, + tagline: { + required, + maxLength: maxLength(140), + }, + description: { required }, + category: { required }, + problemSpace: { required }, + }, + fund: { + receivingAddresses: {}, + currentChainReceivingAddress: { + required, + validEthAddress: utils.isAddress, + }, + plans: { required }, + }, + team: { + name: {}, + description: {}, + }, + links: { + github: { url }, + radicle: { url }, + website: { url }, + twitter: { url }, + discord: { url }, + }, + image: { + bannerHash: { + required, + validIpfsHash: isIPFS.cid, + }, + thumbnailHash: { + required, + validIpfsHash: isIPFS.cid, + }, + }, +} + +/** + * Extract address for the current chain from the fund receiving addresses + * @param receivingAddresses array of EIP-3770 addresses, i.e. eth:0x11111... + * @returns address for the current chain + */ +function getAddressForCurrentChain(receivingAddresses: string[] = []): string { + const addresses = ReceivingAddress.fromArray(receivingAddresses) + return addresses[chain.shortName] +} + +/** + * Get the latest block from subgraph + * @returns block number + */ +async function getLatestBlock(network: string): Promise { + // only interested in the latest block from the specified network + const url = subgraphUrl(network) + const composer = new MetadataComposer([url]) + const result = await composer.query(GET_LATEST_BLOCK_QUERY) + if (result.error) { + throw new Error('Failed to get latest block from subgraph. ' + result.error) + } + + const [meta] = result.data._meta + if (!meta) { + throw new Error('Missing block information') + } + + return meta.block.number +} + +function sleep(factor: number): Promise { + const timeout = factor ** 2 * 1000 + return new Promise((resolve) => setTimeout(resolve, timeout)) +} + +/** + * Metadata class + */ +export class Metadata { + id?: string + name?: string + owner?: string + network?: string + receivingAddresses?: string[] + currentChainReceivingAddress?: string + tagline?: string + description?: string + category?: string + problemSpace?: string + plans?: string + teamName?: string + teamDescription?: string + githubUrl?: string + radicleUrl?: string + websiteUrl?: string + twitterUrl?: string + discordUrl?: string + bannerImageHash?: string + thumbnailImageHash?: string + imageHash?: string + deletedAt?: number + furthestStep?: number + email?: string + + constructor(data: any) { + this.id = data.id + this.name = data.name + this.owner = data.owner + this.network = data.network + this.receivingAddresses = data.receivingAddresses + this.currentChainReceivingAddress = getAddressForCurrentChain( + data.receivingAddresses + ) + this.tagline = data.tagline + this.description = data.description + this.category = data.category + this.problemSpace = data.problemSpace + this.plans = data.plans + this.teamName = data.teamName + this.teamDescription = data.teamDescription + this.githubUrl = data.githubUrl + this.radicleUrl = data.radicleUrl + this.websiteUrl = data.websiteUrl + this.twitterUrl = data.twitterUrl + this.discordUrl = data.discordUrl + this.bannerImageHash = data.bannerImageHash + this.thumbnailImageHash = data.thumbnailImageHash + this.imageHash = data.imageHash + this.furthestStep = data.furthestStep + this.email = data.email + } + + /** + * Search metadata by search text + * @param searchText search for metadata containing this text + * @param options first - option to limit search result + * activeOnly - option to search only active metadata (not deleted) + * @returns array of metadata + */ + static async search( + searchText: string, + options: SearchOptions + ): Promise { + const composer = new MetadataComposer(urls) + const result = await composer.search(searchText, options) + if (result.error) { + throw new Error(result.error) + } + + const { data = [] } = result + return data.map((entry) => new Metadata(entry)) + } + + /** + * Get metadata by id + * @param id metadata id + * @returns metadata + */ + static async get(id: string): Promise { + const composer = new MetadataComposer(urls) + const result = await composer.get(id) + const { data } = result + if (!data) { + // id not found + return null + } + + return new Metadata({ ...data }) + } + + /** + * get metadata given an array of metadata ids + * @param ids array of metadata id + * @returns a list of metadata + */ + static async getBatch(ids: string[]): Promise { + const composer = new MetadataComposer(urls) + const result = await composer.query(GET_METADATA_BATCH_QUERY, { ids }) + if (result.error) { + throw new Error(result.error) + } + + return result.data?.metadataEntries || [] + } + + /** + * Convert metadata to project interface + * @returns project + */ + toProject(): Project { + return { + id: this.id || '', + address: this.currentChainReceivingAddress || '', + name: this.name || '', + tagline: this.tagline, + description: this.description || '', + category: this.category, + problemSpace: this.problemSpace, + plans: this.plans, + teamName: this.teamName, + teamDescription: this.teamDescription, + githubUrl: this.githubUrl, + radicleUrl: this.radicleUrl, + websiteUrl: this.websiteUrl, + twitterUrl: this.twitterUrl, + discordUrl: this.discordUrl, + bannerImageUrl: IPFS.toUrl(this.bannerImageHash), + thumbnailImageUrl: IPFS.toUrl(this.thumbnailImageHash), + imageUrl: IPFS.toUrl(this.imageHash), + index: 0, + isHidden: false, + isLocked: false, + } + } + + /** + * Convert metadata to form data + * @returns recipient application form data + */ + toFormData(): MetadataFormData { + return { + project: { + name: this.name || '', + tagline: this.tagline || '', + description: this.description || '', + category: this.category || '', + problemSpace: this.problemSpace || '', + }, + fund: { + receivingAddresses: this.receivingAddresses || [], + currentChainReceivingAddress: + getAddressForCurrentChain(this.receivingAddresses) || '', + plans: this.plans || '', + }, + team: { + name: this.teamName || '', + description: this.teamDescription || '', + email: this.email || '', + }, + links: { + github: this.githubUrl || '', + radicle: this.radicleUrl || '', + website: this.websiteUrl || '', + twitter: this.twitterUrl || '', + discord: this.discordUrl || '', + }, + image: { + bannerHash: this.bannerImageHash || '', + thumbnailHash: this.thumbnailImageHash || '', + }, + dirtyFields: new Set(), + furthestStep: 0, + id: this.id, + owner: this.owner, + network: this.network, + } + } + + /** + * Convert form data to Metadata + * @param data form data + * @param dirtyOnly only set the field in metadata if it's changed + * @returns Metadata + */ + static fromFormData(data: MetadataFormData, dirtyOnly = false): Metadata { + const { id, project, fund, team, links, image } = data + const metadata = new Metadata({ id }) + + if (!dirtyOnly || data.dirtyFields.has('project.name')) { + metadata.name = project.name + } + if (!dirtyOnly || data.dirtyFields.has('project.tagline')) { + metadata.tagline = project.tagline + } + if (!dirtyOnly || data.dirtyFields.has('project.description')) { + metadata.description = project.description + } + if (!dirtyOnly || data.dirtyFields.has('project.category')) { + metadata.category = project.category + } + if (!dirtyOnly || data.dirtyFields.has('project.problemSpace')) { + metadata.problemSpace = project.problemSpace + } + if (!dirtyOnly || data.dirtyFields.has('fund.plans')) { + metadata.plans = fund.plans + } + if (!dirtyOnly || data.dirtyFields.has('fund.receivingAddresses')) { + metadata.receivingAddresses = fund.receivingAddresses + } + if (!dirtyOnly || data.dirtyFields.has('team.name')) { + metadata.teamName = team.name + } + if (!dirtyOnly || data.dirtyFields.has('team.description')) { + metadata.teamDescription = team.description + } + if (!dirtyOnly || data.dirtyFields.has('links.github')) { + metadata.githubUrl = links.github + } + if (!dirtyOnly || data.dirtyFields.has('links.radicle')) { + metadata.radicleUrl = links.radicle + } + if (!dirtyOnly || data.dirtyFields.has('links.website')) { + metadata.websiteUrl = links.website + } + if (!dirtyOnly || data.dirtyFields.has('links.twitter')) { + metadata.twitterUrl = links.twitter + } + if (!dirtyOnly || data.dirtyFields.has('links.discord')) { + metadata.discordUrl = links.discord + } + if (!dirtyOnly || data.dirtyFields.has('image.bannerHash')) { + metadata.bannerImageHash = image.bannerHash + } + if (!dirtyOnly || data.dirtyFields.has('image.thumbnailHash')) { + metadata.thumbnailImageHash = image.thumbnailHash + } + if (!dirtyOnly) { + metadata.network = data.network + metadata.owner = data.owner + } + + return metadata + } + + /** + * Create a metadata in the registry + * @param web3 EIP1193 web3 provider used to sign the transaction + * @returns transaction handle + */ + async create(web3: providers.Web3Provider): Promise { + const composer = new MetadataComposer(urls) + return composer.create(this, web3.provider) + } + + /** + * Update metadata in the registry + * @param web3 provider used to sign the transaction + * @returns transaction handle + */ + async update(web3: providers.Web3Provider): Promise { + if (!this.id) { + throw new Error('Unable to update metadata, id missing') + } + const metadata = { ...this } + const composer = new MetadataComposer(urls) + return composer.update(metadata, web3.provider) + } + + /** + * Delete metadata to registry + * @param web3 provider used to sign the delete transaction + * @returns transaction handle + */ + async delete(web3: providers.Web3Provider): Promise { + if (!this.id) { + throw new Error('Unable to delete metadata, id missing') + } + const composer = new MetadataComposer(urls) + return composer.delete(this.id, web3.provider) + } + + /** + * Get metadata id from the receipt + * @param receipt receipt containing the metadata transaction + * @returns metadata id + */ + static makeMetadataId(name: string, owner: string): string { + const networkHash = utils.id(chain.name) + const nameHash = utils.id(name) + const hashes = utils.hexConcat([networkHash, owner, nameHash]) + const id = utils.keccak256(hashes) + return id + } + + static async waitForBlock( + blockNumber: number, + network: string, + depth = 0, + callback: (latest: number, total: number) => void + ): Promise { + const latestBlock = await getLatestBlock(network) + callback(latestBlock, blockNumber) + if (latestBlock < blockNumber) { + if (depth > MAX_RETRIES) { + throw new Error('Waited too long for block ' + blockNumber) + } + await sleep(depth) + return Metadata.waitForBlock(blockNumber, network, depth + 1, callback) + } else { + return latestBlock + } + } +} diff --git a/vue-app/src/api/projects.ts b/vue-app/src/api/projects.ts index 8922f7d59..924c7adf8 100644 --- a/vue-app/src/api/projects.ts +++ b/vue-app/src/api/projects.ts @@ -1,11 +1,14 @@ -import { Contract, Signer } from 'ethers' -import { TransactionResponse } from '@ethersproject/abstract-provider' +import { Contract } from 'ethers' import { FundingRound } from './abi' -import { factory, provider, recipientRegistryType } from './core' +import { + factory, + provider, + ipfsGatewayUrl, + recipientRegistryType, +} from './core' -import SimpleRegistry from './recipient-registry-simple' -import OptimisticRegistry from './recipient-registry-optimistic' import KlerosRegistry from './recipient-registry-kleros' +import RecipientRegistry from './recipient-registry' export interface Project { id: string // Address or another ID depending on registry implementation @@ -52,18 +55,14 @@ export async function getProjects( startTime?: number, endTime?: number ): Promise { - if (recipientRegistryType === 'simple') { - return await SimpleRegistry.getProjects(registryAddress, startTime, endTime) - } else if (recipientRegistryType === 'optimistic') { - return await OptimisticRegistry.getProjects( + if (recipientRegistryType === 'kleros') { + return await KlerosRegistry.getProjects(registryAddress, startTime, endTime) + } else { + return await RecipientRegistry.getProjects( registryAddress, startTime, endTime ) - } else if (recipientRegistryType === 'kleros') { - return await KlerosRegistry.getProjects(registryAddress, startTime, endTime) - } else { - throw new Error('invalid recipient registry type') } } @@ -71,35 +70,30 @@ export async function getProject( registryAddress: string, recipientId: string ): Promise { - if (recipientRegistryType === 'simple') { - return await SimpleRegistry.getProject(registryAddress, recipientId) - } else if (recipientRegistryType === 'optimistic') { - return await OptimisticRegistry.getProject(recipientId) - } else if (recipientRegistryType === 'kleros') { + if (recipientRegistryType === 'kleros') { return await KlerosRegistry.getProject(registryAddress, recipientId) } else { - throw new Error('invalid recipient registry type') + return await RecipientRegistry.getProject(recipientId) } } -export async function registerProject( - registryAddress: string, - recipientId: string, - signer: Signer -): Promise { - if (recipientRegistryType === 'optimistic') { - return await OptimisticRegistry.registerProject( - registryAddress, - recipientId, - signer - ) - } else if (recipientRegistryType === 'kleros') { - return await KlerosRegistry.registerProject( - registryAddress, - recipientId, - signer - ) - } else { - throw new Error('invalid recipient registry type') +export async function projectExists(recipientId: string): Promise { + return RecipientRegistry.projectExists(recipientId) +} + +export function toProjectInterface(metadata: any): Project { + const imageUrl = metadata.imageUrl + const bannerImageUrl = metadata.bannerImageHash + ? `${ipfsGatewayUrl}/ipfs/${metadata.bannerImageHash}` + : imageUrl + + const thumbnailImageUrl = metadata.thumbnailImageHash + ? `${ipfsGatewayUrl}/ipfs/${metadata.thumbnailImageHash}` + : imageUrl + + return { + ...metadata, + bannerImageUrl, + thumbnailImageUrl, } } diff --git a/vue-app/src/api/receiving-address.ts b/vue-app/src/api/receiving-address.ts new file mode 100644 index 000000000..6f12d0424 --- /dev/null +++ b/vue-app/src/api/receiving-address.ts @@ -0,0 +1,36 @@ +/** + * Fund Receiving Address + */ +export class ReceivingAddress { + /** + * Convert the receiving addresses from string to a lookup dictionary + * @param addresses array of EIP3770 addresses (e.g. eth:0x1234...) + * @returns a dictionary of chain short name to address + */ + static fromArray(addresses: string[]): Record { + const result: Record = addresses.reduce( + (addresses, item) => { + const chainAddress = item.split(':') + + if (chainAddress.length === 2) { + addresses[chainAddress[0]] = chainAddress[1] + } + return addresses + }, + {} + ) + + return result + } + + /** + * Convert a chain-address dictionary to an array of EIP3770 addresses + * @param addresses a dictionary with chain short name to address + * @returns an array of EIP3770 addresses + */ + static toArray(addresses: Record): string[] { + return Object.entries(addresses).map( + ([chain, address]) => `${chain}:${address}` + ) + } +} diff --git a/vue-app/src/api/recipient-registry-kleros.ts b/vue-app/src/api/recipient-registry-kleros.ts index 9b1903924..694ca736b 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, Event, Signer } from 'ethers' +import { BigNumber, Contract, Event, Signer } from 'ethers' import { TransactionResponse } from '@ethersproject/abstract-provider' import { gtcrDecode } from '@kleros/gtcr-encoder' import { KlerosGTCR, KlerosGTCRAdapter } from './abi' import { provider, ipfsGatewayUrl } from './core' -import { Project } from './projects' +import { RecipientRegistryInterface } from './types' +import { Project, toProjectInterface } from './projects' const KLEROS_CURATE_URL = 'https://curate.kleros.io/tcr/0x2E3B10aBf091cdc53cC892A50daBDb432e220398' @@ -58,13 +59,13 @@ function decodeTcrItemData( function decodeRecipientAdded(event: Event, columns: TcrColumn[]): Project { const args = event.args as any - return { + return toProjectInterface({ id: args._tcrItemId, ...decodeTcrItemData(columns, args._metadata), index: args._index.toNumber(), isHidden: false, isLocked: false, - } + }) } export async function getProjects( @@ -129,7 +130,7 @@ export async function getProjects( if (tcrItemStatus.toNumber() !== TcrItemStatus.Registered) { continue } - const project: Project = { + const project: Project = toProjectInterface({ id: tcrItemId, ...decodeTcrItemData(tcrColumns, tcrItemData), // Only unregistered project can have invalid index 0 @@ -140,7 +141,7 @@ export async function getProjects( tcrItemStatus: TcrItemStatus.Registered, tcrItemUrl: `${KLEROS_CURATE_URL}/${tcrItemId}`, }, - } + }) projects.push(project) } return projects @@ -159,7 +160,7 @@ export async function getProject( // Item is not in TCR return null } - const project: Project = { + const project: Project = toProjectInterface({ id: recipientId, ...decodeTcrItemData(tcrColumns, tcrItemData), // Only unregistered project can have invalid index 0 @@ -170,7 +171,7 @@ export async function getProject( tcrItemStatus: tcrItemStatus.toNumber(), tcrItemUrl: `${KLEROS_CURATE_URL}/${recipientId}`, }, - } + }) const recipientAddedFilter = registry.filters.RecipientAdded(recipientId) const recipientAddedEvents = await registry.queryFilter( recipientAddedFilter, @@ -202,4 +203,32 @@ export async function registerProject( return transaction } -export default { getProjects, getProject, registerProject } +export function addRecipient( + registryAddress: string, + recipientData: any, + _deposit: BigNumber, + signer: Signer +): Promise { + return registerProject(registryAddress, recipientData.id, signer) +} + +function removeProject() { + throw new Error('removeProject Not implemented') +} + +function rejectProject() { + throw new Error('rejectProject Not implemented') +} + +export function create(): RecipientRegistryInterface { + return { + addRecipient, + removeProject, + registerProject, + rejectProject, + isSelfRegistration: false, //TODO: add support for self registration + requireRegistrationDeposit: false, + } +} + +export default { getProjects, getProject, registerProject, create } diff --git a/vue-app/src/api/recipient-registry-optimistic.ts b/vue-app/src/api/recipient-registry-optimistic.ts index fcedfc0b9..c1e1b8d72 100644 --- a/vue-app/src/api/recipient-registry-optimistic.ts +++ b/vue-app/src/api/recipient-registry-optimistic.ts @@ -1,232 +1,10 @@ import { BigNumber, Contract, Signer } from 'ethers' -import { - TransactionResponse, - TransactionReceipt, -} from '@ethersproject/abstract-provider' -import { isHexString } from '@ethersproject/bytes' -import { DateTime } from 'luxon' -import { getEventArg } from '@/utils/contracts' -import { chain } from '@/api/core' +import { TransactionResponse } from '@ethersproject/abstract-provider' import { OptimisticRecipientRegistry } from './abi' -import { provider, ipfsGatewayUrl, recipientRegistryPolicy } from './core' -import { Project } from './projects' -import sdk from '@/graphql/sdk' -import { Recipient } from '@/graphql/API' -import { hasDateElapsed } from '@/utils/dates' - -export interface RegistryInfo { - deposit: BigNumber - depositToken: string - challengePeriodDuration: number - listingPolicyUrl: string - recipientCount: number - owner: string -} - -export async function getRegistryInfo( - registryAddress: string -): Promise { - const registry = new Contract( - registryAddress, - OptimisticRecipientRegistry, - provider - ) - const deposit = await registry.baseDeposit() - const challengePeriodDuration = await registry.challengePeriodDuration() - 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() - return { - deposit, - depositToken: chain.currency, - challengePeriodDuration: challengePeriodDuration.toNumber(), - listingPolicyUrl: `${ipfsGatewayUrl}/ipfs/${recipientRegistryPolicy}`, - recipientCount: recipientCount.toNumber(), - owner, - } -} - -export enum RequestType { - Registration = 'Registration', - Removal = 'Removal', -} - -enum RequestTypeCode { - Registration = 0, - Removal = 1, -} - -export enum RequestStatus { - Submitted = 'Needs review', - Accepted = 'Accepted', - Rejected = 'Rejected', - Executed = 'Live', - Removed = 'Removed', -} - -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 -} - -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, - } -} - -interface RecipientMetadata { - name: string - description: string - imageUrl: string -} - -export interface Request { - transactionHash: string - type: RequestType - status: RequestStatus - acceptanceDate: DateTime - recipientId: string - recipient: string - metadata: RecipientMetadata - requester: string -} - -export async function getRequests( - registryInfo: RegistryInfo, - registryAddress: string -): Promise { - const data = await sdk.GetRecipients({ - registryAddress: registryAddress.toLowerCase(), - }) - - if (!data.recipients?.length) { - return [] - } - - const recipients = data.recipients - - const requests: Record = {} - for (const recipient of recipients) { - let metadata: any - try { - metadata = JSON.parse(recipient.recipientMetadata || '{}') - } catch { - metadata = {} - } - const requestType = Number(recipient.requestType) - if (requestType === RequestTypeCode.Registration) { - // Registration request - const { name, description, imageHash, thumbnailImageHash } = metadata - metadata = { - name, - description, - imageUrl: `${ipfsGatewayUrl}/ipfs/${imageHash}`, - thumbnailImageUrl: thumbnailImageHash - ? `${ipfsGatewayUrl}/ipfs/${thumbnailImageHash}` - : `${ipfsGatewayUrl}/ipfs/${imageHash}`, - } - } - - const submissionTime = Number(recipient.submissionTime) - const acceptanceDate = DateTime.fromSeconds( - submissionTime + registryInfo.challengePeriodDuration - ) - - let requester - if (recipient.requester) { - requester = recipient.requester - } - - const request: Request = { - transactionHash: - recipient.requestResolvedHash || recipient.requestSubmittedHash, - type: RequestType[RequestTypeCode[requestType]], - status: hasDateElapsed(acceptanceDate) - ? RequestStatus.Accepted - : RequestStatus.Submitted, - acceptanceDate, - recipientId: recipient.id, - recipient: recipient.recipientAddress, - metadata, - requester, - } - - if (recipient.rejected) { - request.status = RequestStatus.Rejected - } - - if (recipient.verified) { - request.status = - requestType === RequestTypeCode.Removal - ? RequestStatus.Removed - : RequestStatus.Executed - } - - // In case there are two requests submissions events, we always prioritize - // the last one since you can only have one request per recipient - requests[request.recipientId] = request - } - return Object.keys(requests).map((recipientId) => requests[recipientId]) -} +import { RecipientRegistryInterface } from './types' +import { MetadataFormData } from './metadata' +import { chain } from './core' // TODO merge this with `Project` inteface export interface RecipientData { @@ -250,33 +28,9 @@ export interface RecipientData { 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( registryAddress: string, - recipientApplicationData: RecipientApplicationData, + recipientMetadata: MetadataFormData, deposit: BigNumber, signer: Signer ): Promise { @@ -285,175 +39,25 @@ export async function addRecipient( OptimisticRecipientRegistry, signer ) - const recipientData = formToRecipientData(recipientApplicationData) - const { address, ...metadata } = recipientData - const transaction = await registry.addRecipient( - address, - JSON.stringify(metadata), - { value: deposit } - ) - return transaction -} - -export function getRequestId( - receipt: TransactionReceipt, - registryAddress: string -): string { - const registry = new Contract(registryAddress, OptimisticRecipientRegistry) - return getEventArg(receipt, registry, 'RequestSubmitted', '_recipientId') -} - -function decodeProject(recipient: Partial): Project { - if (!recipient.id) { - throw new Error('Incorrect recipient data') - } - - const metadata = JSON.parse(recipient.recipientMetadata || '') - - // imageUrl is the legacy form property - fall back to this if bannerImageHash or thumbnailImageHash don't exist - const imageUrl = `${ipfsGatewayUrl}/ipfs/${metadata.imageHash}` - - let requester - if (recipient.requester) { - requester = recipient.requester - } - - return { - id: recipient.id, - address: recipient.recipientAddress || '', - requester, - name: metadata.name, - description: metadata.description, - imageUrl, - // Only unregistered project can have invalid index 0 - index: 0, - isHidden: false, - isLocked: false, - extra: { - submissionTime: Number(recipient.submissionTime), - }, - 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: metadata.bannerImageHash - ? `${ipfsGatewayUrl}/ipfs/${metadata.bannerImageHash}` - : imageUrl, - thumbnailImageUrl: metadata.thumbnailImageHash - ? `${ipfsGatewayUrl}/ipfs/${metadata.thumbnailImageHash}` - : imageUrl, - } -} - -export async function getProjects( - registryAddress: string, - startTime?: number, - endTime?: number -): Promise { - const data = await sdk.GetRecipients({ - registryAddress: registryAddress.toLowerCase(), - }) - - if (!data.recipients?.length) { - return [] - } - - const recipients = data.recipients - - const projects: Project[] = recipients - .map((recipient) => { - let project - try { - project = decodeProject(recipient) - } catch (err) { - return - } - - const submissionTime = Number(recipient.submissionTime) - - if (recipient.rejected) { - return - } - - const requestType = Number(recipient.requestType) - if (requestType === RequestTypeCode.Registration) { - if (recipient.verified) { - const addedAt = submissionTime - if (endTime && addedAt >= endTime) { - // Hide recipient if it is added after the end of round - project.isHidden = true - } - project.index = recipient.recipientIndex - return project - } else { - return - } - } - - if (requestType === RequestTypeCode.Removal) { - const removedAt = submissionTime - if (!startTime || removedAt <= startTime) { - // Start time not specified - // or recipient had been removed before start time - project.isHidden = true - } else { - // Disallow contributions to removed recipient, but don't hide it - project.isLocked = true - } - } - - return project - }) - .filter(Boolean) - - return projects -} - -export async function getProject(recipientId: string): Promise { - if (!isHexString(recipientId, 32)) { - return null - } - - const data = await sdk.GetProject({ - recipientId, - }) - - if (!data.recipients?.length) { - // Project does not exist - return null + const { id, fund } = recipientMetadata + if (!id) { + throw new Error('Missing metadata id') } - const recipient = data.recipients?.[0] - - let project: Project - try { - project = decodeProject(recipient) - } catch { - // Invalid metadata - return null + const { currentChainReceivingAddress: address } = fund + if (!address) { + throw new Error(`Missing recipient address for the ${chain.name} network`) } - const requestType = Number(recipient.requestType) - if (requestType === RequestTypeCode.Registration) { - if (recipient.verified) { - project.index = recipient.recipientIndex - } else { - return null + const json = { id } + const transaction = await registry.addRecipient( + address, + JSON.stringify(json), + { + value: deposit, } - } - - if (requestType === RequestTypeCode.Removal && recipient.verified) { - // Disallow contributions to removed recipient - project.isLocked = true - } - return project + ) + return transaction } export async function registerProject( @@ -505,4 +109,15 @@ export async function removeProject( return transaction } -export default { getProjects, getProject, registerProject } +export function create(): RecipientRegistryInterface { + return { + addRecipient, + registerProject, + removeProject, + rejectProject, + isSelfRegistration: true, + requireRegistrationDeposit: true, + } +} + +export default { create } diff --git a/vue-app/src/api/recipient-registry-simple.ts b/vue-app/src/api/recipient-registry-simple.ts index 189f28a71..fc835e4f5 100644 --- a/vue-app/src/api/recipient-registry-simple.ts +++ b/vue-app/src/api/recipient-registry-simple.ts @@ -1,23 +1,25 @@ -import { Contract, Event } from 'ethers' +import { BigNumber, Contract, Event, Signer } from 'ethers' +import { ContractTransaction } from '@ethersproject/contracts' + import { isHexString } from '@ethersproject/bytes' import { SimpleRecipientRegistry } from './abi' -import { provider, ipfsGatewayUrl } from './core' -import { Project } from './projects' +import { provider, ipfsGatewayUrl, chain } from './core' +import { RecipientRegistryInterface } from './types' +import { Project, toProjectInterface } from './projects' function decodeRecipientAdded(event: Event): Project { const args = event.args as any const metadata = JSON.parse(args._metadata) - return { + return toProjectInterface({ + ...metadata, id: args._recipientId, address: args._recipient, - name: metadata.name, - description: metadata.description, imageUrl: `${ipfsGatewayUrl}/ipfs/${metadata.imageHash}`, index: args._index.toNumber(), isHidden: false, isLocked: false, - } + }) } export async function getProjects( @@ -116,4 +118,61 @@ export async function getProject( return project } -export default { getProjects, getProject } +export function addRecipient( + registryAddress: string, + recipientData: any, + _deposit: BigNumber, + signer: Signer +): Promise { + const registry = new Contract( + registryAddress, + SimpleRecipientRegistry, + signer + ) + const { id, fund } = recipientData + if (!id) { + throw new Error('Missing metadata id') + } + + const { currentChainReceivingAddress: address } = fund + if (!address) { + throw new Error(`Missing recipient address for the ${chain.name} network`) + } + + const json = { id } + return registry.addRecipient(address, JSON.stringify(json)) +} + +function removeProject( + registryAddress: string, + recipientId: string, + signer: Signer +): Promise { + const registry = new Contract( + registryAddress, + SimpleRecipientRegistry, + signer + ) + return registry.removeRecipient(recipientId) +} + +function rejectProject() { + throw new Error('removeProject not implemented') +} + +function registerProject() { + throw new Error('removeProject not implemented') +} + +export function create(): RecipientRegistryInterface { + return { + addRecipient, + removeProject, + registerProject, + rejectProject, + isSelfRegistration: false, + requireRegistrationDeposit: false, + } +} + +export default { getProjects, getProject, create } diff --git a/vue-app/src/api/recipient-registry.ts b/vue-app/src/api/recipient-registry.ts new file mode 100644 index 000000000..ef1d574a8 --- /dev/null +++ b/vue-app/src/api/recipient-registry.ts @@ -0,0 +1,418 @@ +import { BigNumber, Contract, Signer } from 'ethers' +import sdk from '@/graphql/sdk' +import { BaseRecipientRegistry } from './abi' +import { + provider, + ipfsGatewayUrl, + recipientRegistryPolicy, + RecipientRegistryType, + recipientRegistryType, + chain, + QUERY_BATCH_SIZE, +} from './core' +import { + RegistryInfo, + RecipientRegistryRequestTypeCode as RequestTypeCode, + RecipientRegistryRequestStatus as RequestStatus, + RecipientRegistryRequest as Request, + RecipientRegistryRequestType as RequestType, +} from '@/api/types' +import OptimisticRegistry from './recipient-registry-optimistic' +import SimpleRegistry from './recipient-registry-simple' +import KlerosRegistry from './recipient-registry-kleros' +import { isHexString } from '@ethersproject/bytes' +import { Recipient } from '@/graphql/API' +import { Project } from './projects' +import { Metadata, MetadataFormData } from './metadata' +import { DateTime } from 'luxon' + +const registryLookup: Record = { + [RecipientRegistryType.OPTIMISTIC]: OptimisticRegistry.create, + [RecipientRegistryType.SIMPLE]: SimpleRegistry.create, + [RecipientRegistryType.KLEROS]: KlerosRegistry.create, +} + +export class RecipientRegistry { + static create(registryType: string) { + const createRegistry = registryLookup[registryType] + + if (createRegistry) { + return createRegistry() + } else { + throw new Error('Invalid registry type') + } + } +} + +export async function getRegistryInfo( + registryAddress: string +): Promise { + const data = await sdk.GetRecipientRegistry({ + registryAddress: registryAddress.toLowerCase(), + }) + + const recipientRegistry = data.recipientRegistry + const baseDeposit = BigNumber.from(recipientRegistry?.baseDeposit || 0) + const challengePeriodDuration = BigNumber.from( + recipientRegistry?.challengePeriodDuration || 0 + ) + const owner = recipientRegistry?.owner || '' + + /* TODO: get recipient count from the subgraph */ + const registryContract = new Contract( + registryAddress, + BaseRecipientRegistry, + provider + ) + const recipientCount = await registryContract.getRecipientCount() + + const registry = RecipientRegistry.create(recipientRegistryType) + return { + deposit: baseDeposit, + depositToken: chain.currency, + challengePeriodDuration: challengePeriodDuration.toNumber(), + listingPolicyUrl: `${ipfsGatewayUrl}/ipfs/${recipientRegistryPolicy}`, + recipientCount: recipientCount.toNumber(), + owner, + isSelfRegistration: registry.isSelfRegistration, + requireRegistrationDeposit: registry.requireRegistrationDeposit, + } +} + +function decodeProject(recipient: Partial): Project { + if (!recipient.id) { + throw new Error('Incorrect recipient data') + } + + const metadata = JSON.parse(recipient.recipientMetadata || '') + + // imageUrl is the legacy form property - fall back to this if bannerImageHash or thumbnailImageHash don't exist + const imageUrl = `${ipfsGatewayUrl}/ipfs/${metadata.imageHash}` + + let requester + if (recipient.requester) { + requester = recipient.requester + } + + return { + id: recipient.id, + address: recipient.recipientAddress || '', + requester, + name: metadata.name, + description: metadata.description, + imageUrl, + // Only unregistered project can have invalid index 0 + index: 0, + isHidden: false, + isLocked: false, + extra: { + submissionTime: Number(recipient.submissionTime), + }, + 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: metadata.bannerImageHash + ? `${ipfsGatewayUrl}/ipfs/${metadata.bannerImageHash}` + : imageUrl, + thumbnailImageUrl: metadata.thumbnailImageHash + ? `${ipfsGatewayUrl}/ipfs/${metadata.thumbnailImageHash}` + : imageUrl, + } +} + +/** + * Build a map to lookup metadata by id + * Retrieve metadata in batches to avoid hitting server too often + * @param ids a list of metadata id + * @returns a key value map of id and metadata + */ +async function buildMetadataMap( + ids: string[] +): Promise> { + const batches: string[][] = [] + const batchSize = QUERY_BATCH_SIZE + for (let i = 0; i < ids.length; i += batchSize) { + batches.push(ids.slice(i, batchSize + i)) + } + + const metadataBatches = await Promise.all( + batches.map((aBatch) => Metadata.getBatch(aBatch)) + ) + + const map = metadataBatches.reduce((res, batch) => { + for (let i = 0; i < batch.length; i++) { + const { id, metadata } = batch[i] || {} + if (id) { + res[id] = metadata + } + } + return res + }, {}) + + return map +} + +/** + * Add metadata to recipients if it's missing; + * @param recipients list of recipients to patch the metadata + * @returns recipients with metadata + */ +async function normalizeRecipients( + recipients: Partial[] +): Promise[]> { + const metadataIds = recipients.map(({ recipientMetadata }) => { + try { + const json = JSON.parse(recipientMetadata || '') + return json.id + } catch { + return null + } + }) + + const metadataMap = await buildMetadataMap(metadataIds.filter(Boolean)) + + return recipients.map((recipient, index) => { + const metadata = metadataMap[metadataIds[index]] + if (metadata) { + recipient.recipientMetadata = metadata + } + return recipient + }) +} + +export async function getProjects( + registryAddress: string, + startTime?: number, + endTime?: number +): Promise { + const data = await sdk.GetRecipients({ + registryAddress: registryAddress.toLowerCase(), + }) + + if (!data.recipients?.length) { + return [] + } + + const recipients = await normalizeRecipients(data.recipients) + const projects: Project[] = recipients + .map((recipient) => { + let project + try { + project = decodeProject(recipient) + } catch (err) { + return + } + + const submissionTime = Number(recipient.submissionTime) + + if (recipient.rejected) { + return + } + + const requestType = Number(recipient.requestType) + if (requestType === RequestTypeCode.Registration) { + if (recipient.verified) { + const addedAt = submissionTime + if (endTime && addedAt >= endTime) { + // Hide recipient if it is added after the end of round + project.isHidden = true + } + project.index = recipient.recipientIndex + return project + } else { + return + } + } + + if (requestType === RequestTypeCode.Removal) { + const removedAt = submissionTime + if (!startTime || removedAt <= startTime) { + // Start time not specified + // or recipient had been removed before start time + project.isHidden = true + } else { + // Disallow contributions to removed recipient, but don't hide it + project.isLocked = true + } + } + + return project + }) + .filter(Boolean) + + return projects +} + +export async function projectExists(recipientId: string): Promise { + if (!isHexString(recipientId, 32)) { + return false + } + + const data = await sdk.GetProject({ + recipientId, + }) + + if (!data.recipients?.length) { + // Project does not exist + return false + } + + const [recipient] = data.recipients + const requestType = Number(recipient.requestType) + if (requestType === RequestTypeCode.Removal && recipient.verified) { + return false + } + + return true +} + +export async function getProject(recipientId: string): Promise { + if (!isHexString(recipientId, 32)) { + return null + } + + const data = await sdk.GetProject({ + recipientId, + }) + + if (!data.recipients?.length) { + // Project does not exist + return null + } + + const [recipient] = await normalizeRecipients(data.recipients) + let project: Project + try { + project = decodeProject(recipient) + } catch { + // Invalid metadata + return null + } + + const requestType = Number(recipient.requestType) + if (requestType === RequestTypeCode.Registration) { + if (recipient.verified) { + project.index = recipient.recipientIndex + } else { + return null + } + } + + if (requestType === RequestTypeCode.Removal && recipient.verified) { + // Disallow contributions to removed recipient + project.isLocked = true + } + return project +} + +export async function getRequests( + registryInfo: RegistryInfo, + registryAddress: string +): Promise { + const data = await sdk.GetRecipients({ + registryAddress: registryAddress.toLowerCase(), + }) + + if (!data.recipients?.length) { + return [] + } + + const recipients = await normalizeRecipients(data.recipients) + + const requests: Record = {} + for (const recipient of recipients) { + let metadata: any = {} + try { + metadata = JSON.parse(recipient.recipientMetadata || '{}') + } catch { + // instead of throwing error, let it flow through so + // we can investigate the issue from the subgraph + metadata.name = 'N/A' + } + + const requestType = Number(recipient.requestType) + if (requestType === RequestTypeCode.Registration) { + // Registration request + const { + name, + description, + imageHash, + bannerImageHash, + thumbnailImageHash, + } = metadata + + metadata = { + name, + description, + imageUrl: `${ipfsGatewayUrl}/ipfs/${imageHash}`, + bannerImageUrl: `${ipfsGatewayUrl}/ipfs/${bannerImageHash}`, + thumbnailImageUrl: thumbnailImageHash + ? `${ipfsGatewayUrl}/ipfs/${thumbnailImageHash}` + : `${ipfsGatewayUrl}/ipfs/${imageHash}`, + } + } + + const submissionTime = Number(recipient.submissionTime) + const acceptanceDate = DateTime.fromSeconds( + submissionTime + registryInfo.challengePeriodDuration + ) + + let requester + if (recipient.requester) { + requester = recipient.requester + } + + const request: Request = { + transactionHash: + recipient.requestResolvedHash || recipient.requestSubmittedHash, + type: RequestType[RequestTypeCode[requestType]], + status: RequestStatus.Submitted, + acceptanceDate, + recipientId: recipient.id || '', + recipient: recipient.recipientAddress, + metadata, + requester, + } + + if (recipient.rejected) { + request.status = RequestStatus.Rejected + } + + if (recipient.verified) { + request.status = + requestType === RequestTypeCode.Removal + ? RequestStatus.Removed + : RequestStatus.Executed + } + + // In case there are two requests submissions events, we always prioritize + // the last one since you can only have one request per recipient + requests[request.recipientId] = request + } + return Object.keys(requests).map((recipientId) => requests[recipientId]) +} + +export async function addRecipient( + registryAddress: string, + recipientMetadata: MetadataFormData, + deposit: BigNumber, + signer: Signer +) { + const registry = RecipientRegistry.create(recipientRegistryType) + return registry.addRecipient( + registryAddress, + recipientMetadata, + deposit, + signer + ) +} + +export default { addRecipient, getProject, getProjects, projectExists } diff --git a/vue-app/src/api/recipient.ts b/vue-app/src/api/recipient.ts new file mode 100644 index 000000000..d001923b2 --- /dev/null +++ b/vue-app/src/api/recipient.ts @@ -0,0 +1,34 @@ +export interface RecipientApplicationData { + project: { + name: string + tagline: string + description: string + category: string + problemSpace: string + } + fund: { + receivingAddresses: string[] + 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 + id?: string +} diff --git a/vue-app/src/api/types.ts b/vue-app/src/api/types.ts new file mode 100644 index 000000000..6b529b26c --- /dev/null +++ b/vue-app/src/api/types.ts @@ -0,0 +1,64 @@ +import { DateTime } from 'luxon' +import { BigNumber } from 'ethers' + +export interface RegistryInfo { + deposit: BigNumber + depositToken: string + challengePeriodDuration: number + listingPolicyUrl: string + recipientCount: number + owner: string + isSelfRegistration: boolean + requireRegistrationDeposit: boolean +} + +export interface RecipientRegistryInterface { + addRecipient: Function + registerProject: Function + rejectProject: Function + removeProject: Function + isSelfRegistration?: boolean + requireRegistrationDeposit?: boolean +} + +// recipient registration request type +export enum RecipientRegistryRequestTypeCode { + Registration = 0, + Removal = 1, +} + +export enum RecipientRegistryRequestType { + Registration = 'Registration', + Removal = 'Removal', +} + +export enum RecipientRegistryRequestStatus { + Submitted = 'Needs review', + Accepted = 'Accepted', + Rejected = 'Rejected', + Executed = 'Live', + Removed = 'Removed', +} + +interface RecipientMetadata { + name: string + description: string + imageUrl: string + thumbnailImageUrl: string +} + +export interface RecipientRegistryRequest { + transactionHash: string + type: RecipientRegistryRequestType + status: RecipientRegistryRequestStatus + acceptanceDate: DateTime + recipientId: string + recipient: string + metadata: RecipientMetadata + requester: string +} + +export interface LinkInfo { + url: string + text: string +} diff --git a/vue-app/src/components/AddressWidget.vue b/vue-app/src/components/AddressWidget.vue new file mode 100644 index 000000000..cb53c49df --- /dev/null +++ b/vue-app/src/components/AddressWidget.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/vue-app/src/components/Box.vue b/vue-app/src/components/Box.vue new file mode 100644 index 000000000..b385022e1 --- /dev/null +++ b/vue-app/src/components/Box.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/vue-app/src/components/ClickableCard.vue b/vue-app/src/components/ClickableCard.vue new file mode 100644 index 000000000..10057333b --- /dev/null +++ b/vue-app/src/components/ClickableCard.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/vue-app/src/components/CriteriaModal.vue b/vue-app/src/components/CriteriaModal.vue index cba535d4f..9662a47a4 100644 --- a/vue-app/src/components/CriteriaModal.vue +++ b/vue-app/src/components/CriteriaModal.vue @@ -33,7 +33,10 @@ - Add project diff --git a/vue-app/src/components/Dropdown.vue b/vue-app/src/components/Dropdown.vue new file mode 100644 index 000000000..f7e42f5be --- /dev/null +++ b/vue-app/src/components/Dropdown.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/vue-app/src/components/FilterDropdown.vue b/vue-app/src/components/FilterDropdown.vue index 608e37b92..333efb5b2 100644 --- a/vue-app/src/components/FilterDropdown.vue +++ b/vue-app/src/components/FilterDropdown.vue @@ -124,7 +124,7 @@ export default class FilterDropdown extends Vue { text-transform: capitalize; line-height: 24px; &:hover { - background: var(--bg-secondary-highlight); + background: var(--bg-secondary-accent); } } .category-btn-selected { diff --git a/vue-app/src/components/FormNavigation.vue b/vue-app/src/components/FormNavigation.vue index 25ae655b8..c654390fd 100644 --- a/vue-app/src/components/FormNavigation.vue +++ b/vue-app/src/components/FormNavigation.vue @@ -30,8 +30,8 @@ import Vue from 'vue' import Component from 'vue-class-component' import { Prop } from 'vue-property-decorator' -import { User } from '@/api/user' import WalletWidget from '@/components/WalletWidget.vue' +import { User } from '@/api/user' @Component({ components: { diff --git a/vue-app/src/components/FormProgressWidget.vue b/vue-app/src/components/FormProgressWidget.vue index 36c421610..48f854441 100644 --- a/vue-app/src/components/FormProgressWidget.vue +++ b/vue-app/src/components/FormProgressWidget.vue @@ -44,7 +44,7 @@

Step {{ currentStep + 1 }} of {{ steps.length }}

- Cancel + Cancel
@@ -91,8 +91,9 @@ export default class FormProgressWidget extends Vue { @Prop() isNavDisabled!: boolean @Prop() isStepUnlocked!: (step: number) => boolean @Prop() isStepValid!: (step: number) => boolean - @Prop() handleStepNav!: () => void + @Prop() handleStepNav!: (step: number) => void @Prop() saveFormData!: (updateFurthest?: boolean) => void + @Prop() cancelRedirectUrl!: string } diff --git a/vue-app/src/components/IpfsImageUpload.vue b/vue-app/src/components/IpfsImageUpload.vue index 03a505c74..bd3c52e69 100644 --- a/vue-app/src/components/IpfsImageUpload.vue +++ b/vue-app/src/components/IpfsImageUpload.vue @@ -81,9 +81,9 @@ export default class IpfsImageUpload extends Vue { @Prop() description!: string @Prop() formProp!: string @Prop() onUpload!: (key: string, value: string) => void + @Prop() hash!: string ipfs: IPFS | null = null - hash = '' loading = false loadedImageFile: File | null = null loadedImageHeight: number | null = null @@ -135,7 +135,6 @@ export default class IpfsImageUpload extends Vue { this.ipfs .add(this.loadedImageFile) .then((hash) => { - this.hash = hash /* eslint-disable-next-line no-console */ console.log(`Uploaded file hash:`, hash) this.onUpload(this.formProp, hash) @@ -151,7 +150,6 @@ export default class IpfsImageUpload extends Vue { } handleRemoveImage(): void { - this.hash = '' this.loading = false this.error = '' this.loadedImageFile = null diff --git a/vue-app/src/components/Links.vue b/vue-app/src/components/Links.vue index 934456610..6c84256ad 100644 --- a/vue-app/src/components/Links.vue +++ b/vue-app/src/components/Links.vue @@ -2,14 +2,14 @@ - + @@ -26,14 +26,18 @@ export default class extends Vue { @Prop() hideArrow!: boolean @Prop() ariaLabel!: string - isExternal = false + get destination(): string | { [key: string]: any } { + return this.href ?? this.to + } - mounted() { - if (this.href) { - this.to = this.href - } - if (typeof this.to === 'string') { - this.isExternal = this.to.includes('http') || this.to.includes('mailto:') + get isExternal(): boolean { + if (typeof this.destination === 'string') { + return ( + this.destination.includes('http') || + this.destination.includes('mailto:') + ) + } else { + return false } } } diff --git a/vue-app/src/components/MetadataSubmissionWidget.vue b/vue-app/src/components/MetadataSubmissionWidget.vue new file mode 100644 index 000000000..f06117839 --- /dev/null +++ b/vue-app/src/components/MetadataSubmissionWidget.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/vue-app/src/components/NavBar.vue b/vue-app/src/components/NavBar.vue index 690b568e6..a9bf16244 100644 --- a/vue-app/src/components/NavBar.vue +++ b/vue-app/src/components/NavBar.vue @@ -71,6 +71,7 @@ export default class NavBar extends Vue { { to: '/about/how-it-works', text: 'How it works', emoji: '⚙️' }, { to: '/about/maci', text: 'Bribery protection', emoji: '🤑' }, { to: '/about/sybil-resistance', text: 'Sybil resistance', emoji: '👤' }, + { to: '/about/layer-2', text: 'Layer 2', emoji: '🚀' }, { to: 'https://github.com/clrfund/monorepo/', text: 'Code', @@ -79,22 +80,21 @@ export default class NavBar extends Vue { { to: '/recipients', text: 'Recipients', - emoji: '🚀', + emoji: '💎', }, - ] + { + to: '/metadata', + text: 'Metadata', + emoji: '📃', + }, + ].filter((item) => { + return chain.isLayer2 || item.text !== 'Layer 2' + }) created() { const savedTheme = lsGet(this.themeKey) const theme = isValidTheme(savedTheme) ? savedTheme : getOsColorScheme() this.$store.commit(TOGGLE_THEME, theme) - - if (chain.isLayer2) { - this.dropdownItems.splice(-1, 0, { - to: '/about/layer-2', - text: 'Layer 2', - emoji: '🚀', - }) - } } closeHelpDropdown(): void { diff --git a/vue-app/src/components/Panel.vue b/vue-app/src/components/Panel.vue new file mode 100644 index 000000000..2bc8d1528 --- /dev/null +++ b/vue-app/src/components/Panel.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/vue-app/src/components/ProjectProfile.vue b/vue-app/src/components/ProjectProfile.vue index 5a05a59be..ea1ccf080 100644 --- a/vue-app/src/components/ProjectProfile.vue +++ b/vue-app/src/components/ProjectProfile.vue @@ -65,10 +65,10 @@
Recipient address
- {{ addressName }} + {{ addressName || 'N/A' }}
-
+
-
+
-
+
Waiting for transaction to be mined...
@@ -26,7 +20,7 @@ Check your wallet or {{ blockExplorerLabel }} for more info.
-
+
Total to submit @@ -201,13 +220,15 @@ export default class RecipientSubmissionWidget extends Vue { padding-bottom: 2rem; } +.no-deposit { + border-radius: calc(1rem - 1px); + margin-bottom: 0rem; +} + .tx-progress-area { background: var(--bg-inactive); text-align: center; - border-radius: calc(1rem - 1px) calc(1rem - 1px) 0 0; - padding: 1.5rem; width: -webkit-fill-available; - margin-bottom: 2rem; display: flex; align-items: center; gap: 1rem; @@ -215,17 +236,20 @@ export default class RecipientSubmissionWidget extends Vue { font-weight: 500; } -.tx-progress-area-no-notice { - background: var(--bg-inactive); - text-align: center; +.error-only { + border-radius: calc(1rem - 1px); + padding: 1.5rem; +} + +.error-and-info { + border-radius: calc(1rem - 1px) calc(1rem - 1px) 0 0; + margin-bottom: 2rem; + padding: 1.5rem; +} + +.info-only { border-radius: calc(3rem - 1px) calc(3rem - 1px) 0 0; - width: -webkit-fill-available; margin-bottom: 2rem; - display: flex; - align-items: center; - gap: 1rem; - justify-content: center; - font-weight: 500; height: 1rem; } @@ -297,6 +321,8 @@ export default class RecipientSubmissionWidget extends Vue { .warning-text { font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; } .warning-text, diff --git a/vue-app/src/components/SearchInput.vue b/vue-app/src/components/SearchInput.vue new file mode 100644 index 000000000..c0248684f --- /dev/null +++ b/vue-app/src/components/SearchInput.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/vue-app/src/components/TransactionModal.vue b/vue-app/src/components/TransactionModal.vue index a303356e7..60e0a8ad9 100644 --- a/vue-app/src/components/TransactionModal.vue +++ b/vue-app/src/components/TransactionModal.vue @@ -12,13 +12,15 @@ } " > - +
+ +
@@ -71,6 +73,11 @@ export default class TransactionModal extends Vue { padding: 1.5rem; } +.btn-row { + display: flex; + justify-content: center; +} + .close-btn { margin-top: $modal-space; } diff --git a/vue-app/src/components/TransactionReceipt.vue b/vue-app/src/components/TransactionReceipt.vue index 2a1202ae9..04fc7c512 100644 --- a/vue-app/src/components/TransactionReceipt.vue +++ b/vue-app/src/components/TransactionReceipt.vue @@ -41,10 +41,16 @@ import { Prop } from 'vue-property-decorator' import Loader from '@/components/Loader.vue' import CopyButton from '@/components/CopyButton.vue' import Links from '@/components/Links.vue' -import { chain } from '@/api/core' +import { chain as defaultChain } from '@/api/core' import { isTransactionMined } from '@/utils/contracts' import { renderAddressOrHash } from '@/utils/accounts' +interface ChainInterface { + explorer: string + explorerLabel: string + explorerLogo: string +} + @Component({ components: { Loader, CopyButton, Links }, }) @@ -53,6 +59,7 @@ export default class TransactionReceipt extends Vue { isCopied = false @Prop() hash!: string + @Prop() chain!: ChainInterface updateIsCopied(value: boolean): void { this.isCopied = value @@ -75,10 +82,11 @@ export default class TransactionReceipt extends Vue { } get blockExplorer(): { label: string; url: string; logo: string } { + const chainInfo = this.chain ?? defaultChain return { - label: chain.explorerLabel, - url: `${chain.explorer}/tx/${this.hash}`, - logo: chain.explorerLogo, + label: chainInfo.explorerLabel, + url: `${chainInfo.explorer}/tx/${this.hash}`, + logo: chainInfo.explorerLogo, } } } diff --git a/vue-app/src/components/TransactionResult.vue b/vue-app/src/components/TransactionResult.vue new file mode 100644 index 000000000..0800ddab4 --- /dev/null +++ b/vue-app/src/components/TransactionResult.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/vue-app/src/graphql/API.ts b/vue-app/src/graphql/API.ts index cb34548d9..51a4179d3 100644 --- a/vue-app/src/graphql/API.ts +++ b/vue-app/src/graphql/API.ts @@ -72,6 +72,7 @@ export type Contribution_Filter = { contributor_ends_with_nocase: Maybe; contributor_not_ends_with: Maybe; contributor_not_ends_with_nocase: Maybe; + contributor_: Maybe; fundingRound: Maybe; fundingRound_not: Maybe; fundingRound_gt: Maybe; @@ -92,6 +93,7 @@ export type Contribution_Filter = { fundingRound_ends_with_nocase: Maybe; fundingRound_not_ends_with: Maybe; fundingRound_not_ends_with_nocase: Maybe; + fundingRound_: Maybe; amount: Maybe; amount_not: Maybe; amount_gt: Maybe; @@ -231,6 +233,7 @@ export type ContributorRegistry_Filter = { fundingRoundFactory_ends_with_nocase: Maybe; fundingRoundFactory_not_ends_with: Maybe; fundingRoundFactory_not_ends_with_nocase: Maybe; + fundingRoundFactory_: Maybe; context: Maybe; context_not: Maybe; context_gt: Maybe; @@ -257,6 +260,7 @@ export type ContributorRegistry_Filter = { owner_not_in: Maybe>; owner_contains: Maybe; owner_not_contains: Maybe; + contributors_: Maybe; createdAt: Maybe; createdAt_not: Maybe; createdAt_gt: Maybe; @@ -340,6 +344,8 @@ export type Contributor_Filter = { contributorRegistry_ends_with_nocase: Maybe; contributorRegistry_not_ends_with: Maybe; contributorRegistry_not_ends_with_nocase: Maybe; + contributorRegistry_: Maybe; + votes_: Maybe; verified: Maybe; verified_not: Maybe; verified_in: Maybe>; @@ -376,6 +382,8 @@ export type Contributor_Filter = { fundingRounds_contains_nocase: Maybe>; fundingRounds_not_contains: Maybe>; fundingRounds_not_contains_nocase: Maybe>; + fundingRounds_: Maybe; + contributions_: Maybe; createdAt: Maybe; createdAt_not: Maybe; createdAt_gt: Maybe; @@ -560,6 +568,7 @@ export type Donation_Filter = { recipient_ends_with_nocase: Maybe; recipient_not_ends_with: Maybe; recipient_not_ends_with_nocase: Maybe; + recipient_: Maybe; fundingRound: Maybe; fundingRound_not: Maybe; fundingRound_gt: Maybe; @@ -580,6 +589,7 @@ export type Donation_Filter = { fundingRound_ends_with_nocase: Maybe; fundingRound_not_ends_with: Maybe; fundingRound_not_ends_with_nocase: Maybe; + fundingRound_: Maybe; amount: Maybe; amount_not: Maybe; amount_gt: Maybe; @@ -792,6 +802,7 @@ export type FundingRoundFactory_Filter = { contributorRegistry_ends_with_nocase: Maybe; contributorRegistry_not_ends_with: Maybe; contributorRegistry_not_ends_with_nocase: Maybe; + contributorRegistry_: Maybe; contributorRegistryAddress: Maybe; contributorRegistryAddress_not: Maybe; contributorRegistryAddress_in: Maybe>; @@ -818,6 +829,7 @@ export type FundingRoundFactory_Filter = { recipientRegistry_ends_with_nocase: Maybe; recipientRegistry_not_ends_with: Maybe; recipientRegistry_not_ends_with_nocase: Maybe; + recipientRegistry_: Maybe; recipientRegistryAddress: Maybe; recipientRegistryAddress_not: Maybe; recipientRegistryAddress_in: Maybe>; @@ -844,6 +856,7 @@ export type FundingRoundFactory_Filter = { currentRound_ends_with_nocase: Maybe; currentRound_not_ends_with: Maybe; currentRound_not_ends_with_nocase: Maybe; + currentRound_: Maybe; maciFactory: Maybe; maciFactory_not: Maybe; maciFactory_in: Maybe>; @@ -962,6 +975,7 @@ export type FundingRoundFactory_Filter = { maxVoteOptions_lte: Maybe; maxVoteOptions_in: Maybe>; maxVoteOptions_not_in: Maybe>; + fundingRounds_: Maybe; createdAt: Maybe; createdAt_not: Maybe; createdAt_gt: Maybe; @@ -1064,12 +1078,14 @@ export type FundingRound_Filter = { fundingRoundFactory_ends_with_nocase: Maybe; fundingRoundFactory_not_ends_with: Maybe; fundingRoundFactory_not_ends_with_nocase: Maybe; + fundingRoundFactory_: Maybe; maci: Maybe; maci_not: Maybe; maci_in: Maybe>; maci_not_in: Maybe>; maci_contains: Maybe; maci_not_contains: Maybe; + messages_: Maybe; recipientRegistry: Maybe; recipientRegistry_not: Maybe; recipientRegistry_gt: Maybe; @@ -1090,6 +1106,7 @@ export type FundingRound_Filter = { recipientRegistry_ends_with_nocase: Maybe; recipientRegistry_not_ends_with: Maybe; recipientRegistry_not_ends_with_nocase: Maybe; + recipientRegistry_: Maybe; recipientRegistryAddress: Maybe; recipientRegistryAddress_not: Maybe; recipientRegistryAddress_in: Maybe>; @@ -1116,6 +1133,7 @@ export type FundingRound_Filter = { contributorRegistry_ends_with_nocase: Maybe; contributorRegistry_not_ends_with: Maybe; contributorRegistry_not_ends_with_nocase: Maybe; + contributorRegistry_: Maybe; contributorRegistryAddress: Maybe; contributorRegistryAddress_not: Maybe; contributorRegistryAddress_in: Maybe>; @@ -1234,6 +1252,10 @@ export type FundingRound_Filter = { tallyHash_ends_with_nocase: Maybe; tallyHash_not_ends_with: Maybe; tallyHash_not_ends_with_nocase: Maybe; + recipients_: Maybe; + contributors_: Maybe; + contributions_: Maybe; + votes_: Maybe; createdAt: Maybe; createdAt_not: Maybe; createdAt_gt: Maybe; @@ -1362,6 +1384,7 @@ export type Message_Filter = { publicKey_ends_with_nocase: Maybe; publicKey_not_ends_with: Maybe; publicKey_not_ends_with_nocase: Maybe; + publicKey_: Maybe; fundingRound: Maybe; fundingRound_not: Maybe; fundingRound_gt: Maybe; @@ -1382,6 +1405,7 @@ export type Message_Filter = { fundingRound_ends_with_nocase: Maybe; fundingRound_not_ends_with: Maybe; fundingRound_not_ends_with_nocase: Maybe; + fundingRound_: Maybe; timestamp: Maybe; timestamp_not: Maybe; timestamp_gt: Maybe; @@ -1470,6 +1494,8 @@ export type PublicKey_Filter = { fundingRound_ends_with_nocase: Maybe; fundingRound_not_ends_with: Maybe; fundingRound_not_ends_with_nocase: Maybe; + fundingRound_: Maybe; + messages_: Maybe; x: Maybe; x_not: Maybe; x_gt: Maybe; @@ -1879,6 +1905,7 @@ export type RecipientRegistry_Filter = { fundingRoundFactory_ends_with_nocase: Maybe; fundingRoundFactory_not_ends_with: Maybe; fundingRoundFactory_not_ends_with_nocase: Maybe; + fundingRoundFactory_: Maybe; baseDeposit: Maybe; baseDeposit_not: Maybe; baseDeposit_gt: Maybe; @@ -1915,6 +1942,7 @@ export type RecipientRegistry_Filter = { owner_not_in: Maybe>; owner_contains: Maybe; owner_not_contains: Maybe; + recipients_: Maybe; createdAt: Maybe; createdAt_not: Maybe; createdAt_gt: Maybe; @@ -2001,6 +2029,7 @@ export type Recipient_Filter = { recipientRegistry_ends_with_nocase: Maybe; recipientRegistry_not_ends_with: Maybe; recipientRegistry_not_ends_with_nocase: Maybe; + recipientRegistry_: Maybe; recipientIndex: Maybe; recipientIndex_not: Maybe; recipientIndex_gt: Maybe; @@ -2137,6 +2166,8 @@ export type Recipient_Filter = { fundingRounds_contains_nocase: Maybe>; fundingRounds_not_contains: Maybe>; fundingRounds_not_contains_nocase: Maybe>; + fundingRounds_: Maybe; + donations_: Maybe; createdAt: Maybe; createdAt_not: Maybe; createdAt_gt: Maybe; @@ -2617,6 +2648,7 @@ export type Vote_Filter = { contributor_ends_with_nocase: Maybe; contributor_not_ends_with: Maybe; contributor_not_ends_with_nocase: Maybe; + contributor_: Maybe; fundingRound: Maybe; fundingRound_not: Maybe; fundingRound_gt: Maybe; @@ -2637,6 +2669,7 @@ export type Vote_Filter = { fundingRound_ends_with_nocase: Maybe; fundingRound_not_ends_with: Maybe; fundingRound_not_ends_with_nocase: Maybe; + fundingRound_: Maybe; voterAddress: Maybe; voterAddress_not: Maybe; voterAddress_in: Maybe>; @@ -2730,6 +2763,13 @@ export type GetRecipientDonationsQueryVariables = Exact<{ export type GetRecipientDonationsQuery = { __typename?: 'Query', donations: Array<{ __typename?: 'Donation', id: string }> }; +export type GetRecipientRegistryQueryVariables = Exact<{ + registryAddress: Scalars['ID']; +}>; + + +export type GetRecipientRegistryQuery = { __typename?: 'Query', recipientRegistry: Maybe<{ __typename?: 'RecipientRegistry', id: string, baseDeposit: Maybe, challengePeriodDuration: Maybe, owner: Maybe, controller: Maybe, maxRecipients: Maybe }> }; + export type GetRecipientsQueryVariables = Exact<{ registryAddress: Scalars['String']; }>; @@ -2811,6 +2851,18 @@ export const GetRecipientDonationsDocument = gql` } } `; +export const GetRecipientRegistryDocument = gql` + query GetRecipientRegistry($registryAddress: ID!) { + recipientRegistry(id: $registryAddress) { + id + baseDeposit + challengePeriodDuration + owner + controller + maxRecipients + } +} + `; export const GetRecipientsDocument = gql` query GetRecipients($registryAddress: String!) { recipients(where: {recipientRegistry: $registryAddress}) { @@ -2865,6 +2917,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = GetRecipientDonations(variables: GetRecipientDonationsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetRecipientDonationsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'GetRecipientDonations'); }, + GetRecipientRegistry(variables: GetRecipientRegistryQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(GetRecipientRegistryDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'GetRecipientRegistry'); + }, GetRecipients(variables: GetRecipientsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetRecipientsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'GetRecipients'); }, diff --git a/vue-app/src/graphql/queries/GetRecipientRegistry.graphql b/vue-app/src/graphql/queries/GetRecipientRegistry.graphql new file mode 100644 index 000000000..9f0b2917a --- /dev/null +++ b/vue-app/src/graphql/queries/GetRecipientRegistry.graphql @@ -0,0 +1,10 @@ +query GetRecipientRegistry($registryAddress: ID!) { + recipientRegistry(id: $registryAddress) { + id + baseDeposit + challengePeriodDuration + owner + controller + maxRecipients + } +} \ No newline at end of file diff --git a/vue-app/src/plugins/Web3/constants/chains.ts b/vue-app/src/plugins/Web3/constants/chains.ts index 8a6dccc58..7035a8041 100644 --- a/vue-app/src/plugins/Web3/constants/chains.ts +++ b/vue-app/src/plugins/Web3/constants/chains.ts @@ -20,6 +20,10 @@ export interface ChainInfo { explorerLabel: string rpcUrl?: string bridge?: string + shortName: string + // The subgraph network name from: + // https://thegraph.com/docs/en/developer/create-subgraph-hosted/#supported-networks + name: string } } @@ -32,6 +36,8 @@ export const CHAIN_INFO: ChainInfo = { explorer: 'https://etherscan.io', explorerLogo: 'etherscan.svg', explorerLabel: 'Etherscan', + shortName: 'eth', + name: 'mainnet', }, [ChainId.GOERLI]: { label: 'Goerli', @@ -41,6 +47,8 @@ export const CHAIN_INFO: ChainInfo = { explorer: 'https://goerli.etherscan.io', explorerLogo: 'etherscan.svg', explorerLabel: 'Etherscan', + shortName: 'gor', + name: 'goerli', }, [ChainId.HARDHAT]: { label: 'Arbitrum Hardhat', @@ -52,6 +60,8 @@ export const CHAIN_INFO: ChainInfo = { explorerLabel: 'Arbiscan', rpcUrl: 'https://rinkeby.arbitrum.io/rpc', bridge: 'https://bridge.arbitrum.io', + shortName: 'arb-rinkeby', + name: 'arbitrum-rinkeby', }, [ChainId.ARBITRUM_ONE]: { label: 'Arbitrum', @@ -63,6 +73,8 @@ export const CHAIN_INFO: ChainInfo = { explorerLabel: 'Arbiscan', rpcUrl: 'https://arb1.arbitrum.io/rpc', bridge: 'https://bridge.arbitrum.io', + shortName: 'arb1', + name: 'arbitrum-one', }, [ChainId.ARBITRUM_RINKEBY]: { label: 'Arbitrum Rinkeby', @@ -74,6 +86,8 @@ export const CHAIN_INFO: ChainInfo = { explorerLabel: 'Arbiscan', rpcUrl: 'https://rinkeby.arbitrum.io/rpc', bridge: 'https://bridge.arbitrum.io', + shortName: 'arb-rinkeby', + name: 'arbitrum-rinkeby', }, [ChainId.OPTIMISM]: { label: 'Optimism', @@ -85,6 +99,8 @@ export const CHAIN_INFO: ChainInfo = { explorerLabel: 'Etherscan', rpcUrl: 'https://mainnet.optimism.io', bridge: 'https://gateway.optimism.io', + shortName: 'oeth', + name: 'optimism', }, [ChainId.XDAI]: { label: 'xDai', @@ -96,6 +112,8 @@ export const CHAIN_INFO: ChainInfo = { explorerLabel: 'Blockscout', rpcUrl: 'https://rpc.xdaichain.com/', bridge: 'https://bridge.xdaichain.com', + shortName: 'gno', + name: 'xdai', }, [ChainId.POLYGON]: { label: 'Polygon', @@ -107,5 +125,16 @@ export const CHAIN_INFO: ChainInfo = { explorerLabel: 'Polygonscan', rpcUrl: 'https://rpc-mainnet.matic.network', bridge: 'https://wallet.polygon.technology', + shortName: 'MATIC', + name: 'matic', }, } + +// a lookup table for chain id +export const CHAIN_ID: Record = Object.entries( + CHAIN_INFO +).reduce((ids, [id, chain]) => { + ids[chain.name] = id + ids[chain.shortName] = id + return ids +}, {}) diff --git a/vue-app/src/plugins/Web3/index.ts b/vue-app/src/plugins/Web3/index.ts index 7ac3d3a2d..6276227a2 100644 --- a/vue-app/src/plugins/Web3/index.ts +++ b/vue-app/src/plugins/Web3/index.ts @@ -60,6 +60,7 @@ export default { // Check if user is using the supported chain id const supportedChainId = Number(process.env.VUE_APP_ETHEREUM_API_CHAINID) + if (conn.chainId !== supportedChainId) { if (conn.provider instanceof WalletConnectProvider) { // Close walletconnect session diff --git a/vue-app/src/router/index.ts b/vue-app/src/router/index.ts index 09932d59a..dadf0c3b0 100644 --- a/vue-app/src/router/index.ts +++ b/vue-app/src/router/index.ts @@ -25,6 +25,12 @@ import VerifyView from '../views/Verify.vue' import RecipientRegistryView from '@/views/RecipientRegistry.vue' import CartView from '@/views/Cart.vue' import TransactionSuccess from '@/views/TransactionSuccess.vue' +import MetadataDetail from '@/views/Metadata.vue' +import MetadataRegistry from '@/views/MetadataRegistry.vue' +import MetadataFormAdd from '@/views/MetadataFormAdd.vue' +import MetadataFormEdit from '@/views/MetadataFormEdit.vue' +import MetadataTransactionSuccess from '@/views/MetadataTransactionSuccess.vue' +import NotFound from '@/views/NotFound.vue' import BrightIdGuide from '@/views/BrightIdGuide.vue' import BrightIdSponsor from '@/views/BrightIdSponsor.vue' import BrightIdSponsored from '@/views/BrightIdSponsored.vue' @@ -212,14 +218,13 @@ const routes = [ }, }, { - path: '/join/:step', + path: '/join/:step/:id?', name: 'join-step', component: JoinView, meta: { title: 'Project Application', }, }, - { path: '/cart', name: 'cart', @@ -236,6 +241,46 @@ const routes = [ title: 'Transaction Success', }, }, + { + path: '/metadata-success/:hash/:id', + name: 'metadata-success', + component: MetadataTransactionSuccess, + meta: { + title: 'Metadata Transaction Success', + }, + }, + { + path: '/metadata', + name: 'metadata-registry', + component: MetadataRegistry, + meta: { + title: 'Metadata Registry', + }, + }, + { + path: '/metadata/:id', + name: 'metadata', + component: MetadataDetail, + meta: { + title: 'Metadata Detail', + }, + }, + { + path: '/metadata/:id/edit/:step', + name: 'metadata-edit', + component: MetadataFormEdit, + meta: { + title: 'Edit Metadata', + }, + }, + { + path: '/metadata/new/:step', + name: 'metadata-new', + component: MetadataFormAdd, + meta: { + title: 'Add Metadata', + }, + }, { path: '/brightid', name: 'brightid', @@ -260,6 +305,18 @@ const routes = [ title: 'Sponsored', }, }, + { + path: '/not-found', + name: 'not-found', + component: NotFound, + meta: { + title: 'Page Not Found', + }, + }, + { + path: '*', + redirect: '/not-found', + }, ] const router = new VueRouter({ base: window.location.pathname, diff --git a/vue-app/src/store/actions.ts b/vue-app/src/store/actions.ts index 98298950d..5db563c92 100644 --- a/vue-app/src/store/actions.ts +++ b/vue-app/src/store/actions.ts @@ -19,7 +19,7 @@ import { RoundStatus, getRoundInfo } from '@/api/round' import { storage } from '@/api/storage' import { getTally } from '@/api/tally' import { getEtherBalance, getTokenBalance, isVerifiedUser } from '@/api/user' -import { getRegistryInfo } from '@/api/recipient-registry-optimistic' +import { getRegistryInfo } from '@/api/recipient-registry' // Constants import { diff --git a/vue-app/src/store/getters.ts b/vue-app/src/store/getters.ts index 6dda075fa..323072821 100644 --- a/vue-app/src/store/getters.ts +++ b/vue-app/src/store/getters.ts @@ -4,16 +4,14 @@ import { DateTime } from 'luxon' // API import { CartItem, Contributor } from '@/api/contributions' -import { recipientRegistryType, operator } from '@/api/core' +import { operator } from '@/api/core' import { RoundInfo, RoundStatus } from '@/api/round' import { Tally } from '@/api/tally' import { User } from '@/api/user' import { Factory } from '@/api/factory' import { MACIFactory } from '@/api/maci-factory' -import { - RecipientApplicationData, - RegistryInfo, -} from '@/api/recipient-registry-optimistic' +import { RegistryInfo } from '@/api/types' +import { RecipientApplicationData } from '@/api/recipient' // Utils import { isSameAddress } from '@/utils/accounts' @@ -46,9 +44,7 @@ const getters = { } const challengePeriodDuration = - recipientRegistryType === 'optimistic' - ? state.recipientRegistryInfo.challengePeriodDuration - : 0 + state.recipientRegistryInfo.challengePeriodDuration const deadline = state.currentRound.signUpDeadline.minus({ seconds: challengePeriodDuration, @@ -83,13 +79,6 @@ const getters = { getters.recipientSpacesRemaining < 20 ) }, - isRoundBufferPhase: (state: RootState, getters): boolean => { - return ( - !!state.currentRound && - !getters.isJoinPhase && - !hasDateElapsed(state.currentRound.signUpDeadline) - ) - }, isRoundContributionPhase: (state: RootState): boolean => { return ( !!state.currentRound && @@ -158,7 +147,13 @@ const getters = { ) }, isRecipientRegistryOwner: (state: RootState): boolean => { - if (!state.currentUser || !state.recipientRegistryInfo) { + if ( + !state.currentUser || + !state.recipientRegistryInfo || + !state.recipientRegistryInfo.owner + ) { + // return false if no owner or logged in user information + // e.g. the kleros recipient registry does not have a owner return false } return isSameAddress( @@ -223,9 +218,33 @@ const getters = { return nativeTokenDecimals }, + isSelfRegistration: (state: RootState): boolean => { + return !!state.recipientRegistryInfo?.isSelfRegistration + }, + requireRegistrationDeposit: (state: RootState): boolean => { + return !!state.recipientRegistryInfo?.requireRegistrationDeposit + }, + canAddProject: (_, getters): boolean => { + const { + requireRegistrationDeposit, + isRecipientRegistryOwner, + isRecipientRegistryFull, + isRoundJoinPhase, + } = getters + + return ( + (requireRegistrationDeposit || isRecipientRegistryOwner) && + !isRecipientRegistryFull && + isRoundJoinPhase + ) + }, + joinFormUrl: + () => + (metadataId?: string): string => { + return metadataId ? `/join/metadata/${metadataId}` : '/join/project' + }, maxRecipients: (state: RootState): number | undefined => { const { currentRound, maciFactory } = state - if (currentRound) { return currentRound.maxRecipients } diff --git a/vue-app/src/store/mutation-types.ts b/vue-app/src/store/mutation-types.ts index 6f7ff0d61..b59ab4f5e 100644 --- a/vue-app/src/store/mutation-types.ts +++ b/vue-app/src/store/mutation-types.ts @@ -14,6 +14,8 @@ export const REMOVE_CART_ITEM = 'REMOVE_CART_ITEM' export const CLEAR_CART = 'CLEAR_CART' export const SET_RECIPIENT_DATA = 'SET_RECIPIENT_DATA' export const RESET_RECIPIENT_DATA = 'RESET_RECIPIENT_DATA' +export const SET_METADATA = 'SET_METADATA' +export const RESET_METADATA = 'RESET_METADATA' export const TOGGLE_SHOW_CART_PANEL = 'TOGGLE_SHOW_CART_PANEL' export const RESTORE_COMMITTED_CART_TO_LOCAL_CART = 'RESTORE_COMMITTED_CART_TO_LOCAL_CART' diff --git a/vue-app/src/store/mutations.ts b/vue-app/src/store/mutations.ts index d586d40b3..94932e62c 100644 --- a/vue-app/src/store/mutations.ts +++ b/vue-app/src/store/mutations.ts @@ -9,10 +9,9 @@ import { Tally } from '@/api/tally' import { User } from '@/api/user' import { Factory } from '@/api/factory' import { MACIFactory } from '@/api/maci-factory' -import { - RecipientApplicationData, - RegistryInfo, -} from '@/api/recipient-registry-optimistic' +import { RecipientApplicationData } from '@/api/recipient' +import { MetadataFormData } from '@/api/metadata' +import { RegistryInfo } from '@/api/types' // Constants import { @@ -29,6 +28,8 @@ import { SET_CURRENT_USER, SET_RECIPIENT_DATA, RESET_RECIPIENT_DATA, + SET_METADATA, + RESET_METADATA, SET_RECIPIENT_REGISTRY_ADDRESS, SET_RECIPIENT_REGISTRY_INFO, TOGGLE_SHOW_CART_PANEL, @@ -144,19 +145,24 @@ const mutations = { state, payload: { updatedData: RecipientApplicationData - step: string - stepNumber: number } ) { - if (!state.recipient) { - state.recipient = payload.updatedData - } else { - state.recipient[payload.step] = payload.updatedData[payload.step] - } + state.recipient = payload.updatedData }, [RESET_RECIPIENT_DATA](state) { state.recipient = null }, + [SET_METADATA]( + state, + payload: { + updatedData: MetadataFormData + } + ) { + state.metadata = payload.updatedData + }, + [RESET_METADATA](state) { + state.metadata = null + }, [TOGGLE_SHOW_CART_PANEL](state, isOpen: boolean | undefined) { // Handle the case of both null and undefined if (isOpen != null) { diff --git a/vue-app/src/views/AboutHowItWorks.vue b/vue-app/src/views/AboutHowItWorks.vue index 091e0cce5..b2481f0ed 100644 --- a/vue-app/src/views/AboutHowItWorks.vue +++ b/vue-app/src/views/AboutHowItWorks.vue @@ -83,7 +83,7 @@

- If you dont contribute in the contribution phase, the round is over for + If you don't contribute in the contribution phase, the round is over for you once this phase ends.

diff --git a/vue-app/src/views/AboutRecipients.vue b/vue-app/src/views/AboutRecipients.vue index a24ecc9b6..f80bd1b13 100644 --- a/vue-app/src/views/AboutRecipients.vue +++ b/vue-app/src/views/AboutRecipients.vue @@ -37,9 +37,14 @@

Register your project

- In order to participate in a funding round as a project, you'll need to - submit an application to join the recipient registry (via an on-chain - transaction). + In order to participate in a funding round as a project, + you'll need to submit an application to join the recipient registry + (via an on-chain transaction)you'll need to contact the round coordinator to submit an + application.

MACI, our anti-bribery tech, currently limits the amount of projects @@ -60,32 +65,43 @@

  • Once you're familiar with the criteria and you're sure your project - meets them, click "Add project." You'll see a series of forms to fill - out asking for more information about your project. -
  • -
  • - With the forms finished, you can finish your submission: -
      -
    1. Connect to the right network via your wallet of choice.
    2. -
    3. - Send a transaction (with a deposit of {{ depositAmount }} - {{ depositToken }}) to the registry contract. -
    4. -
    + meets them, + click "Add project." You'll see a series of forms to fill out asking + for more information about your project + contact the round coordinator to add your project to the recipient + registry.
  • + -

    - Projects are accepted by default, but the registry admin may remove - projects that don't meet the criteria. -

    -

    - In any case, your - {{ depositToken }} will be returned once your application has been either - accepted or denied. Note that metadata pointing to all your project - information (but not contact information) will be stored publicly - on-chain. -

    +

    Claim your funds

    After a clr.fund round is finished, it's simple to claim your project's diff --git a/vue-app/src/views/Join.vue b/vue-app/src/views/Join.vue index 0959c94c7..5a3f79bd6 100644 --- a/vue-app/src/views/Join.vue +++ b/vue-app/src/views/Join.vue @@ -1,1562 +1,24 @@