From f0d81b4ab73c5fd51e49f84c74d8dd043f1f34e9 Mon Sep 17 00:00:00 2001 From: Matt Casey Date: Tue, 18 Feb 2025 08:59:35 -0700 Subject: [PATCH] Remove ceramic consumer code (#5182) * remove ceramic service * remove more code * remvoe test --- .ebstalk.apps.env/cron.env | 4 - .ebstalk.apps.env/webapp.env | 4 - .github/workflows/destroy_staging.yml | 10 +- apps/cron/src/cron.ts | 4 - .../updateVotesStatus/updateVoteStatus.ts | 8 - charmClient/apis/credentialsApi.ts | 3 +- config/constants.ts | 3 - ...ndSpaceIssuableProposalCredentials.spec.ts | 45 -- ...findSpaceIssuableRewardCredentials.spec.ts | 36 -- ...chainProposalCredentialIfNecessary.spec.ts | 519 ---------------- ...ffchainRewardCredentialIfNecessary.spec.ts | 584 ------------------ .../__tests__/saveIssuedCredential.spec.ts | 219 ------- lib/credentials/attestOffchain.ts | 251 -------- ...eOffchainCredentialsForExternalProjects.ts | 12 - lib/credentials/getAllUserCredentials.ts | 9 - .../getProposalOrApplicationCredentials.ts | 17 +- ...eOffchainProposalCredentialsIfNecessary.ts | 213 ------- ...sueOffchainRewardCredentialsIfNecessary.ts | 185 ------ lib/credentials/queriesAndMutations.ts | 384 ------------ lib/credentials/saveIssuedCredential.ts | 12 +- lib/gitcoin/createProjectCredentials.ts | 87 --- lib/proposals/createRewardsForProposal.ts | 6 - lib/questbook/createProjectCredentials.ts | 82 --- lib/questbook/getGrantApplications.ts | 116 ---- lib/questbook/graphql/client.ts | 8 - lib/questbook/graphql/endpoints.ts | 8 - lib/questbook/graphql/queries.ts | 38 -- lib/rewards/closeOutReward.ts | 6 - lib/rewards/lockApplicationAndSubmissions.ts | 6 - lib/rewards/markRewardAsPaid.ts | 6 - lib/rewards/markSubmissionAsPaid.ts | 7 - lib/rewards/reviewApplication.ts | 9 - package-lock.json | 47 +- package.json | 2 +- pages/api/v1/rewards/submissions.ts | 31 +- scripts/query.ts | 92 +-- 36 files changed, 19 insertions(+), 3054 deletions(-) delete mode 100644 lib/credentials/__tests__/issueOffchainProposalCredentialIfNecessary.spec.ts delete mode 100644 lib/credentials/__tests__/issueOffchainRewardCredentialIfNecessary.spec.ts delete mode 100644 lib/credentials/__tests__/saveIssuedCredential.spec.ts delete mode 100644 lib/credentials/attestOffchain.ts delete mode 100644 lib/credentials/createOffchainCredentialsForExternalProjects.ts delete mode 100644 lib/credentials/issueOffchainProposalCredentialsIfNecessary.ts delete mode 100644 lib/credentials/issueOffchainRewardCredentialsIfNecessary.ts delete mode 100644 lib/credentials/queriesAndMutations.ts delete mode 100644 lib/gitcoin/createProjectCredentials.ts delete mode 100644 lib/questbook/createProjectCredentials.ts delete mode 100644 lib/questbook/getGrantApplications.ts delete mode 100644 lib/questbook/graphql/client.ts delete mode 100644 lib/questbook/graphql/endpoints.ts delete mode 100644 lib/questbook/graphql/queries.ts diff --git a/.ebstalk.apps.env/cron.env b/.ebstalk.apps.env/cron.env index 781c4a1678..a9233651b6 100644 --- a/.ebstalk.apps.env/cron.env +++ b/.ebstalk.apps.env/cron.env @@ -32,7 +32,3 @@ MAILGUN_SIGNING_KEY="{{pull:secretsmanager:/io.cv.app/prd/mailgun:SecretString:m # Secrets for issuing credentials CREDENTIAL_WALLET_KEY="{{pull:secretsmanager:/io.cv.app/prd/credentials:SecretString:credential_wallet_key}}" -CERAMIC_GRAPHQL_SERVER="{{pull:secretsmanager:/io.cv.app/prd/ceramic:SecretString:ceramic_graphql_server}}" -CERAMIC_HOST="{{pull:secretsmanager:/io.cv.app/prd/ceramic:SecretString:ceramic_host}}" -CERAMIC_SEED="{{pull:secretsmanager:/io.cv.app/prd/ceramic:SecretString:ceramic_seed}}" -CERAMIC_DID="{{pull:secretsmanager:/io.cv.app/prd/ceramic:SecretString:ceramic_did}}" diff --git a/.ebstalk.apps.env/webapp.env b/.ebstalk.apps.env/webapp.env index 678e514a11..b6fd95a639 100644 --- a/.ebstalk.apps.env/webapp.env +++ b/.ebstalk.apps.env/webapp.env @@ -53,10 +53,6 @@ XPS_API_TOKEN="{{pull:secretsmanager:/io.cv.app/prd/summon:SecretString:api_key} #FORCE_SUBDOMAINS="{{pull:secretsmanager:/io.cv.app/prd/subdomains:SecretString:force_subdomains}}" AWS_ELB_LISTENER_ARN="{{pull:secretsmanager:/io.cv.app/prd/aws:SecretString:elb_listener_arn}}" CREDENTIAL_WALLET_KEY="{{pull:secretsmanager:/io.cv.app/prd/credentials:SecretString:credential_wallet_key}}" -CERAMIC_GRAPHQL_SERVER="{{pull:secretsmanager:/io.cv.app/prd/ceramic:SecretString:ceramic_graphql_server}}" -CERAMIC_HOST="{{pull:secretsmanager:/io.cv.app/prd/ceramic:SecretString:ceramic_host}}" -CERAMIC_SEED="{{pull:secretsmanager:/io.cv.app/prd/ceramic:SecretString:ceramic_seed}}" -CERAMIC_DID="{{pull:secretsmanager:/io.cv.app/prd/ceramic:SecretString:ceramic_did}}" RECOVERY_CODE_SECRET_KEY="{{pull:secretsmanager:/io.cv.app/prd/recovery-code-secret-key:SecretString:recovery_code_secret_key}}" GITCOIN_API_KEY="{{pull:secretsmanager:/io.cv.app/prd/gitcoin:SecretString:gitcoin_api_key}}" FARCASTER_ACCOUNT_SEED_PHRASE="{{pull:secretsmanager:/io.cv.app/prd/farcaster:SecretString:account_seed_phrase}}" diff --git a/.github/workflows/destroy_staging.yml b/.github/workflows/destroy_staging.yml index 521522cb8e..024bc12729 100644 --- a/.github/workflows/destroy_staging.yml +++ b/.github/workflows/destroy_staging.yml @@ -11,7 +11,7 @@ jobs: clean-up: if: | (github.event.action == 'unlabeled' && startsWith(github.event.label.name, ':rocket: deploy')) || - (github.event.action == 'closed' && (contains(github.event.pull_request.labels.*.name, ':rocket: deploy') || contains(github.event.pull_request.labels.*.name, ':rocket: deploy-ceramic') || contains(github.event.pull_request.labels.*.name, ':rocket: deploy-sunnyawards') || contains(github.event.pull_request.labels.*.name, ':rocket: deploy-farcaster') || contains(github.event.pull_request.labels.*.name, ':rocket: deploy-cron') || contains(github.event.pull_request.labels.*.name, ':rocket: deploy-waitlist') || contains(github.event.pull_request.labels.*.name, ':rocket: deploy-websockets'))) + (github.event.action == 'closed' && (contains(github.event.pull_request.labels.*.name, ':rocket: deploy') || contains(github.event.pull_request.labels.*.name, ':rocket: deploy-sunnyawards') || contains(github.event.pull_request.labels.*.name, ':rocket: deploy-farcaster') || contains(github.event.pull_request.labels.*.name, ':rocket: deploy-cron') || contains(github.event.pull_request.labels.*.name, ':rocket: deploy-waitlist') || contains(github.event.pull_request.labels.*.name, ':rocket: deploy-websockets'))) runs-on: ubuntu-latest steps: - name: Configure AWS credentials @@ -47,7 +47,7 @@ jobs: run: | stage_name_suffix="${{ github.event.number }}-${{ env.GITHUB_HEAD_REF_SLUG }}" - for app in ceramic cron farcaster sunnyawards waitlist webapp websockets; do + for app in cron farcaster sunnyawards waitlist webapp websockets; do # sanitize and trim string so that it can be used as a valid subdomain. Includes removing hyphens at the start and end of the name stage_name=`echo "stg-${app}-${stage_name_suffix}" | sed -E -e 's/[^a-zA-Z0-9-]+//g' -e 's/(.{40}).*/\1/' -e 's/^-/0/' -e 's/-$/0/'` @@ -57,12 +57,6 @@ jobs: done - - name: Delete Ceramic Github deployment - uses: strumwolf/delete-deployment-environment@v3 - with: - token: ${{ steps.get-token.outputs.token }} - environment: ${{ steps.destroy_aws_stack.outputs.ceramic_env }} - - name: Delete Cron Github deployment uses: strumwolf/delete-deployment-environment@v3 with: diff --git a/apps/cron/src/cron.ts b/apps/cron/src/cron.ts index bb24cff117..661f4550cf 100644 --- a/apps/cron/src/cron.ts +++ b/apps/cron/src/cron.ts @@ -1,5 +1,4 @@ import { log } from '@charmverse/core/log'; -import { createOffchainCredentialsForExternalProjects } from '@root/lib/credentials/createOffchainCredentialsForExternalProjects'; import { relay } from '@root/lib/websockets/relay'; import cron from 'node-cron'; import { Server } from 'socket.io'; @@ -71,9 +70,6 @@ cron.schedule('0 1 * * *', updateMixpanelProfilesTask); // Sync summon space roles every day at midnight cron.schedule('0 0 * * *', syncSummonSpacesRoles); -// Create external eas credentials for Gitcoin and Questbook every day at midnight -cron.schedule('0 0 * * *', createOffchainCredentialsForExternalProjects); - // Refresh docusign credentials every 6 hours cron.schedule('0 */6 * * *', refreshDocusignOAuthTask); // Sync op reviews every 15 minutes - remove by July 2024 diff --git a/apps/cron/src/tasks/updateVotesStatus/updateVoteStatus.ts b/apps/cron/src/tasks/updateVotesStatus/updateVoteStatus.ts index c754bb5acf..7a31b796b9 100644 --- a/apps/cron/src/tasks/updateVotesStatus/updateVoteStatus.ts +++ b/apps/cron/src/tasks/updateVotesStatus/updateVoteStatus.ts @@ -1,5 +1,4 @@ import { prisma } from '@charmverse/core/prisma-client'; -import { issueOffchainProposalCredentialsIfNecessary } from '@root/lib/credentials/issueOffchainProposalCredentialsIfNecessary'; import { getVotesByState } from '@root/lib/votes/getVotesByState'; import { VOTE_STATUS } from '@root/lib/votes/interfaces'; import { publishProposalEvent } from '@root/lib/webhookPublisher/publishEvent'; @@ -93,13 +92,6 @@ const updateVoteStatus = async () => { }) ]); - for (const passedEval of passedEvaluations) { - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: passedEval.proposalId - }); - } - return votesPassedDeadline.length; }; diff --git a/charmClient/apis/credentialsApi.ts b/charmClient/apis/credentialsApi.ts index f3168294db..29e72248cc 100644 --- a/charmClient/apis/credentialsApi.ts +++ b/charmClient/apis/credentialsApi.ts @@ -1,6 +1,5 @@ import * as http from '@root/adapters/http'; -import type { CharmVerseCredentialInput } from 'lib/credentials/attestOffchain'; import type { EASAttestationFromApi } from 'lib/credentials/external/getOnchainCredentials'; import type { GnosisSafeTransactionToIndex } from 'lib/credentials/indexGnosisSafeCredentialTransaction'; import type { ProposalCredentialsToIndex } from 'lib/credentials/indexOnChainProposalCredential'; @@ -9,7 +8,7 @@ import type { CreateCredentialTemplateInput, CredentialTemplateUpdate } from 'li export class CredentialsApi { // TODO Test endpoint for generating a credential - remove later - attest(data: CharmVerseCredentialInput): Promise { + attest(data: any): Promise { return http.POST(`/api/credentials`, data); } diff --git a/config/constants.ts b/config/constants.ts index f53505693e..82c4f3f317 100644 --- a/config/constants.ts +++ b/config/constants.ts @@ -59,9 +59,6 @@ export const appSubdomain = 'app'; export const credentialsWalletPrivateKey = process.env.CREDENTIAL_WALLET_KEY; export const awsS3Bucket = process.env.S3_UPLOAD_BUCKET as string; -// Ceramic Node -export const graphQlServerEndpoint = process.env.CERAMIC_GRAPHQL_SERVER as string; - // Github export const githubPrivateKey = process.env.GITHUB_APP_PRIVATE_KEY as string; export const githubAppId = Number(process.env.GITHUB_APP_ID); diff --git a/lib/credentials/__tests__/findSpaceIssuableProposalCredentials.spec.ts b/lib/credentials/__tests__/findSpaceIssuableProposalCredentials.spec.ts index 906edebfe3..edf2a5aaa7 100644 --- a/lib/credentials/__tests__/findSpaceIssuableProposalCredentials.spec.ts +++ b/lib/credentials/__tests__/findSpaceIssuableProposalCredentials.spec.ts @@ -673,49 +673,4 @@ describe('generateCredentialInputsForProposal', () => { expect(resultAfterIssuedCredentialSaved).toHaveLength(0); }); - - it('should return credentials that were only issued offchain but not yet onchain', async () => { - const userWallet = randomETHWalletAddress().toLowerCase(); - const generated = await testUtilsUser.generateUserAndSpace({ wallet: userWallet }); - const user = generated.user; - - const space = await prisma.space.update({ - where: { - id: generated.space.id - }, - data: { - useOnchainCredentials: true - } - }); - - const credentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'], - schemaType: 'reward' - }); - - const proposal = await testUtilsProposals.generateProposal({ - spaceId: space.id, - userId: user.id, - authors: [user.id], - proposalStatus: 'published', - selectedCredentialTemplateIds: [credentialTemplate.id], - evaluationInputs: [{ evaluationType: 'feedback', result: 'pass', permissions: [], reviewers: [] }] - }); - - const result = await findSpaceIssuableProposalCredentials({ spaceId: space.id }); - expect(result.length).toBe(1); - - // Simulate having issued this credential already - await testUtilsCredentials.generateIssuedOffchainCredential({ - credentialEvent: 'proposal_approved', - credentialTemplateId: credentialTemplate.id, - proposalId: proposal.id, - userId: user.id - }); - - const resultAfterIssuedCredentialSaved = await findSpaceIssuableProposalCredentials({ spaceId: space.id }); - - expect(resultAfterIssuedCredentialSaved).toHaveLength(1); - }); }); diff --git a/lib/credentials/__tests__/findSpaceIssuableRewardCredentials.spec.ts b/lib/credentials/__tests__/findSpaceIssuableRewardCredentials.spec.ts index 7a3d1ff554..2860a15a00 100644 --- a/lib/credentials/__tests__/findSpaceIssuableRewardCredentials.spec.ts +++ b/lib/credentials/__tests__/findSpaceIssuableRewardCredentials.spec.ts @@ -425,40 +425,4 @@ describe('findSpaceIssuableRewardCredentials', () => { expect(resultAfterIssuedCredentialSaved).toHaveLength(0); }); - - it('should return credentials that were only issued offchain but not yet onchain', async () => { - const userWallet = randomETHWalletAddress().toLowerCase(); - const { space, user } = await testUtilsUser.generateUserAndSpace({ wallet: userWallet }); - - const credentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['reward_submission_approved'], - schemaType: 'reward' - }); - - const reward = await generateBountyWithSingleApplication({ - bountyCap: null, - applicationStatus: 'complete', - spaceId: space.id, - userId: user.id, - selectedCredentialTemplateIds: [credentialTemplate.id] - }); - - const rewardApplication = reward.applications[0]; - - const result = await findSpaceIssuableRewardCredentials({ spaceId: space.id }); - expect(result.length).toBe(1); - - // Simulate having issued this credential already - await testUtilsCredentials.generateIssuedOffchainCredential({ - credentialEvent: 'reward_submission_approved', - credentialTemplateId: credentialTemplate.id, - rewardApplicationId: rewardApplication.id, - userId: user.id - }); - - const resultAfterIssuedCredentialSaved = await findSpaceIssuableRewardCredentials({ spaceId: space.id }); - - expect(resultAfterIssuedCredentialSaved.length).toBe(1); - }); }); diff --git a/lib/credentials/__tests__/issueOffchainProposalCredentialIfNecessary.spec.ts b/lib/credentials/__tests__/issueOffchainProposalCredentialIfNecessary.spec.ts deleted file mode 100644 index e746c32596..0000000000 --- a/lib/credentials/__tests__/issueOffchainProposalCredentialIfNecessary.spec.ts +++ /dev/null @@ -1,519 +0,0 @@ -import type { IssuedCredential } from '@charmverse/core/prisma-client'; -import { prisma } from '@charmverse/core/prisma-client'; -import { testUtilsCredentials, testUtilsProposals, testUtilsUser } from '@charmverse/core/test'; -import { pseudoRandomHexString } from '@root/lib/utils/random'; -import { v4 as uuid } from 'uuid'; -import { optimism } from 'viem/chains'; - -import { randomETHWalletAddress } from 'testing/generateStubs'; - -import { issueOffchainProposalCredentialsIfNecessary } from '../issueOffchainProposalCredentialsIfNecessary'; -import { publishSignedCredential, type PublishedSignedCredential } from '../queriesAndMutations'; -import { attestationSchemaIds } from '../schemas'; - -jest.mock('lib/credentials/queriesAndMutations', () => ({ - publishSignedCredential: jest.fn().mockImplementation(() => - Promise.resolve({ - chainId: optimism.id, - content: {}, - id: uuid(), - issuer: '0x66d96dab921F7c8Ce98d0e05fb0B76Db8Bd54773', - recipient: '0xAEfe164A5f55121AD98d0e347dA7990CcC8BE295', - schemaId: attestationSchemaIds.proposal, - sig: 'Signature content', - timestamp: new Date(), - type: 'proposal', - verificationUrl: 'https://eas-explorer-example.com/verification' - } as PublishedSignedCredential) - ) -})); - -const mockedPublishSignedCredential = jest.mocked(publishSignedCredential); - -afterEach(() => { - mockedPublishSignedCredential.mockClear(); -}); - -describe('issueProposalCredentialIfNecessary', () => { - it('should issue credentials once for a unique combination of user, proposal, event and credential template', async () => { - const { space, user: author1 } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - const author2 = await testUtilsUser.generateSpaceUser({ spaceId: space.id, wallet: randomETHWalletAddress() }); - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'] - }); - const secondCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'] - }); - - const proposal = await testUtilsProposals.generateProposal({ - spaceId: space.id, - authors: [author1.id, author2.id], - userId: author1.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id, secondCredentialTemplate.id], - proposalStatus: 'published', - evaluationInputs: [{ reviewers: [], evaluationType: 'pass_fail', permissions: [], result: 'pass' }] - }); - - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: proposal.id - }); - - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: proposal.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(4); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - proposalId: proposal.id - } - }); - - // 1 event types * 2 credential templates * 2 authors - expect(issuedCredentials).toHaveLength(4); - - expect(issuedCredentials).toMatchObject( - expect.arrayContaining>([ - expect.objectContaining({ - userId: author1.id, - credentialEvent: 'proposal_approved', - credentialTemplateId: firstCredentialTemplate.id - }), - expect.objectContaining({ - userId: author1.id, - credentialEvent: 'proposal_approved', - credentialTemplateId: firstCredentialTemplate.id - }), - expect.objectContaining({ - userId: author2.id, - credentialEvent: 'proposal_approved', - credentialTemplateId: secondCredentialTemplate.id - }), - expect.objectContaining({ - userId: author2.id, - credentialEvent: 'proposal_approved', - credentialTemplateId: secondCredentialTemplate.id - }) - ]) - ); - }); - - it('should not issue credentials if the proposal is a template', async () => { - const { space, user: author1 } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'] - }); - - const proposal = await testUtilsProposals.generateProposal({ - spaceId: space.id, - authors: [author1.id], - userId: author1.id, - pageType: 'proposal_template', - selectedCredentialTemplateIds: [firstCredentialTemplate.id], - proposalStatus: 'published', - evaluationInputs: [{ reviewers: [], evaluationType: 'pass_fail', permissions: [], result: 'pass' }] - }); - - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: proposal.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(0); - }); - - it('should issue the offchain credentials for a unique combination of user, proposal, event and credential template if it exists onchain, but not offchain', async () => { - const { space, user: author1 } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'] - }); - - const proposal = await testUtilsProposals.generateProposal({ - spaceId: space.id, - authors: [author1.id], - userId: author1.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id], - proposalStatus: 'published', - evaluationInputs: [{ reviewers: [], evaluationType: 'pass_fail', permissions: [], result: 'pass' }] - }); - - const existingOnchainCredential = await testUtilsCredentials.generateIssuedOnchainCredential({ - credentialEvent: 'proposal_approved', - credentialTemplateId: firstCredentialTemplate.id, - userId: author1.id, - proposalId: proposal.id, - onchainChainId: optimism.id, - onchainAttestationId: pseudoRandomHexString() - }); - - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: proposal.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(1); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - proposalId: proposal.id - } - }); - - // 2 event types * 2 credential templates * 2 authors - expect(issuedCredentials).toHaveLength(1); - - expect(issuedCredentials).toMatchObject( - expect.arrayContaining>([ - expect.objectContaining>({ - id: existingOnchainCredential.id, - userId: author1.id, - credentialEvent: 'proposal_approved', - credentialTemplateId: firstCredentialTemplate.id, - ceramicId: expect.any(String), - onchainChainId: existingOnchainCredential.onchainChainId, - onchainAttestationId: existingOnchainCredential.onchainAttestationId - }) - ]) - ); - }); - - it('should only issue credentials if the credential template allows issuing credentials for the event', async () => { - const { space, user: author1 } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - const author2 = await testUtilsUser.generateSpaceUser({ spaceId: space.id, wallet: randomETHWalletAddress() }); - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'] - }); - const secondCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: [] - }); - - const proposal = await testUtilsProposals.generateProposal({ - spaceId: space.id, - authors: [author1.id, author2.id], - userId: author1.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id, secondCredentialTemplate.id], - proposalStatus: 'published', - evaluationInputs: [{ reviewers: [], evaluationType: 'pass_fail', permissions: [], result: 'pass' }] - }); - - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: proposal.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(2); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - proposalId: proposal.id - } - }); - - // 1 event type * 1 credential templates * 2 authors - expect(issuedCredentials).toHaveLength(2); - - expect(issuedCredentials).toMatchObject( - expect.arrayContaining>([ - expect.objectContaining({ - userId: author1.id, - credentialEvent: 'proposal_approved', - credentialTemplateId: firstCredentialTemplate.id - }), - expect.objectContaining({ - userId: author2.id, - credentialEvent: 'proposal_approved', - credentialTemplateId: firstCredentialTemplate.id - }) - ]) - ); - }); - - it('should issue credentials for newly added authors', async () => { - const { space, user: author1 } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - const author2 = await testUtilsUser.generateSpaceUser({ spaceId: space.id, wallet: randomETHWalletAddress() }); - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'] - }); - const secondCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'] - }); - - const proposal = await testUtilsProposals.generateProposal({ - spaceId: space.id, - authors: [author1.id, author2.id], - userId: author1.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id, secondCredentialTemplate.id], - proposalStatus: 'published', - evaluationInputs: [{ reviewers: [], evaluationType: 'pass_fail', permissions: [], result: 'pass' }] - }); - - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: proposal.id - }); - - const newAuthor = await testUtilsUser.generateSpaceUser({ spaceId: space.id, wallet: randomETHWalletAddress() }); - - await prisma.proposalAuthor.create({ - data: { - author: { connect: { id: newAuthor.id } }, - proposal: { connect: { id: proposal.id } } - } - }); - - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: proposal.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(6); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - proposalId: proposal.id, - userId: newAuthor.id - } - }); - - // 1 event types * 2 credential templates * 1 author (filtered on the query) - expect(issuedCredentials).toHaveLength(2); - - expect(issuedCredentials).toMatchObject( - expect.arrayContaining>([ - expect.objectContaining({ - userId: newAuthor.id, - credentialEvent: 'proposal_approved', - credentialTemplateId: firstCredentialTemplate.id - }), - expect.objectContaining({ - userId: newAuthor.id, - credentialEvent: 'proposal_approved', - credentialTemplateId: secondCredentialTemplate.id - }) - ]) - ); - }); - - it('should ignore inexistent selected credentials', async () => { - const { space, user: author1 } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - - const inexistentCredentialId = uuid(); - - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'] - }); - - const proposal = await testUtilsProposals.generateProposal({ - spaceId: space.id, - authors: [author1.id], - userId: author1.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id, inexistentCredentialId], - proposalStatus: 'published', - evaluationInputs: [{ reviewers: [], evaluationType: 'pass_fail', permissions: [], result: 'pass' }] - }); - - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: proposal.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(1); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - proposalId: proposal.id - } - }); - - // 1 event types * 1 existing credential template * 1 author - expect(issuedCredentials).toHaveLength(1); - - expect(issuedCredentials).toMatchObject( - expect.arrayContaining>([ - expect.objectContaining({ - userId: author1.id, - credentialEvent: 'proposal_approved', - credentialTemplateId: firstCredentialTemplate.id - }) - ]) - ); - }); - - it('should not attempt to issue the credential if the user has no wallet', async () => { - const { space, user: author1 } = await testUtilsUser.generateUserAndSpace({ - // Dont' assign a wallet to the user - wallet: undefined, - domain: `cvt-testing-${uuid()}` - }); - - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'] - }); - - const proposal = await testUtilsProposals.generateProposal({ - spaceId: space.id, - authors: [author1.id], - userId: author1.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id], - proposalStatus: 'published', - evaluationInputs: [{ reviewers: [], evaluationType: 'pass_fail', permissions: [], result: 'pass' }] - }); - - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: proposal.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(0); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - proposalId: proposal.id - } - }); - - expect(issuedCredentials).toHaveLength(0); - }); - - it('should not issue a proposal_approved credential if the proposal status is draft', async () => { - const { space, user: author1 } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'] - }); - - const proposal = await testUtilsProposals.generateProposal({ - spaceId: space.id, - authors: [author1.id], - userId: author1.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id], - proposalStatus: 'draft', - evaluationInputs: [{ reviewers: [], evaluationType: 'pass_fail', permissions: [] }] - }); - - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: proposal.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(0); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - proposalId: proposal.id - } - }); - - expect(issuedCredentials).toHaveLength(0); - }); - - it('should not issue a proposal_approved credential if the proposal evaluation is failed', async () => { - const { space, user: author1 } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'] - }); - - const proposal = await testUtilsProposals.generateProposal({ - spaceId: space.id, - authors: [author1.id], - userId: author1.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id], - proposalStatus: 'published', - evaluationInputs: [{ reviewers: [], evaluationType: 'pass_fail', result: 'fail', permissions: [] }] - }); - - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: proposal.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(0); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - proposalId: proposal.id - } - }); - - expect(issuedCredentials).toHaveLength(0); - }); - - it('should not issue a proposal_approved credential if the final proposal evaluation has not been reached', async () => { - const { space, user: author1 } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved'] - }); - - const proposal = await testUtilsProposals.generateProposal({ - spaceId: space.id, - authors: [author1.id], - userId: author1.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id], - proposalStatus: 'published', - evaluationInputs: [ - { reviewers: [], evaluationType: 'pass_fail', result: 'pass', permissions: [] }, - { reviewers: [], evaluationType: 'pass_fail', permissions: [] } - ] - }); - - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId: proposal.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(0); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - proposalId: proposal.id - } - }); - - expect(issuedCredentials).toHaveLength(0); - }); -}); diff --git a/lib/credentials/__tests__/issueOffchainRewardCredentialIfNecessary.spec.ts b/lib/credentials/__tests__/issueOffchainRewardCredentialIfNecessary.spec.ts deleted file mode 100644 index 1db132ebd8..0000000000 --- a/lib/credentials/__tests__/issueOffchainRewardCredentialIfNecessary.spec.ts +++ /dev/null @@ -1,584 +0,0 @@ -import type { Application, IssuedCredential } from '@charmverse/core/prisma-client'; -import { ApplicationStatus, prisma } from '@charmverse/core/prisma-client'; -import { testUtilsCredentials, testUtilsUser } from '@charmverse/core/test'; -import { typedKeys } from '@root/lib/utils/objects'; -import { pseudoRandomHexString } from '@root/lib/utils/random'; -import { v4 as uuid } from 'uuid'; -import { optimism } from 'viem/chains'; - -import { randomETHWalletAddress } from 'testing/generateStubs'; -import { generateBounty, generateBountyApplication, generateBountyWithSingleApplication } from 'testing/setupDatabase'; - -import { issueOffchainRewardCredentialsIfNecessary } from '../issueOffchainRewardCredentialsIfNecessary'; -import { publishSignedCredential, type PublishedSignedCredential } from '../queriesAndMutations'; -import { attestationSchemaIds } from '../schemas'; - -jest.mock('lib/credentials/queriesAndMutations', () => ({ - publishSignedCredential: jest.fn().mockImplementation(() => - Promise.resolve({ - chainId: optimism.id, - content: {}, - id: uuid(), - issuer: '0x66d96dab921F7c8Ce98d0e05fb0B76Db8Bd54773', - recipient: '0xAEfe164A5f55121AD98d0e347dA7990CcC8BE295', - schemaId: attestationSchemaIds.reward, - sig: 'Signature content', - timestamp: new Date(), - type: 'reward', - verificationUrl: 'https://eas-explorer-example.com/verification' - } as PublishedSignedCredential) - ) -})); - -const mockedPublishSignedCredential = jest.mocked(publishSignedCredential); - -afterEach(() => { - mockedPublishSignedCredential.mockClear(); -}); -describe('issueRewardCredentialIfNecessary', () => { - it('should issue credentials once for a unique combination of user, reward submission, event and credential template', async () => { - const { space, user: rewardCreatorAndSubmitter } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - const submitter = await testUtilsUser.generateSpaceUser({ spaceId: space.id, wallet: randomETHWalletAddress() }); - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['reward_submission_approved'] - }); - const secondCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['reward_submission_approved'] - }); - - const reward = await generateBountyWithSingleApplication({ - applicationStatus: 'complete', - bountyCap: null, - spaceId: space.id, - userId: rewardCreatorAndSubmitter.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id, secondCredentialTemplate.id] - }); - - const submitterApplication = await generateBountyApplication({ - applicationStatus: 'complete', - bountyId: reward.id, - spaceId: space.id, - userId: submitter.id - }); - - const secondSubmitterApplication = await generateBountyApplication({ - applicationStatus: 'complete', - bountyId: reward.id, - spaceId: space.id, - userId: submitter.id - }); - - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId: reward.id - }); - - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId: reward.id - }); - - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId: reward.id - }); - - // 1 event types * 2 credential templates * 3 submissions - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(6); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - rewardApplication: { - bounty: { - id: reward.id - } - } - } - }); - - const creatorApplicationId = reward.applications[0].id; - const submitterApplicationId = submitterApplication.id; - const secondSubmitterApplicationId = secondSubmitterApplication.id; - - // 1 event types * 2 credential templates * 3 submissions - expect(issuedCredentials).toHaveLength(6); - - expect(issuedCredentials).toMatchObject( - expect.arrayContaining>([ - expect.objectContaining>({ - userId: rewardCreatorAndSubmitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: firstCredentialTemplate.id, - rewardApplicationId: creatorApplicationId - }), - expect.objectContaining>({ - userId: rewardCreatorAndSubmitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: secondCredentialTemplate.id, - rewardApplicationId: creatorApplicationId - }), - expect.objectContaining>({ - userId: submitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: firstCredentialTemplate.id, - rewardApplicationId: submitterApplicationId - }), - expect.objectContaining>({ - userId: submitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: secondCredentialTemplate.id, - rewardApplicationId: submitterApplicationId - }), - expect.objectContaining>({ - userId: submitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: firstCredentialTemplate.id, - rewardApplicationId: secondSubmitterApplicationId - }), - expect.objectContaining>({ - userId: submitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: secondCredentialTemplate.id, - rewardApplicationId: secondSubmitterApplicationId - }) - ]) - ); - }); - - it('should issue the offchain credentials for a unique combination of user, reward submission, event and credential template if it exists onchain, but not offchain', async () => { - const { space, user: rewardCreatorAndSubmitter } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['reward_submission_approved'] - }); - - const reward = await generateBountyWithSingleApplication({ - applicationStatus: 'complete', - bountyCap: null, - spaceId: space.id, - userId: rewardCreatorAndSubmitter.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id] - }); - - const existingOnchainCredential = await testUtilsCredentials.generateIssuedOnchainCredential({ - credentialEvent: 'reward_submission_approved', - credentialTemplateId: firstCredentialTemplate.id, - userId: rewardCreatorAndSubmitter.id, - rewardApplicationId: reward.applications[0].id, - onchainChainId: optimism.id, - onchainAttestationId: pseudoRandomHexString() - }); - - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId: reward.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(1); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - rewardApplicationId: reward.applications[0].id - } - }); - - // 2 event types * 2 credential templates * 2 authors - expect(issuedCredentials).toHaveLength(1); - - expect(issuedCredentials).toMatchObject([ - expect.objectContaining>({ - id: existingOnchainCredential.id, - userId: rewardCreatorAndSubmitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: firstCredentialTemplate.id, - ceramicId: expect.any(String), - onchainChainId: existingOnchainCredential.onchainChainId, - onchainAttestationId: existingOnchainCredential.onchainAttestationId - }) - ]); - }); - - it('should target only a specific submission if this parameter is provided credentials once for a unique combination of user, reward submission and credential template', async () => { - const { space, user: rewardCreatorAndSubmitter } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - const submitter = await testUtilsUser.generateSpaceUser({ spaceId: space.id, wallet: randomETHWalletAddress() }); - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['reward_submission_approved'] - }); - - const reward = await generateBountyWithSingleApplication({ - applicationStatus: 'complete', - bountyCap: null, - spaceId: space.id, - userId: rewardCreatorAndSubmitter.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id] - }); - - const submitterApplication = await generateBountyApplication({ - applicationStatus: 'complete', - bountyId: reward.id, - spaceId: space.id, - userId: submitter.id - }); - - const secondSubmitterApplication = await generateBountyApplication({ - applicationStatus: 'complete', - bountyId: reward.id, - spaceId: space.id, - userId: submitter.id - }); - - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId: reward.id, - submissionId: submitterApplication.id - }); - - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId: reward.id, - submissionId: submitterApplication.id - }); - - // 1 credential template, and 1 submission targeted - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(1); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - rewardApplication: { - bounty: { - id: reward.id - } - } - } - }); - - const submitterApplicationId = submitterApplication.id; - - // 1 event types * 2 credential templates * 3 submissions - expect(issuedCredentials).toHaveLength(1); - - expect(issuedCredentials).toMatchObject( - expect.arrayContaining>([ - expect.objectContaining>({ - userId: submitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: firstCredentialTemplate.id, - rewardApplicationId: submitterApplicationId - }) - ]) - ); - }); - - it('should only issue credentials if the credential template allows issuing credentials for the event', async () => { - const { space, user: rewardCreatorAndSubmitter } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - const submitter = await testUtilsUser.generateSpaceUser({ spaceId: space.id, wallet: randomETHWalletAddress() }); - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: [] - }); - const secondCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: [] - }); - - const reward = await generateBountyWithSingleApplication({ - applicationStatus: 'complete', - bountyCap: null, - spaceId: space.id, - userId: rewardCreatorAndSubmitter.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id, secondCredentialTemplate.id] - }); - - const submitterApplication = await generateBountyApplication({ - applicationStatus: 'complete', - bountyId: reward.id, - spaceId: space.id, - userId: submitter.id - }); - - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId: reward.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(0); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - rewardApplication: { - bountyId: reward.id - } - } - }); - // 1 event types * 2 credential templates * 2 authors - expect(issuedCredentials).toHaveLength(0); - }); - - it('should issue credentials for new submitters', async () => { - const { space, user: rewardCreatorAndSubmitter } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - const submitter = await testUtilsUser.generateSpaceUser({ spaceId: space.id, wallet: randomETHWalletAddress() }); - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['reward_submission_approved'] - }); - const secondCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['reward_submission_approved'] - }); - - const reward = await generateBountyWithSingleApplication({ - applicationStatus: 'complete', - bountyCap: null, - spaceId: space.id, - userId: rewardCreatorAndSubmitter.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id, secondCredentialTemplate.id] - }); - - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId: reward.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(2); - - const submitterApplication = await generateBountyApplication({ - applicationStatus: 'complete', - bountyId: reward.id, - spaceId: space.id, - userId: submitter.id - }); - - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId: reward.id - }); - - // 2 previous calls + 2 current calls - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(4); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - rewardApplication: { - bountyId: reward.id - } - } - }); - - const creatorApplicationId = reward.applications[0].id; - const submitterApplicationId = submitterApplication.id; - - // 1 event types * 2 credential templates * 2 authors - expect(issuedCredentials).toHaveLength(4); - - expect(issuedCredentials).toMatchObject( - expect.arrayContaining>([ - expect.objectContaining>({ - userId: rewardCreatorAndSubmitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: firstCredentialTemplate.id, - rewardApplicationId: creatorApplicationId - }), - expect.objectContaining>({ - userId: rewardCreatorAndSubmitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: secondCredentialTemplate.id, - rewardApplicationId: creatorApplicationId - }), - expect.objectContaining>({ - userId: submitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: firstCredentialTemplate.id, - rewardApplicationId: submitterApplicationId - }), - expect.objectContaining>({ - userId: submitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: secondCredentialTemplate.id, - rewardApplicationId: submitterApplicationId - }) - ]) - ); - }); - - it('should ignore inexistent selected credentials', async () => { - const { space, user: rewardCreatorAndSubmitter } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - - const inexistentCredentialId = uuid(); - - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['reward_submission_approved'] - }); - - const reward = await generateBountyWithSingleApplication({ - applicationStatus: 'complete', - bountyCap: null, - spaceId: space.id, - userId: rewardCreatorAndSubmitter.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id, inexistentCredentialId] - }); - - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId: reward.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(1); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - rewardApplication: { - bountyId: reward.id - } - } - }); - - // 1 event types * 1 existing credential template * 1 author - expect(issuedCredentials).toHaveLength(1); - - expect(issuedCredentials).toMatchObject( - expect.arrayContaining>([ - expect.objectContaining>({ - userId: rewardCreatorAndSubmitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: firstCredentialTemplate.id, - rewardApplicationId: reward.applications[0].id - }) - ]) - ); - }); - - it('should not attempt to issue the credential if the user has no wallet', async () => { - const { space, user: rewardCreatorAndSubmitter } = await testUtilsUser.generateUserAndSpace({ - domain: `cvt-testing-${uuid()}` - }); - - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['reward_submission_approved'] - }); - - const reward = await generateBountyWithSingleApplication({ - applicationStatus: 'complete', - bountyCap: null, - spaceId: space.id, - userId: rewardCreatorAndSubmitter.id, - selectedCredentialTemplateIds: [firstCredentialTemplate.id] - }); - - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId: reward.id - }); - - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(0); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - rewardApplication: { - bountyId: reward.id - } - } - }); - - expect(issuedCredentials).toHaveLength(0); - }); - - it('should only issue a reward_submission_approved credential if the application status is complete, processing or paid', async () => { - const { space, user: rewardCreator } = await testUtilsUser.generateUserAndSpace({ - wallet: randomETHWalletAddress(), - domain: `cvt-testing-${uuid()}` - }); - const submitter = await testUtilsUser.generateSpaceUser({ spaceId: space.id, wallet: randomETHWalletAddress() }); - - const firstCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['reward_submission_approved'] - }); - - const reward = await generateBounty({ - createdBy: rewardCreator.id, - spaceId: space.id, - selectedCredentialTemplates: [firstCredentialTemplate.id] - }); - - const applicationStatuses = typedKeys(ApplicationStatus); - - const generatedApplications: Record = {} as Record; - - for (const applicationStatus of applicationStatuses) { - const application = await generateBountyApplication({ - applicationStatus, - bountyId: reward.id, - spaceId: space.id, - userId: submitter.id - }); - generatedApplications[applicationStatus] = application; - } - - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId: reward.id - }); - - // Only 3 valid application statuses, complete, processing or paid - expect(mockedPublishSignedCredential).toHaveBeenCalledTimes(3); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - rewardApplication: { - bounty: { - id: reward.id - } - } - } - }); - - // Only 3 valid application statuses, complete, processing or paid - expect(issuedCredentials).toHaveLength(3); - - expect(issuedCredentials).toMatchObject( - expect.arrayContaining>([ - expect.objectContaining>({ - userId: submitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: firstCredentialTemplate.id, - rewardApplicationId: generatedApplications.complete.id - }), - expect.objectContaining>({ - userId: submitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: firstCredentialTemplate.id, - rewardApplicationId: generatedApplications.processing.id - }), - expect.objectContaining>({ - userId: submitter.id, - credentialEvent: 'reward_submission_approved', - credentialTemplateId: firstCredentialTemplate.id, - rewardApplicationId: generatedApplications.paid.id - }) - ]) - ); - }); -}); diff --git a/lib/credentials/__tests__/saveIssuedCredential.spec.ts b/lib/credentials/__tests__/saveIssuedCredential.spec.ts deleted file mode 100644 index 759ce960cd..0000000000 --- a/lib/credentials/__tests__/saveIssuedCredential.spec.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { InvalidInputError } from '@charmverse/core/errors'; -import type { - Application, - Bounty, - CredentialTemplate, - IssuedCredential, - Proposal, - Space, - User -} from '@charmverse/core/prisma-client'; -import { testUtilsCredentials, testUtilsProposals, testUtilsUser } from '@charmverse/core/test'; -import { pseudoRandomHexString } from '@root/lib/utils/random'; -import { v4 as uuid } from 'uuid'; -import { mainnet } from 'viem/chains'; - -import { generateBountyWithSingleApplication } from 'testing/setupDatabase'; - -import type { IdenticalCredentialProps } from '../saveIssuedCredential'; -import { saveIssuedCredential } from '../saveIssuedCredential'; -import { proposalCredentialSchemaId } from '../schemas/proposal'; -import { rewardCredentialSchemaId } from '../schemas/reward'; - -describe('saveIssuedCredential', () => { - let user: User; - let space: Space; - let proposal: Proposal; - let proposalCredentialTemplate: CredentialTemplate; - let reward: Bounty; - let rewardApplication: Application; - let rewardCredentialTemplate: CredentialTemplate; - - beforeAll(async () => { - ({ user, space } = await testUtilsUser.generateUserAndSpace()); - proposalCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['proposal_approved', 'proposal_created'], - description: 'Proposal credential template', - name: 'Proposal credential template', - schemaAddress: proposalCredentialSchemaId, - organization: 'Org', - schemaType: 'proposal' - }); - rewardCredentialTemplate = await testUtilsCredentials.generateCredentialTemplate({ - spaceId: space.id, - credentialEvents: ['reward_submission_approved'], - description: 'Reward credential template', - name: 'Reward credential template', - schemaAddress: 'schemaAddress', - organization: 'Org', - schemaType: 'reward' - }); - - proposal = await testUtilsProposals.generateProposal({ - spaceId: space.id, - userId: user.id - }); - - const generatedReward = await generateBountyWithSingleApplication({ - spaceId: space.id, - userId: user.id, - applicationStatus: 'complete', - bountyCap: null - }); - - reward = generatedReward; - - rewardApplication = generatedReward.applications[0]; - }); - - it('should create a new issued credential with proposalId and save a matching credential against the existing issued credential', async () => { - const validOffchainData = { - ceramicId: pseudoRandomHexString(), - ceramicRecord: { data: 'dataExample' } - }; - - const validOnChainData = { - onchainChainId: mainnet.id, - onchainAttestationId: pseudoRandomHexString() - }; - - const commonData: IdenticalCredentialProps = { - credentialTemplateId: proposalCredentialTemplate.id, - userId: user.id, - credentialEvent: 'proposal_approved', - schemaId: proposalCredentialSchemaId, - proposalId: proposal.id - }; - - const issuedCredential = await saveIssuedCredential({ - credentialProps: commonData, - offchainData: validOffchainData - }); - - expect(issuedCredential).toMatchObject( - expect.objectContaining>({ - ...commonData, - ceramicId: validOffchainData.ceramicId, - ceramicRecord: validOffchainData.ceramicRecord, - onchainAttestationId: null, - onchainChainId: null - }) - ); - - const resavedCredential = await saveIssuedCredential({ - credentialProps: commonData, - onChainData: validOnChainData - }); - - expect(resavedCredential).toEqual({ ...issuedCredential, ...validOnChainData }); - }); - - it('should create a new issued credential with rewardApplicationId and save a matching credential against the existing issued credential', async () => { - const validOffchainData = { - ceramicId: pseudoRandomHexString(), - ceramicRecord: { data: 'dataExample' } - }; - - const validOnChainData = { - onchainChainId: mainnet.id, - onchainAttestationId: pseudoRandomHexString() - }; - - const commonData: IdenticalCredentialProps = { - credentialTemplateId: rewardCredentialTemplate.id, - userId: user.id, - credentialEvent: 'reward_submission_approved', - schemaId: rewardCredentialSchemaId, - rewardApplicationId: rewardApplication.id - }; - - const issuedCredential = await saveIssuedCredential({ - credentialProps: commonData, - offchainData: validOffchainData - }); - - expect(issuedCredential).toMatchObject( - expect.objectContaining>({ - ...commonData, - ceramicId: validOffchainData.ceramicId, - ceramicRecord: validOffchainData.ceramicRecord, - onchainAttestationId: null, - onchainChainId: null - }) - ); - - const resavedCredential = await saveIssuedCredential({ - credentialProps: commonData, - onChainData: validOnChainData - }); - - expect(resavedCredential).toEqual({ ...issuedCredential, ...validOnChainData }); - }); - - it('should throw invalid input error if both proposalId and rewardApplicationId are provided', async () => { - await expect( - saveIssuedCredential({ - credentialProps: { - credentialTemplateId: proposalCredentialTemplate.id, - userId: user.id, - credentialEvent: 'proposal_approved', - schemaId: proposalCredentialSchemaId, - proposalId: uuid(), - rewardApplicationId: uuid() - } - }) - ).rejects.toThrow(InvalidInputError); - }); - - it('should throw invalid input error if neither proposalId nor rewardApplicationId is provided, or they are both provided', async () => { - await expect( - saveIssuedCredential({ - credentialProps: { - credentialTemplateId: proposalCredentialTemplate.id, - userId: user.id, - credentialEvent: 'proposal_approved', - schemaId: proposalCredentialSchemaId, - proposalId: undefined, - rewardApplicationId: undefined - }, - offchainData: { - ceramicId: pseudoRandomHexString(), - ceramicRecord: { data: 'dataExample' } - } - }) - ).rejects.toThrow(InvalidInputError); - - await expect( - saveIssuedCredential({ - credentialProps: { - credentialTemplateId: proposalCredentialTemplate.id, - userId: user.id, - credentialEvent: 'proposal_approved', - schemaId: proposalCredentialSchemaId, - proposalId: uuid(), - rewardApplicationId: uuid() - }, - offchainData: { - ceramicId: pseudoRandomHexString(), - ceramicRecord: { data: 'dataExample' } - } - }) - ).rejects.toThrow(InvalidInputError); - }); - - it('should throw invalid input error if no offchainData or onChainData is provided', async () => { - await expect( - saveIssuedCredential({ - credentialProps: { - credentialTemplateId: proposalCredentialTemplate.id, - userId: user.id, - credentialEvent: 'proposal_approved', - schemaId: proposalCredentialSchemaId, - proposalId: uuid(), - rewardApplicationId: undefined - } - }) - ).rejects.toThrow(InvalidInputError); - }); -}); diff --git a/lib/credentials/attestOffchain.ts b/lib/credentials/attestOffchain.ts deleted file mode 100644 index de4e64b8ca..0000000000 --- a/lib/credentials/attestOffchain.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { InvalidInputError } from '@charmverse/core/errors'; -import { log } from '@charmverse/core/log'; -import { prisma } from '@charmverse/core/prisma-client'; -import type { AttestationType, CredentialEventType } from '@charmverse/core/prisma-client'; -import type { - AttestationShareablePackageObject, - SignedOffchainAttestation -} from '@ethereum-attestation-service/eas-sdk'; -import { Offchain, createOffchainURL } from '@ethereum-attestation-service/eas-sdk'; -import { credentialsWalletPrivateKey } from '@root/config/constants'; -import { getChainById } from '@root/connectors/chains'; -import { trackUserAction } from '@root/lib/metrics/mixpanel/trackUserAction'; -import { isValidChainAddress } from '@root/lib/tokens/validation'; -import { Wallet, providers } from 'ethers'; -import { v4 as uuid } from 'uuid'; - -import type { EasSchemaChain } from './connectors'; -import { easSchemaChains, getEasConnector } from './connectors'; -import { getEasInstance } from './getEasInstance'; -import type { PublishedSignedCredential } from './queriesAndMutations'; -import { publishSignedCredential } from './queriesAndMutations'; -import { saveIssuedCredential } from './saveIssuedCredential'; -import { attestationSchemaIds } from './schemas'; -import { encodeAttestation } from './schemas/encodeAttestation'; -import type { CredentialData } from './schemas/interfaces'; - -type AttestationInput = { - recipient: string; - credential: CredentialData; - signer: Wallet; - attester: string; - chainId: EasSchemaChain; - linkedAttestationUid?: string; -}; - -async function attestOffchain({ - credential, - recipient, - attester, - chainId, - signer, - linkedAttestationUid -}: AttestationInput): Promise { - if (!signer) { - throw new InvalidInputError(`Signer is required to attest`); - } else if (!isValidChainAddress(recipient) || !isValidChainAddress(attester)) { - throw new InvalidInputError(`Invalid address`); - } else if (!easSchemaChains.includes(chainId)) { - throw new InvalidInputError(`Unsupported chainId`); - } - - const eas = getEasInstance(chainId); - - // We are currently running on pre v1 EAS in order to maintain ethers v5. In order to bypass contract errors, we need to manually instantiate offchain - const offchain = new Offchain( - { - address: eas.contract.address, - chainId, - version: '1' - }, - 1 - ); - - // (signer as any).signTypedData = (signer as any)._signTypedData; - const signedOffchainAttestation = await offchain.signOffchainAttestation( - { - recipient: recipient.toLowerCase(), - // Unix timestamp of when attestation expires. (0 for no expiration) - expirationTime: BigInt(0), - // Unix timestamp of current time - time: BigInt(Math.floor(Date.now() / 1000)), - revocable: true, - version: 1, - nonce: BigInt(0), - schema: attestationSchemaIds[credential.type], - refUID: linkedAttestationUid ?? '0x0000000000000000000000000000000000000000000000000000000000000000', - data: encodeAttestation(credential) - }, - signer - ); - return signedOffchainAttestation; -} - -export type CharmVerseCredentialInput = { - chainId: EasSchemaChain; - credential: CredentialData; - recipient: string; -}; - -export type SignedAttestation = { - sig: SignedOffchainAttestation; - signer: string; - verificationUrl: string; - credentialData: CredentialData; - recipient: string; - timestamp: number; -}; - -/** - * Only the raw offchain signed credential is returned. The call will handle persisting or publishing this signature - * */ -async function signCharmverseAttestation({ - chainId, - credential, - recipient -}: CharmVerseCredentialInput): Promise { - const provider = new providers.JsonRpcProvider(getChainById(chainId)?.rpcUrls[0] as string, chainId); - - const wallet = new Wallet(credentialsWalletPrivateKey as string, provider); - - const signature = await attestOffchain({ - attester: wallet.address, - recipient, - chainId, - signer: wallet, - credential - }); - - const offchainCredentialVerificationUrl = getOffchainUrl({ - chainId, - pkg: { sig: signature, signer: wallet.address } - }); - - const signedCredential: SignedAttestation = { - sig: signature, - signer: wallet.address, - verificationUrl: offchainCredentialVerificationUrl, - credentialData: credential, - recipient, - timestamp: Number(signature.message.time) * 1000 - }; - return signedCredential; -} - -/** - * Sign the credential offchain and send to IPFS - * - * Only useful for scripts. Prefer signPublishAndRecordCharmverseCredential for production use with existing users - */ -export async function signAndPublishCharmverseCredential({ - chainId, - credential, - recipient -}: CharmVerseCredentialInput) { - const signedCredential = await signCharmverseAttestation({ chainId, credential, recipient }); - - const contentToPublish: Omit = { - chainId, - recipient: signedCredential.recipient, - content: credential.data, - timestamp: new Date(signedCredential.timestamp), - type: credential.type, - verificationUrl: signedCredential.verificationUrl, - issuer: signedCredential.signer, - schemaId: attestationSchemaIds[credential.type], - sig: JSON.stringify(signedCredential.sig) - }; - - const published = await publishSignedCredential(contentToPublish); - - return published; -} - -/** - * Sign the credential offchain, send to IPFS, record as Issued Credential - * */ -export async function signPublishAndRecordCharmverseCredential({ - chainId, - credential, - recipient, - event, - recipientUserId, - pageId, - proposalId, - rewardApplicationId, - credentialTemplateId -}: CharmVerseCredentialInput & { - event: CredentialEventType; - recipientUserId: string; - credentialTemplateId: string; - pageId: string; - rewardApplicationId?: string; - proposalId?: string; -}) { - const { spaceId } = await prisma.credentialTemplate.findUniqueOrThrow({ - where: { - id: credentialTemplateId - }, - select: { - spaceId: true - } - }); - - const signedCredential = await signCharmverseAttestation({ chainId, credential, recipient }); - - const publishedCredentialId = uuid(); - - const schemaId = attestationSchemaIds[credential.type]; - - const contentToPublish: Omit = { - chainId, - recipient: signedCredential.recipient, - content: credential.data, - timestamp: new Date(signedCredential.timestamp), - type: credential.type, - verificationUrl: signedCredential.verificationUrl, - issuer: signedCredential.signer, - schemaId, - sig: JSON.stringify(signedCredential.sig), - charmverseId: publishedCredentialId - }; - - const published = await publishSignedCredential(contentToPublish); - - await saveIssuedCredential({ - credentialProps: { - credentialEvent: event, - credentialTemplateId, - schemaId: attestationSchemaIds[credential.type], - userId: recipientUserId, - proposalId, - rewardApplicationId - }, - offchainData: { - ceramicId: published.id, - ceramicRecord: published - } - }); - - trackUserAction('credential_issued', { - userId: recipientUserId, - spaceId, - trigger: event, - credentialTemplateId - }); - - log.info('Issued credential', { - pageId, - event, - proposalId, - rewardApplicationId, - userId: recipientUserId, - credentialTemplateId - }); - - return published; -} - -function getOffchainUrl({ chainId, pkg }: { pkg: AttestationShareablePackageObject; chainId: EasSchemaChain }) { - return `${getEasConnector(chainId).attestationExplorerUrl}${createOffchainURL(pkg)}`; -} diff --git a/lib/credentials/createOffchainCredentialsForExternalProjects.ts b/lib/credentials/createOffchainCredentialsForExternalProjects.ts deleted file mode 100644 index eca2563ba5..0000000000 --- a/lib/credentials/createOffchainCredentialsForExternalProjects.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { log } from '@charmverse/core/log'; -import { createOffchainCredentialsForProjects as createGitcoinOffchainCredentialsForProjects } from '@root/lib/gitcoin/createProjectCredentials'; -import { createOffchainCredentialsForProjects as createQuestbookOffchainCredentialsForProjects } from '@root/lib/questbook/createProjectCredentials'; - -export async function createOffchainCredentialsForExternalProjects() { - await createGitcoinOffchainCredentialsForProjects().catch((error) => { - log.error('Error creating Gitcoin offchain credentials', { error }); - }); - await createQuestbookOffchainCredentialsForProjects().catch((error) => { - log.error('Error creating Questbook offchain credentials', { error }); - }); -} diff --git a/lib/credentials/getAllUserCredentials.ts b/lib/credentials/getAllUserCredentials.ts index e2e13e930d..1b03881b20 100644 --- a/lib/credentials/getAllUserCredentials.ts +++ b/lib/credentials/getAllUserCredentials.ts @@ -6,7 +6,6 @@ import { stringUtils } from '@charmverse/core/utilities'; import type { EASAttestationWithFavorite } from './external/getOnchainCredentials'; import { getAllOnChainAttestations } from './external/getOnchainCredentials'; import { getGitcoinCredentialsByWallets } from './getGitcoinCredentialsByWallets'; -import { getExternalCredentialsByWallets, getCharmverseOffchainCredentialsByWallets } from './queriesAndMutations'; // Use these wallets to return at least 1 of all the tracked credentials const testWallets = [ @@ -43,14 +42,6 @@ export async function getAllUserCredentials({ } const allCredentials = await Promise.all([ - getCharmverseOffchainCredentialsByWallets({ wallets }).catch((error) => { - log.error(`Error loading Charmverse Ceramic credentials for user ${userId}`, { error, userId }); - return []; - }), - getExternalCredentialsByWallets({ wallets }).catch((error) => { - log.error(`Error loading External Ceramic credentials for user ${userId}`, { error, userId }); - return []; - }), getGitcoinCredentialsByWallets({ wallets }).catch((error) => { log.error(`Error loading Gitcoin Ceramic credentials for user ${userId}`, { error, userId }); return []; diff --git a/lib/credentials/getProposalOrApplicationCredentials.ts b/lib/credentials/getProposalOrApplicationCredentials.ts index 66b563d82b..cafeb4955c 100644 --- a/lib/credentials/getProposalOrApplicationCredentials.ts +++ b/lib/credentials/getProposalOrApplicationCredentials.ts @@ -5,7 +5,6 @@ import { prisma } from '@charmverse/core/prisma-client'; import type { EasSchemaChain } from './connectors'; import { getOnchainCredentialsById, type EASAttestationFromApi } from './external/getOnchainCredentials'; -import { getCharmverseOffchainCredentialsByIds, getParsedCredential } from './queriesAndMutations'; export async function getProposalOrApplicationCredentials({ proposalId, @@ -35,25 +34,13 @@ export async function getProposalOrApplicationCredentials({ (acc, val) => { if (val.onchainAttestationId) { acc.onchain.push(val); - } else if (val.ceramicId) { - acc.offchain.push(val); } return acc; }, - { offchain: [], onchain: [] } as { offchain: IssuedCredential[]; onchain: IssuedCredential[] } + { onchain: [] } as { onchain: IssuedCredential[] } ) ); - const offchainData = await getCharmverseOffchainCredentialsByIds({ - ceramicIds: issuedCredentials.offchain.map((offchain) => offchain.ceramicId as string) - }).catch((err) => { - log.error('Error fetching offchain credentials', err); - - return issuedCredentials.offchain - .map((offchain) => (offchain.ceramicRecord ? getParsedCredential(offchain.ceramicRecord as any) : null)) - .filter(Boolean) as EASAttestationFromApi[]; - }); - const onChainData = await getOnchainCredentialsById({ attestations: issuedCredentials.onchain.map((c) => ({ chainId: c.onchainChainId as EasSchemaChain, @@ -61,5 +48,5 @@ export async function getProposalOrApplicationCredentials({ })) }); - return [...onChainData, ...offchainData]; + return [...onChainData]; } diff --git a/lib/credentials/issueOffchainProposalCredentialsIfNecessary.ts b/lib/credentials/issueOffchainProposalCredentialsIfNecessary.ts deleted file mode 100644 index 3ad4889154..0000000000 --- a/lib/credentials/issueOffchainProposalCredentialsIfNecessary.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { DataNotFoundError, InvalidInputError } from '@charmverse/core/errors'; -import { log } from '@charmverse/core/log'; -import type { CredentialEventType, CredentialTemplate } from '@charmverse/core/prisma-client'; -import { prisma } from '@charmverse/core/prisma-client'; -import { getCurrentEvaluation } from '@charmverse/core/proposals'; -import { getFeatureTitle } from '@root/lib/features/getFeatureTitle'; -import { getPagePermalink } from '@root/lib/pages/getPagePermalink'; -import { optimism } from 'viem/chains'; - -import { signPublishAndRecordCharmverseCredential } from './attestOffchain'; -import { credentialEventLabels, disableCredentialAutopublish } from './constants'; -import type { CredentialDataInput } from './schemas/interfaces'; - -export async function issueOffchainProposalCredentialsIfNecessary({ - proposalId, - event -}: { - proposalId: string; - event: Extract; -}): Promise { - if (disableCredentialAutopublish) { - log.warn('Published credentials are disabled'); - return; - } - - if (event !== 'proposal_approved') { - throw new InvalidInputError(`Invalid event type: ${event} for proposal credentials`); - } - - const baseProposal = await prisma.proposal.findUniqueOrThrow({ - where: { - id: proposalId - }, - select: { - selectedCredentialTemplates: true, - status: true, - evaluations: { - orderBy: { - index: 'asc' - } - }, - page: { - select: { - type: true - } - }, - space: { - select: { - useOnchainCredentials: true - } - } - } - }); - - if (baseProposal?.page?.type === 'proposal_template') { - return; - } - - if (baseProposal.status === 'draft') { - return; - } - if (!baseProposal.selectedCredentialTemplates?.length) { - return; - } - const currentEvaluation = getCurrentEvaluation(baseProposal.evaluations); - - if (!currentEvaluation) { - return; - } else if ( - !( - (currentEvaluation.finalStep || - currentEvaluation.appealedAt || - currentEvaluation.id === baseProposal.evaluations[baseProposal.evaluations.length - 1].id) && - currentEvaluation.result === 'pass' - ) - ) { - return; - } - - const proposalWithSpaceConfig = await prisma.proposal.findFirstOrThrow({ - where: { - id: proposalId, - page: { - type: 'proposal' - } - }, - select: { - selectedCredentialTemplates: true, - status: true, - page: { - select: { - id: true - } - }, - authors: true, - space: { - select: { - id: true, - features: true, - credentialTemplates: { - where: { - credentialEvents: { - has: event - } - } - } - } - } - } - }); - - if (!proposalWithSpaceConfig.page) { - throw new DataNotFoundError(`Proposal with id ${proposalId} has no matching page`); - } - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - credentialEvent: event, - proposalId, - userId: { - in: proposalWithSpaceConfig.authors.map((author) => author.userId) - } - } - }); - - // Credential template ids grouped by user id - const credentialsToIssue: Record = {}; - - for (const author of proposalWithSpaceConfig.authors) { - proposalWithSpaceConfig.selectedCredentialTemplates.forEach((credentialTemplateId) => { - const userHasNotReceivedCredential = !issuedCredentials.some( - (issuedCredential) => - issuedCredential.credentialTemplateId === credentialTemplateId && - issuedCredential.userId === author.userId && - !!issuedCredential.ceramicId - ); - if ( - userHasNotReceivedCredential && - // Only credentials which match the event will have been returned by the query - proposalWithSpaceConfig.space.credentialTemplates.some((t) => t.id === credentialTemplateId) - ) { - if (!credentialsToIssue[author.userId]) { - credentialsToIssue[author.userId] = []; - } - credentialsToIssue[author.userId].push(credentialTemplateId); - } - }); - } - - const uniqueAuthors = Object.keys(credentialsToIssue); - - for (const authorUserId of uniqueAuthors) { - const credentialsToGiveUser = credentialsToIssue[authorUserId].map( - (cred) => proposalWithSpaceConfig.space.credentialTemplates.find((t) => t.id === cred) as CredentialTemplate - ); - - const author = await prisma.user.findUniqueOrThrow({ - where: { - id: authorUserId - }, - select: { - wallets: true, - primaryWallet: true - } - }); - - const targetWallet = author.primaryWallet ?? author.wallets[0]; - - if (targetWallet) { - try { - for (const credentialTemplate of credentialsToGiveUser) { - const getEventLabel = credentialEventLabels[event]; - if (!getEventLabel) { - throw new Error(`No label mapper found for event: ${event}`); - } - const eventLabel = getEventLabel((value) => - getFeatureTitle(value, proposalWithSpaceConfig.space.features as any[]) - ); - - const credentialContent: CredentialDataInput<'proposal'> = { - Name: credentialTemplate.name, - Description: credentialTemplate.description ?? '', - Organization: credentialTemplate.organization, - Event: eventLabel, - URL: getPagePermalink({ pageId: proposalWithSpaceConfig.page.id }) - }; - - await signPublishAndRecordCharmverseCredential({ - chainId: optimism.id, - recipient: targetWallet.address, - credential: { - type: 'proposal', - data: credentialContent - }, - credentialTemplateId: credentialTemplate.id, - event, - recipientUserId: authorUserId, - proposalId, - pageId: proposalWithSpaceConfig.page.id - }); - } - } catch (e) { - log.error('Failed to issue credential', { - pageId: proposalWithSpaceConfig.page.id, - proposalId, - userId: authorUserId, - credentialsToGiveUser, - error: e - }); - } - } - } -} diff --git a/lib/credentials/issueOffchainRewardCredentialsIfNecessary.ts b/lib/credentials/issueOffchainRewardCredentialsIfNecessary.ts deleted file mode 100644 index d657d48560..0000000000 --- a/lib/credentials/issueOffchainRewardCredentialsIfNecessary.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { DataNotFoundError, InvalidInputError } from '@charmverse/core/errors'; -import { log } from '@charmverse/core/log'; -import type { CredentialEventType, CredentialTemplate } from '@charmverse/core/prisma-client'; -import { prisma } from '@charmverse/core/prisma-client'; -import { getFeatureTitle } from '@root/lib/features/getFeatureTitle'; -import { getSubmissionPagePermalink } from '@root/lib/pages/getPagePermalink'; -import { optimism } from 'viem/chains'; - -import { signPublishAndRecordCharmverseCredential } from './attestOffchain'; -import { credentialEventLabels, disableCredentialAutopublish } from './constants'; -import type { CredentialDataInput } from './schemas/interfaces'; - -export async function issueOffchainRewardCredentialsIfNecessary({ - rewardId, - event, - submissionId -}: { - rewardId: string; - event: Extract; - submissionId?: string; -}): Promise { - if (disableCredentialAutopublish) { - log.warn('Published credentials are disabled'); - return; - } - - if (event !== 'reward_submission_approved') { - throw new InvalidInputError(`Invalid event type: ${event} for reward credentials`); - } - - const baseReward = await prisma.bounty.findUniqueOrThrow({ - where: { - id: rewardId - }, - select: { - selectedCredentialTemplates: true, - page: { - select: { - id: true, - path: true - } - }, - applications: { - where: { - id: submissionId, - status: { - in: ['complete', 'processing', 'paid'] - } - }, - select: { - id: true, - createdBy: true - } - }, - space: { - select: { - id: true, - useOnchainCredentials: true, - features: true, - credentialTemplates: { - where: { - credentialEvents: { - has: event - } - } - } - } - } - } - }); - - if (!baseReward.page) { - throw new DataNotFoundError(`Reward with id ${rewardId} has no matching page`); - } - - // If no complete submissions, no selected templates, or no templates available for the event, early exit - if ( - !baseReward.applications.length || - !baseReward.selectedCredentialTemplates?.length || - !baseReward.space.credentialTemplates.length - ) { - return; - } - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: { - credentialEvent: event, - rewardApplicationId: { - in: baseReward.applications.map((app) => app.id) - } - } - }); - - // Credential template ids grouped by user id - const credentialsToIssue: Record = {}; - - for (const application of baseReward.applications) { - const submitterUserId = application.createdBy; - baseReward.selectedCredentialTemplates.forEach((credentialTemplateId) => { - const userHasNotReceivedCredential = !issuedCredentials.some( - (issuedCredential) => - issuedCredential.credentialTemplateId === credentialTemplateId && - issuedCredential.userId === submitterUserId && - issuedCredential.rewardApplicationId === application.id && - issuedCredential.ceramicId - ); - if ( - userHasNotReceivedCredential && - // Only credentials which match the event will have been returned by the query - baseReward.space.credentialTemplates.some((t) => t.id === credentialTemplateId) - ) { - if (!credentialsToIssue[submitterUserId]) { - credentialsToIssue[submitterUserId] = []; - } - credentialsToIssue[submitterUserId].push({ - rewardApplicationId: application.id, - credentialTemplateId - }); - } - }); - } - - const uniqueSubmitters = Object.keys(credentialsToIssue); - - for (const submitterUserId of uniqueSubmitters) { - const submitter = await prisma.user.findUniqueOrThrow({ - where: { - id: submitterUserId - }, - select: { - wallets: true, - primaryWallet: true - } - }); - - const credentialsToGiveUser = credentialsToIssue[submitterUserId].map((cred) => ({ - ...(baseReward.space.credentialTemplates.find((t) => t.id === cred.credentialTemplateId) as CredentialTemplate), - applicationId: cred.rewardApplicationId - })); - - const targetWallet = submitter.primaryWallet ?? submitter.wallets[0]; - - if (targetWallet) { - try { - for (const credentialTemplate of credentialsToGiveUser) { - const getEventLabel = credentialEventLabels[event]; - if (!getEventLabel) { - throw new Error(`No label mapper found for event: ${event}`); - } - const eventLabel = getEventLabel((value) => getFeatureTitle(value, baseReward.space.features as any[])); - - const credentialContent: CredentialDataInput<'reward'> = { - Name: credentialTemplate.name, - Description: credentialTemplate.description ?? '', - Organization: credentialTemplate.organization, - Event: eventLabel, - rewardURL: getSubmissionPagePermalink({ submissionId: credentialTemplate.applicationId }) - }; - // Iterate through credentials one at a time so we can ensure they're properly created and tracked - await signPublishAndRecordCharmverseCredential({ - chainId: optimism.id, - recipient: targetWallet.address, - credential: { - type: 'reward', - data: credentialContent - }, - credentialTemplateId: credentialTemplate.id, - event, - recipientUserId: submitterUserId, - rewardApplicationId: credentialTemplate.applicationId, - pageId: baseReward.page.id - }); - } - } catch (e) { - log.error('Failed to issue credential', { - pageId: baseReward.page.id, - rewardId, - userId: submitterUserId, - credentialsToGiveUser, - error: e - }); - } - } - } -} diff --git a/lib/credentials/queriesAndMutations.ts b/lib/credentials/queriesAndMutations.ts deleted file mode 100644 index 39f4551fd9..0000000000 --- a/lib/credentials/queriesAndMutations.ts +++ /dev/null @@ -1,384 +0,0 @@ -import { gql } from '@apollo/client'; -import { log } from '@charmverse/core/log'; -import { prisma, type AttestationType } from '@charmverse/core/prisma-client'; -import { credentialsWalletPrivateKey, graphQlServerEndpoint, isDevEnv, isStagingEnv } from '@root/config/constants'; -import { Wallet } from 'ethers'; - -import { ApolloClientWithRedisCache } from './apolloClientWithRedisCache'; -import type { EasSchemaChain } from './connectors'; -import type { EASAttestationFromApi, EASAttestationWithFavorite } from './external/getOnchainCredentials'; -import type { ExternalCredentialChain } from './external/schemas'; -import { externalCredentialSchemaId } from './schemas/external'; -import type { CredentialData } from './schemas/interfaces'; -import { proposalCredentialSchemaId } from './schemas/proposal'; -import { rewardCredentialSchemaId } from './schemas/reward'; - -const ceramicGraphQlClient = new ApolloClientWithRedisCache({ - uri: graphQlServerEndpoint, - // Allows us to bypass native - persistForSeconds: 300, - skipRedisCache: isStagingEnv || isDevEnv, - cacheKeyPrefix: 'ceramic' -}); - -type CredentialFromCeramic = { - id: string; - issuer: string; - recipient: string; - content: string; - sig: string; - type: AttestationType; - verificationUrl: string; - chainId: ExternalCredentialChain | EasSchemaChain; - schemaId: string; - charmverseId?: string; - timestamp: Date; -}; - -/** - * @content - The actual keymap values of the credential created using EAS - */ -export type PublishedSignedCredential = Omit< - CredentialFromCeramic, - 'content' -> & { - content: CredentialData['data']; -}; - -const CREATE_SIGNED_CREDENTIAL_MUTATION = gql` - mutation CreateCredentials($i: CreateCharmverseCredentialInput!) { - createCharmverseCredential(input: $i) { - document { - id - type - issuer - chainId - content - schemaId - recipient - verificationUrl - timestamp - charmverseId - } - } - } -`; - -export type CredentialToPublish = Omit; - -export function getParsedCredential(credential: CredentialFromCeramic): EASAttestationFromApi { - let parsed = {} as any; - - if (typeof credential.content === 'object') { - parsed = credential.content; - } else { - try { - const parsedData = JSON.parse(credential.content); - parsed = parsedData; - } catch (err) { - log.error(`Failed to parse content from ceramic record ${credential.id}`); - } - } - - return { - ...credential, - content: parsed, - attester: credential.issuer, - timeCreated: new Date(credential.timestamp).valueOf(), - type: 'charmverse' - }; -} - -export async function publishSignedCredential(input: CredentialToPublish): Promise { - const record = await ceramicGraphQlClient - .mutate({ - mutation: CREATE_SIGNED_CREDENTIAL_MUTATION, - variables: { - i: { - content: { - ...input, - content: JSON.stringify(input.content), - issuer: input.issuer.toLowerCase(), - recipient: input.recipient.toLowerCase(), - timestamp: new Date(input.timestamp).toISOString() - } - } - } - }) - .then((doc) => getParsedCredential(doc.data.createCharmverseCredential.document)); - - return record; -} - -const GET_CREDENTIALS = gql` - query GetCredentials($filter: CharmverseCredentialFiltersInput!) { - charmverseCredentialIndex(filters: $filter, first: 1000) { - edges { - node { - id - issuer - recipient - content - type - verificationUrl - chainId - schemaId - timestamp - charmverseId - } - } - } - } -`; - -const GET_CREDENTIALS_BY_ID = gql` - query GetCredentialsById($ids: [ID!]!) { - nodes(ids: $ids) { - id - __typename - ... on CharmverseCredential { - id - issuer - recipient - content - type - verificationUrl - chainId - schemaId - timestamp - charmverseId - } - } - } -`; - -export async function getCharmverseOffchainCredentialsByIds({ - ceramicIds -}: { - ceramicIds: string[]; -}): Promise { - if (!ceramicIds.length) { - return []; - } - - const charmverseCredentials: EASAttestationFromApi[] | null = await ceramicGraphQlClient - .query({ - query: GET_CREDENTIALS_BY_ID, - variables: { - ids: ceramicIds - } - // For now, let's refetch each time and rely on http endpoint-level caching - // https://www.apollographql.com/docs/react/data/queries/#supported-fetch-policies - }) - .then((response) => - response - ? response.data.nodes.map((e: any) => getParsedCredential(e)) - : Promise.reject(new Error('Unknown error')) - ) - .catch((err) => { - log.error('Failed to fetch offchain credentials from ceramic', { error: err, ceramicIds }); - return null; - }); - - return charmverseCredentials ?? []; -} - -export async function getCharmverseOffchainCredentialsByWallets({ - wallets -}: { - wallets: string[]; -}): Promise { - if (typeof credentialsWalletPrivateKey !== 'string') { - return []; - } - const credentialWalletAddress = new Wallet(credentialsWalletPrivateKey).address.toLowerCase(); - if (!wallets.length) { - return []; - } - - const lowerCaseWallets = wallets.map((w) => w.toLowerCase()); - - const charmverseCredentials: EASAttestationFromApi[] | null = await ceramicGraphQlClient - .query({ - query: GET_CREDENTIALS, - variables: { - filter: { - where: { - schemaId: { in: [proposalCredentialSchemaId, rewardCredentialSchemaId] }, - recipient: { in: lowerCaseWallets }, - issuer: { equalTo: credentialWalletAddress } - } - } - } - // For now, let's refetch each time and rely on http endpoint-level caching - // https://www.apollographql.com/docs/react/data/queries/#supported-fetch-policies - }) - .then((response) => - response - ? response.data.charmverseCredentialIndex.edges.map((e: any) => getParsedCredential(e.node)) - : Promise.reject(new Error('Unknown error')) - ) - .catch((err) => { - log.error('Failed to fetch offchain credentials from ceramic', { error: err, wallets }); - return null; - }); - - const credentialIds = charmverseCredentials?.map((c) => c.id); - - const issuedCredentials = await prisma.issuedCredential.findMany({ - where: charmverseCredentials - ? { - ceramicId: { - in: credentialIds - } - } - : { - user: { - wallets: { - some: { - address: { - in: wallets - } - } - } - } - }, - select: { - id: true, - ceramicId: true, - // Only fetch the saved record if we failed to fetch data from ceramic - ceramicRecord: !charmverseCredentials, - onchainAttestationId: true, - rewardApplication: { - select: { - bounty: { - select: { - space: { - select: { - spaceArtwork: true, - credentialLogo: true - } - } - } - } - } - }, - proposal: { - select: { - space: { - select: { - spaceArtwork: true, - credentialLogo: true - } - } - } - } - } - }); - - const issuedCredsMap = issuedCredentials.reduce( - (acc, val) => { - acc[val.ceramicId as string] = val; - return acc; - }, - {} as Record - ); - - const favoriteCredentials = await prisma.favoriteCredential.findMany({ - where: { - issuedCredentialId: { - in: issuedCredentials.map((a) => a.id) - } - }, - select: { - index: true, - issuedCredentialId: true, - id: true - } - }); - - const sourceData = charmverseCredentials - ? charmverseCredentials - // Only display IPFS credentials for which we have a reference in our database, and which have not been attested on-chain - .filter( - (credentialFromCeramic) => - !!issuedCredsMap[credentialFromCeramic.id] && !issuedCredsMap[credentialFromCeramic.id].onchainAttestationId - ) - : issuedCredentials - .filter((ic) => !!ic.ceramicRecord) - .map((cachedCred) => getParsedCredential(cachedCred.ceramicRecord as any as CredentialFromCeramic)); - - return sourceData.map((credential) => { - const issuedCredential = issuedCredentials.find((ic) => ic.ceramicId === credential.id); - const favoriteCredential = favoriteCredentials.find((fc) => fc.issuedCredentialId === issuedCredential?.id); - const iconUrl = - (issuedCredential?.proposal ?? issuedCredential?.rewardApplication?.bounty)?.space.credentialLogo || - (issuedCredential?.proposal ?? issuedCredential?.rewardApplication?.bounty)?.space.spaceArtwork || - null; - - if (favoriteCredential) { - return { - ...credential, - iconUrl, - favoriteCredentialId: favoriteCredential.id, - index: favoriteCredential.index, - issuedCredentialId: issuedCredential?.id - }; - } - - return { - ...credential, - iconUrl, - favoriteCredentialId: null, - index: -1, - issuedCredentialId: issuedCredential?.id - }; - }); -} - -export async function getExternalCredentialsByWallets({ - wallets -}: { - wallets: string[]; -}): Promise { - if (typeof credentialsWalletPrivateKey !== 'string') { - return []; - } - const credentialWalletAddress = new Wallet(credentialsWalletPrivateKey).address.toLowerCase(); - if (!wallets.length) { - return []; - } - - const externalCredentials: EASAttestationFromApi[] = await ceramicGraphQlClient - .query({ - query: GET_CREDENTIALS, - variables: { - filter: { - where: { - schemaId: { in: [externalCredentialSchemaId] }, - recipient: { in: wallets.map((w) => w.toLowerCase()) }, - issuer: { equalTo: credentialWalletAddress } - } - } - } - // For now, let's refetch each time and rely on http endpoint-level caching - // https://www.apollographql.com/docs/react/data/queries/#supported-fetch-policies - }) - .then((response) => - response - ? response.data.charmverseCredentialIndex.edges.map((e: any) => getParsedCredential(e.node)) - : Promise.reject(new Error('Unknown error')) - ); - - const blacklistedNameRegexes = [/test/i, /demo/i]; - - return externalCredentials - .map((credential) => ({ - ...credential, - iconUrl: null, - favoriteCredentialId: null, - index: -1, - issuedCredentialId: undefined - })) - .filter((c) => c?.content.Name && !blacklistedNameRegexes.some((pattern) => pattern.test(c.content.Name))); -} diff --git a/lib/credentials/saveIssuedCredential.ts b/lib/credentials/saveIssuedCredential.ts index 9c80f0f0fb..1d9b2c303c 100644 --- a/lib/credentials/saveIssuedCredential.ts +++ b/lib/credentials/saveIssuedCredential.ts @@ -22,19 +22,17 @@ export type IdenticalCredentialProps = { type IssuedCredentialToSave = { credentialProps: IdenticalCredentialProps; - offchainData?: NonNullableValues>; - onChainData?: NonNullableValues>; + onChainData: NonNullableValues>; }; export async function saveIssuedCredential({ credentialProps: { credentialEvent, credentialTemplateId, schemaId, userId, proposalId, rewardApplicationId }, - offchainData, onChainData }: IssuedCredentialToSave): Promise { if ((!proposalId && !rewardApplicationId) || (proposalId && rewardApplicationId)) { throw new InvalidInputError('Either proposalId or rewardApplicationId must be provided'); } - if (!offchainData && !onChainData) { + if (!onChainData) { throw new InvalidInputError('Either offchainData or onChainData must be provided'); } @@ -56,9 +54,7 @@ export async function saveIssuedCredential({ where: { id: existingCredential.id }, data: { onchainChainId: onChainData?.onchainChainId, - onchainAttestationId: onChainData?.onchainAttestationId, - ceramicId: offchainData?.ceramicId, - ceramicRecord: offchainData?.ceramicRecord + onchainAttestationId: onChainData?.onchainAttestationId } }); } else { @@ -70,8 +66,6 @@ export async function saveIssuedCredential({ schemaId, onchainChainId: onChainData?.onchainChainId, onchainAttestationId: onChainData?.onchainAttestationId, - ceramicId: offchainData?.ceramicId, - ceramicRecord: offchainData?.ceramicRecord, proposal: proposalId ? { connect: { id: proposalId } } : undefined, rewardApplication: rewardApplicationId ? { connect: { id: rewardApplicationId } } : undefined } diff --git a/lib/gitcoin/createProjectCredentials.ts b/lib/gitcoin/createProjectCredentials.ts deleted file mode 100644 index 723fbc7451..0000000000 --- a/lib/gitcoin/createProjectCredentials.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { log } from '@charmverse/core/log'; -import { prisma } from '@charmverse/core/prisma-client'; -import { signAndPublishCharmverseCredential } from '@root/lib/credentials/attestOffchain'; -import type { ExternalProjectMetadata } from '@root/lib/credentials/schemas/external'; -import { optimism } from 'viem/chains'; -import { getAddress } from 'viem/utils'; - -import { GITCOIN_SUPPORTED_CHAINS } from './constants'; -import { getProjectOwners } from './getProjectDetails'; -import { getRoundApplicationsWithMeta } from './getRoundApplications'; - -export async function createOffchainCredentialsForProjects() { - for (const chainId of GITCOIN_SUPPORTED_CHAINS) { - const approvedApplications = await getRoundApplicationsWithMeta(chainId); - - log.info(`Running ${approvedApplications.length} approved applications from gitcoin, on chain ${chainId}`); - - for (const application of approvedApplications) { - const metadata = application.metadata; - const recepient = getAddress(metadata.recipient) as `0x${string}`; - const owners = await getProjectOwners([recepient], chainId); - const approvedStatusSnapshot = application.statusSnapshots?.find((s) => String(s.status) === '1'); - const approvedSnapshotDate = new Date((Number(approvedStatusSnapshot?.timestamp) || 0) * 1000).toISOString(); - const credentialDate = approvedStatusSnapshot ? approvedSnapshotDate : new Date().toISOString(); - const roundUrl = `https://explorer.gitcoin.co/#/round/${chainId}/${application.round.id}`; - - const metadataPayload: ExternalProjectMetadata = { - name: metadata.title, - round: metadata.roundName, - proposalUrl: `${roundUrl}/${application.applicationIndex}`, - website: metadata.website, - twitter: metadata.projectTwitter, - github: metadata.userGithub, - proposalId: application.id - }; - - for (const owner of owners) { - const existingExternalProject = await prisma.externalProject.findFirst({ - where: { - metadata: { - path: ['proposalId'], - equals: application.id - } - } - }); - - if (!existingExternalProject) { - const externalProject = await prisma.externalProject.create({ - data: { - recipient: owner, - source: 'gitcoin', - metadata: metadataPayload - } - }); - - try { - await signAndPublishCharmverseCredential({ - credential: { - type: 'external', - data: { - Name: metadata.title, - ProjectId: externalProject.id, - Source: 'Gitcoin', - Event: 'Approved', - GrantRound: metadata.roundName, - Date: credentialDate, - GrantURL: roundUrl, - URL: `${roundUrl}/${application.applicationIndex}` - } - }, - chainId: optimism.id, - recipient: owner - }); - - log.info( - `External credential created for Gitcoin round application id ${application.id} and chain id ${chainId}` - ); - } catch (err) { - log.debug( - `Failed to create external credential for Gitcoin round application id ${application.id} and chain id ${chainId}` - ); - } - } - } - } - } -} diff --git a/lib/proposals/createRewardsForProposal.ts b/lib/proposals/createRewardsForProposal.ts index 93f7ec669c..42bffcfa4b 100644 --- a/lib/proposals/createRewardsForProposal.ts +++ b/lib/proposals/createRewardsForProposal.ts @@ -16,7 +16,6 @@ import { publishProposalEventBase } from '@root/lib/webhookPublisher/publishEven import { relay } from '@root/lib/websockets/relay'; import { uniqBy } from 'lodash'; -import { issueOffchainProposalCredentialsIfNecessary } from '../credentials/issueOffchainProposalCredentialsIfNecessary'; import { permissionsApiClient } from '../permissions/api/client'; export async function createRewardsForProposal({ proposalId, userId }: { userId: string; proposalId: string }) { @@ -196,10 +195,5 @@ export async function createRewardsForProposal({ proposalId, userId }: { userId: }); } - await issueOffchainProposalCredentialsIfNecessary({ - event: 'proposal_approved', - proposalId - }); - return updatedProposal; } diff --git a/lib/questbook/createProjectCredentials.ts b/lib/questbook/createProjectCredentials.ts deleted file mode 100644 index 618de4cf0d..0000000000 --- a/lib/questbook/createProjectCredentials.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { log } from '@charmverse/core/log'; -import { prisma } from '@charmverse/core/prisma-client'; -import { signAndPublishCharmverseCredential } from '@root/lib/credentials/attestOffchain'; -import type { ExternalProjectMetadata } from '@root/lib/credentials/schemas/external'; -import { getProjectOwners } from '@root/lib/gitcoin/getProjectDetails'; -import { optimism } from 'viem/chains'; -import { getAddress } from 'viem/utils'; - -import { getGrantApplicationsWithMeta } from './getGrantApplications'; -import { QUESTBOOK_SUPPORTED_CHAINS } from './graphql/endpoints'; - -export async function createOffchainCredentialsForProjects() { - for (const chainId of QUESTBOOK_SUPPORTED_CHAINS) { - const approvedApplications = await getGrantApplicationsWithMeta(chainId); - - log.info(`Running ${approvedApplications.length} approved applications from questbook, on chain ${chainId}`); - - for (const application of approvedApplications) { - const recepient = getAddress(application.recipient) as `0x{string}`; - const owners = await getProjectOwners([recepient], chainId); - const updatedAt = Number(application.date || 0) * 1000; - const credentialDate = updatedAt ? new Date(updatedAt).toISOString() : new Date().toISOString(); - - const metadataPayload: ExternalProjectMetadata = { - name: application.projectName, - round: application.grantTitle, - proposalUrl: application.proposalUrl, - proposalId: application.id, - twitter: application.twitter, - website: '', - github: '' - }; - - for (const owner of owners) { - const existingExternalProject = await prisma.externalProject.findFirst({ - where: { - metadata: { - path: ['proposalId'], - equals: application.id - } - } - }); - - if (!existingExternalProject) { - const externalProject = await prisma.externalProject.create({ - data: { - recipient: owner, - source: 'questbook', - metadata: metadataPayload - } - }); - - try { - await signAndPublishCharmverseCredential({ - credential: { - type: 'external', - data: { - Name: application.projectName, - ProjectId: externalProject.id, - Source: 'Questbook', - Event: 'Grant Approved', - GrantRound: application.grantTitle, - Date: credentialDate, - GrantURL: '', - URL: application.proposalUrl - } - }, - chainId: optimism.id, - recipient: owner - }); - - log.info(`External credential created for Questbook proposal id ${application.id} and chain id ${chainId}`); - } catch (err) { - log.debug( - `Failed to create external credential for Questbook proposal id ${application.id} and chain id ${chainId}` - ); - } - } - } - } - } -} diff --git a/lib/questbook/getGrantApplications.ts b/lib/questbook/getGrantApplications.ts deleted file mode 100644 index 13c69fd6af..0000000000 --- a/lib/questbook/getGrantApplications.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { DataNotFoundError } from '@charmverse/core/errors'; -import { isTruthy } from '@root/lib/utils/types'; -import { getAddress } from 'viem'; - -import { createGraphqlClient } from './graphql/client'; -import type { ChainId } from './graphql/endpoints'; -import { endpoints } from './graphql/endpoints'; -import { getGrantApplicationsQuery } from './graphql/queries'; - -type Application = { - id: string; - applicantId: string; - state: string; - updatedAtS: string; - grant: { - id: string; - title: string; - }; - actions: { - id: string; - state: string; - updatedAtS: string; - }[]; - fields: { - id: string; - values: { - id: string; - value: string; - }[]; - }[]; -}; - -export async function getGrantApplications(chainId: ChainId, startDate?: number) { - const chainEndpoint = endpoints[chainId]; - - if (!chainEndpoint) { - throw new DataNotFoundError(`Chain ${chainId} is not supported`); - } - const client = createGraphqlClient(endpoints[chainId]); - - const { data } = await client.query<{ grantApplications: Application[] }>({ - query: getGrantApplicationsQuery, - variables: { - startDate - } - }); - return data?.grantApplications; -} - -export async function getGrantApplicationsWithMeta(chainId: ChainId) { - const yesterday = yesterdayUnixTimestamp(); - const startDate = Math.round(yesterday); - const applications = await getGrantApplications(chainId, startDate); - - return mapApplications(applications, chainId); -} - -function mapApplications(aplications: Application[], chainId: ChainId) { - const mappedApplications = aplications.map((app) => { - // 1. Get all the required fields - const applicantAddressField = getField(app.fields, 'applicantAddress'); - const applicationApplicantNameField = getField(app.fields, 'applicantName'); - const projectNameField = getField(app.fields, 'projectName'); - const twitterField = getField(app.fields, 'Twitter'); - - // 2. Get all the required values - const recipient = getValueFromField(applicantAddressField); - const applicantName = getValueFromField(applicationApplicantNameField); - const projectName = getValueFromField(projectNameField); - const twitter = getValueFromField(twitterField); - - const proposalUrl = `https://www.questbook.app/dashboard/?proposalId=${app.id}&grantId=${app.grant.id}&role=community&isRenderingProposalBody=true&chainId=${chainId}`; - const date = app.actions.find((action) => action.state === 'approved')?.updatedAtS; - - if (!recipient || !projectName) { - return null; - } - - try { - const recipientAddress = getAddress(recipient); - - return { - id: app.id, - applicantId: app.applicantId, - state: app.state, - grantTitle: app.grant.title, - grantId: app.grant.id, - date, - twitter, - projectName, - recipient: recipientAddress, - applicantName, - proposalUrl - }; - } catch (_err) { - return null; - } - }); - - return mappedApplications.filter(isTruthy); -} - -function getField(fields: Application['fields'], name: string) { - return fields.find((field) => field.id.includes(name)); -} - -function getValueFromField(field?: Application['fields'][0]) { - return field?.values?.[0]?.value; -} - -function yesterdayUnixTimestamp() { - const yesterday = new Date().getTime() - 24 * 60 * 60 * 1000; - return Math.round(yesterday / 1000); -} - -getGrantApplicationsWithMeta(10); diff --git a/lib/questbook/graphql/client.ts b/lib/questbook/graphql/client.ts deleted file mode 100644 index 13c624af04..0000000000 --- a/lib/questbook/graphql/client.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApolloClient, InMemoryCache } from '@apollo/client'; - -export function createGraphqlClient(uri: string) { - return new ApolloClient({ - uri: `${uri}`, - cache: new InMemoryCache({}) - }); -} diff --git a/lib/questbook/graphql/endpoints.ts b/lib/questbook/graphql/endpoints.ts deleted file mode 100644 index 57a4cb1600..0000000000 --- a/lib/questbook/graphql/endpoints.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const endpoints = { - 10: 'https://the-graph.questbook.app/subgraphs/name/qb-subgraph-optimism-mainnet', - 137: 'https://the-graph.questbook.app/subgraphs/name/qb-subgraph-polygon-mainnet' -} as const; - -export type ChainId = keyof typeof endpoints; - -export const QUESTBOOK_SUPPORTED_CHAINS = Object.keys(endpoints).map(Number) as ChainId[]; diff --git a/lib/questbook/graphql/queries.ts b/lib/questbook/graphql/queries.ts deleted file mode 100644 index f03bbdf0b3..0000000000 --- a/lib/questbook/graphql/queries.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { gql } from '@apollo/client'; - -export const getGrantApplicationsQuery = gql` - query getGrantApplicationsQuery( - $first: Int = 1000 - $skip: Int = 0 - $state: String = "approved" - $startDate: Int = 1 - ) { - grantApplications( - where: { state: $state, actions_: { state: $state, updatedAtS_gte: $startDate } } - first: $first - skip: $skip - ) { - id - applicantId - state - updatedAtS - actions { - id - state - updatedAtS - } - grant { - id - title - link - } - fields { - id - values { - id - value - } - } - } - } -`; diff --git a/lib/rewards/closeOutReward.ts b/lib/rewards/closeOutReward.ts index 06a4877dd4..663619a890 100644 --- a/lib/rewards/closeOutReward.ts +++ b/lib/rewards/closeOutReward.ts @@ -1,5 +1,4 @@ import { prisma } from '@charmverse/core/prisma-client'; -import { issueOffchainRewardCredentialsIfNecessary } from '@root/lib/credentials/issueOffchainRewardCredentialsIfNecessary'; import { trackOpUserAction } from '../metrics/mixpanel/trackOpUserAction'; @@ -50,10 +49,5 @@ export async function closeOutReward(rewardId: string): Promise }); } - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId - }); - return getRewardOrThrow({ rewardId }); } diff --git a/lib/rewards/lockApplicationAndSubmissions.ts b/lib/rewards/lockApplicationAndSubmissions.ts index 80c61e7262..019e34f22f 100644 --- a/lib/rewards/lockApplicationAndSubmissions.ts +++ b/lib/rewards/lockApplicationAndSubmissions.ts @@ -1,5 +1,4 @@ import { prisma } from '@charmverse/core/prisma-client'; -import { issueOffchainRewardCredentialsIfNecessary } from '@root/lib/credentials/issueOffchainRewardCredentialsIfNecessary'; import type { RewardWithUsers } from './interfaces'; import { rollupRewardStatus } from './rollupRewardStatus'; @@ -22,10 +21,5 @@ export async function lockApplicationAndSubmissions({ const rollup = await rollupRewardStatus({ rewardId }); - await issueOffchainRewardCredentialsIfNecessary({ - event: 'reward_submission_approved', - rewardId - }); - return rollup; } diff --git a/lib/rewards/markRewardAsPaid.ts b/lib/rewards/markRewardAsPaid.ts index 523979cc5d..afb9f637ad 100644 --- a/lib/rewards/markRewardAsPaid.ts +++ b/lib/rewards/markRewardAsPaid.ts @@ -1,5 +1,4 @@ import { prisma } from '@charmverse/core/prisma-client'; -import { issueOffchainRewardCredentialsIfNecessary } from '@root/lib/credentials/issueOffchainRewardCredentialsIfNecessary'; import { InvalidInputError } from '@root/lib/utils/errors'; import { paidRewardStatuses } from './constants'; @@ -40,10 +39,5 @@ export async function markRewardAsPaid(rewardId: string): Promise { const submission = await prisma.application.findUniqueOrThrow({ @@ -18,12 +17,6 @@ export async function markSubmissionAsPaid(submissionId: string): Promise { - return credential.ceramicRecord as EASAttestationFromApi; - }) - .filter(isTruthy) - .map((ceramicRecord) => { - return { - id: ceramicRecord.id, - source: ceramicRecord.type as 'onchain' | 'charmverse', - chainId: ceramicRecord.chainId, - content: ceramicRecord.content, - attester: ceramicRecord.attester, - recipient: ceramicRecord.recipient, - schemaId: ceramicRecord.schemaId, - createdAt: new Date(ceramicRecord.timeCreated).toISOString(), - verificationUrl: ceramicRecord.verificationUrl - }; - }) + credentials: [] }; }) ) diff --git a/scripts/query.ts b/scripts/query.ts index f760b0cce5..8c1586595d 100644 --- a/scripts/query.ts +++ b/scripts/query.ts @@ -3,43 +3,7 @@ import { prettyPrint } from 'lib/utils/strings'; import { DateTime } from 'luxon'; async function query() { - const result = await prisma.proposalNotification.findMany({ - where: { - proposal: { - page: { - path: 'appeal-test-475296146939983' - } - }, - type: 'proposal_appealed' - // notificationMetadata: { - // user: { - // email: { - // not: null - // }, - // emailNotifications: true - // } - // } - // evaluation: { - // id: 'c35e1b9c-532b-4d6d-9315-a5dfeb920613' - // } - }, - select: { - notificationMetadata: { - select: { - id: true, - seenAt: true, - user: { - select: { - username: true, - id: true, - email: true - } - } - } - }, - evaluationId: true, - type: true - } + const result = await prisma.externalProject.count({ // take: 50 // include: { // events: { @@ -50,59 +14,7 @@ async function query() { // } }); - console.log(result.length); - const metaIds = result.map((r) => r.notificationMetadata.id); - console.log( - await prisma.userNotificationMetadata.updateMany({ - where: { - id: { - in: metaIds - } - }, - data: { seenAt: new Date(), deletedAt: new Date() } - }) - ); - - return; - console.log( - await prisma.spaceRole.count({ - where: { - space: { - domain: 'op-grants' - } - // spaceRoleToRole: { - // some: { - // OR: [ - // { - // role: { - // name: 'GrantNERDS' - // } - // }, - // { - // role: { - // name: 'Approvers' - // } - // } - // ] - // } - // } - } - // select: { - // user: { - // select: { - // username: true - // } - // } - // } - }) - ); - // console.log(await prisma.githubUser.findFirst({ where: { login: 'rikahanabi' }, include: { builder: true } })); - // console.log( - // await prisma.scout.findFirst({ - // where: { id: 'ac1ab2d2-45b6-44a1-b33d-81da68827e3b' }, - // include: { githubUsers: true } - // }) - // ); + console.log(result); } query();