From 785435021f34715230c4de7950ba1a68b836dc5e Mon Sep 17 00:00:00 2001 From: ali Date: Sat, 16 Nov 2024 12:56:38 +0700 Subject: [PATCH 01/33] add dynamic offset for time --- src/scripts/runFundingPotService.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/scripts/runFundingPotService.ts b/src/scripts/runFundingPotService.ts index 42f5dc04f..65a708501 100644 --- a/src/scripts/runFundingPotService.ts +++ b/src/scripts/runFundingPotService.ts @@ -42,10 +42,15 @@ async function generateBatchFile(batchNumber: number) { round.startDate = round.beginDate; } + const now = new Date(); + const offsetSecs = now.getTimezoneOffset() * 60; + const batchConfig = { TIMEFRAME: { - FROM_TIMESTAMP: Math.floor(new Date(round.startDate).getTime() / 1000), // Convert to timestamp - TO_TIMESTAMP: Math.floor(new Date(round.endDate).getTime() / 1000), + FROM_TIMESTAMP: + Math.floor(new Date(round.startDate).getTime() / 1000) - offsetSecs, // Convert to timestamp + TO_TIMESTAMP: + Math.floor(new Date(round.endDate).getTime() / 1000) - offsetSecs, }, VESTING_DETAILS: getStreamDetails(isEarlyAccess), LIMITS: { From ebe7add44f12ccef447fb1266545e2cdde2e42da Mon Sep 17 00:00:00 2001 From: ali Date: Sat, 16 Nov 2024 12:57:16 +0700 Subject: [PATCH 02/33] add dryRun flag --- src/scripts/runFundingPotService.ts | 35 ++++++++++++++++------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/scripts/runFundingPotService.ts b/src/scripts/runFundingPotService.ts index 65a708501..d020b4b94 100644 --- a/src/scripts/runFundingPotService.ts +++ b/src/scripts/runFundingPotService.ts @@ -35,7 +35,7 @@ async function pullLatestVersionOfFundingPot() { } } -async function generateBatchFile(batchNumber: number) { +async function generateBatchFile(batchNumber: number, dryRun: boolean) { console.info(`Generating batch config for batch number: ${batchNumber}`); const { round, isEarlyAccess } = await getRoundByBatchNumber(batchNumber); if (!isEarlyAccess) { @@ -71,7 +71,7 @@ async function generateBatchFile(batchNumber: number) { }, IS_EARLY_ACCESS: isEarlyAccess, // Set based on the round type PRICE: (round.tokenPrice || '0.1').toString(), // Default price to "0.1" if not provided - // ONLY_REPORT: onlyReport, // If we set this flag, only report will be generated and no transactions propose to the safes + ONLY_REPORT: dryRun, // If we set this flag, only report will be generated and no transactions propose to the safes }; const batchFilePath = path.join( @@ -226,7 +226,7 @@ async function installDependencies() { await execShellCommand('npm install --loglevel=error', serviceDir); } -async function runFundingPotService(batchNumber: number) { +async function runFundingPotService(batchNumber: number, dryRun?: boolean) { const command = 'npm run all ' + batchNumber; console.info(`Running "${command}" in ${serviceDir}...`); try { @@ -234,8 +234,10 @@ async function runFundingPotService(batchNumber: number) { } catch (e) { console.error('Error in funding pot execution:', e); } - console.info('Saving reports to the DB...'); - await saveReportsToDB(reportFilesDir); + if (!dryRun) { + console.info('Saving reports to the DB...'); + await saveReportsToDB(reportFilesDir); + } } async function getFirstRoundThatNeedExecuteBatchMinting() { @@ -327,6 +329,7 @@ async function main() { (await getFirstRoundThatNeedExecuteBatchMinting()).batchNumber; console.info('Batch number is:', batchNumber); + const dryRun = Boolean(process.argv[3]) || false; // Step 1 console.info('Start pulling latest version of funding pot service...'); await pullLatestVersionOfFundingPot(); @@ -344,7 +347,7 @@ async function main() { // Step 5 console.info('Create batch config in the funding pot service...'); - await generateBatchFile(batchNumber); + await generateBatchFile(batchNumber, dryRun); console.info('Batch config created successfully.'); // Step 4 @@ -359,18 +362,20 @@ async function main() { // Step 6 console.info('Running funding pot service...'); - await runFundingPotService(batchNumber); + await runFundingPotService(batchNumber, dryRun); console.info('Funding pot service executed successfully!'); // Step 7 - console.info('Setting batch minting execution flag in round data...'); - await setBatchMintingExecutionFlag(batchNumber); - console.info('Batch minting execution flag set successfully.'); - - // Step 8 - console.info('Start Syncing reward data in donations...'); - await updateRewardsForDonations(batchNumber); - console.info('Rewards data synced successfully.'); + if (!dryRun) { + console.info('Setting batch minting execution flag in round data...'); + await setBatchMintingExecutionFlag(batchNumber); + console.info('Batch minting execution flag set successfully.'); + + // Step 8 + console.info('Start Syncing reward data in donations...'); + await updateRewardsForDonations(batchNumber); + console.info('Rewards data synced successfully.'); + } console.info('Done!'); process.exit(); } catch (error) { From 925296eca2cf21bae17c89e9d5e300a70bfcbf92 Mon Sep 17 00:00:00 2001 From: ali Date: Sat, 16 Nov 2024 16:05:38 +0700 Subject: [PATCH 03/33] put correct backend url for production --- src/scripts/runFundingPotService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/scripts/runFundingPotService.ts b/src/scripts/runFundingPotService.ts index d020b4b94..64296c62c 100644 --- a/src/scripts/runFundingPotService.ts +++ b/src/scripts/runFundingPotService.ts @@ -178,7 +178,11 @@ async function createEnvFile() { 'RPC_URL="https://rpc.ankr.com/base_sepolia"', 'RPC_URL="https://zkevm-rpc.com"', ) - .replace('CHAIN_ID=84532', 'CHAIN_ID=1101'); + .replace('CHAIN_ID=84532', 'CHAIN_ID=1101') + .replace( + 'BACKEND_URL="https://staging.qacc-be.generalmagic.io/graphql"', + `BACKEND_URL="${config.get('SERVER_URL')}/graphql"`, + ); await fs.writeFile(envFilePath, updatedEnvContent, 'utf-8'); } catch (error) { From 58b449992bacbf5628cd8e5665e6c922158b1f1f Mon Sep 17 00:00:00 2001 From: geleeroyale Date: Tue, 19 Nov 2024 17:04:49 +0100 Subject: [PATCH 04/33] Update main-pipeline.yml --- .github/workflows/main-pipeline.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main-pipeline.yml b/.github/workflows/main-pipeline.yml index 72b8cad1a..2eae7af12 100644 --- a/.github/workflows/main-pipeline.yml +++ b/.github/workflows/main-pipeline.yml @@ -179,8 +179,8 @@ jobs: cd QAcc-BE ## Update each backend service one by one ## First Deployment - docker compose down qacc-be-graph-ql1 - docker compose down qacc-be-job + docker compose rm -fs qacc-be-graph-ql1 + docker compose rm -fs qacc-be-job docker compose up --force-recreate -d qacc-be-graph-ql1 docker compose up --force-recreate -d qacc-be-job @@ -217,7 +217,7 @@ jobs: script: | cd QAcc-BE ## Second Deployment - docker compose down qacc-be-graph-ql2 + docker compose rm -fs qacc-be-graph-ql2 docker compose up --force-recreate -d qacc-be-graph-ql2 # Wait for qacc-be-graph-ql2 to be healthy (timeout after 5 minutes) @@ -248,7 +248,7 @@ jobs: script: | cd QAcc-BE ## Third Deployment - docker compose down qacc-be-graph-ql3 + docker compose rm -fs qacc-be-graph-ql3 docker compose up --force-recreate -d qacc-be-graph-ql3 # Wait for qacc-be-graph-ql3 to be healthy (timeout after 5 minutes) @@ -263,4 +263,4 @@ jobs: echo "qacc-be-graph-ql3 is not healthy, stopping deployment" exit 1 fi - echo "First deployment phase completed successfully" \ No newline at end of file + echo "First deployment phase completed successfully" From 6e98c9925a3e8d382a088e71285d30c84d173081 Mon Sep 17 00:00:00 2001 From: geleeroyale Date: Tue, 19 Nov 2024 17:06:29 +0100 Subject: [PATCH 05/33] Update staging-pipeline.yml --- .github/workflows/staging-pipeline.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/staging-pipeline.yml b/.github/workflows/staging-pipeline.yml index 26fbe244a..29108aed5 100644 --- a/.github/workflows/staging-pipeline.yml +++ b/.github/workflows/staging-pipeline.yml @@ -179,8 +179,8 @@ jobs: cd QAcc-BE ## Update each backend service one by one ## First Deployment - docker compose down qacc-be-graph-ql1 - docker compose down qacc-be-job + docker compose rm -fs qacc-be-graph-ql1 + docker compose rm -fs qacc-be-job docker compose up --force-recreate -d qacc-be-graph-ql1 docker compose up --force-recreate -d qacc-be-job @@ -217,7 +217,7 @@ jobs: script: | cd QAcc-BE ## Second Deployment - docker compose down qacc-be-graph-ql2 + docker compose rm -fs qacc-be-graph-ql2 docker compose up --force-recreate -d qacc-be-graph-ql2 # Wait for qacc-be-graph-ql2 to be healthy (timeout after 5 minutes) @@ -248,7 +248,7 @@ jobs: script: | cd QAcc-BE ## Third Deployment - docker compose down qacc-be-graph-ql3 + docker compose rm -fs qacc-be-graph-ql3 docker compose up --force-recreate -d qacc-be-graph-ql3 # Wait for qacc-be-graph-ql3 to be healthy (timeout after 5 minutes) @@ -263,4 +263,4 @@ jobs: echo "qacc-be-graph-ql3 is not healthy, stopping deployment" exit 1 fi - echo "First deployment phase completed successfully" \ No newline at end of file + echo "First deployment phase completed successfully" From 854a10411e74df543ad109efa77e6d34eb37ed58 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 25 Nov 2024 03:22:22 +0330 Subject: [PATCH 06/33] add variables for gitcoin score check --- config/example.env | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/example.env b/config/example.env index 90c43da09..f4560eee4 100644 --- a/config/example.env +++ b/config/example.env @@ -305,3 +305,8 @@ ANKR_SYNC_CRONJOB_EXPRESSION= # Reports database MONGO_DB_URI= MONGO_DB_REPORT_DB_NAME= + +# Gitcoin score +GITCOIN_MIN_SCORE= +MAX_AMOUNT_NO_KYC= +ACTIVATE_GITCOIN_SCORE_CHECK= From f9404d47210cf6faf802eb53684bb4bbf33a14cd Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 25 Nov 2024 03:24:15 +0330 Subject: [PATCH 07/33] add update user gitcoin score function --- src/services/userService.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/services/userService.ts b/src/services/userService.ts index 993a432e6..1dc06aee6 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -2,6 +2,7 @@ import { User } from '../entities/user'; import { Donation } from '../entities/donation'; import { logger } from '../utils/logger'; import { findAdminUserByEmail } from '../repositories/userRepository'; +import { getGitcoinAdapter } from '../adapters/adaptersFactory'; // eslint-disable-next-line @typescript-eslint/no-var-requires const bcrypt = require('bcrypt'); @@ -84,3 +85,14 @@ export const fetchAdminAndValidatePassword = async (params: { return; } }; + +export const updateUserGitcoinScore = async (user: User) => { + const passportScore = await getGitcoinAdapter().getWalletAddressScore( + user.walletAddress as string, + ); + if (passportScore && passportScore?.score) { + const score = Number(passportScore.score); + user.passportScore = score; + await user.save(); + } +}; From 86c8643e9254b54572ce28e7927a697eee615cc8 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 25 Nov 2024 03:24:53 +0330 Subject: [PATCH 08/33] add validate donation based on kyc and score function --- src/services/qAccService.ts | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/services/qAccService.ts b/src/services/qAccService.ts index 9ffa84fdd..862575888 100644 --- a/src/services/qAccService.ts +++ b/src/services/qAccService.ts @@ -2,11 +2,14 @@ import { FindOneOptions } from 'typeorm'; import { EarlyAccessRound } from '../entities/earlyAccessRound'; import { ProjectRoundRecord } from '../entities/projectRoundRecord'; import { ProjectUserRecord } from '../entities/projectUserRecord'; +import { User } from '../entities/user'; import { QfRound } from '../entities/qfRound'; import { findActiveEarlyAccessRound } from '../repositories/earlyAccessRoundRepository'; import { updateOrCreateProjectRoundRecord } from '../repositories/projectRoundRecordRepository'; import { updateOrCreateProjectUserRecord } from '../repositories/projectUserRecordRepository'; import { findActiveQfRound } from '../repositories/qfRoundRepository'; +import config from '../config'; +import { updateUserGitcoinScore } from './userService'; const getEaProjectRoundRecord = async ({ projectId, @@ -195,6 +198,43 @@ const getQAccDonationCap = async ({ } }; +const validDonationAmountBasedOnKYCAndScore = async ({ + projectId, + user, + amount, +}: { + projectId: number; + user: User; + amount: number; +}): Promise => { + if (user.privadoVerified) { + return true; + } + if (!user.passportScore) { + await updateUserGitcoinScore(user); + } + if ( + !user.passportScore || + user.passportScore < Number(config.get('GITCOIN_MIN_SCORE')) + ) { + throw new Error( + `passport score is less than ${config.get('GITCOIN_MIN_SCORE')}`, + ); + } + const userRecord = await getUserProjectRecord({ + projectId, + userId: user.id, + }); + const qfTotalDonationAmount = userRecord.qfTotalDonationAmount; + const remainedCap = + Number(config.get('MAX_AMOUNT_NO_KYC')) - qfTotalDonationAmount; + if (amount > remainedCap) { + throw new Error('amount is more than allowed cap with gitcoin score'); + } + return true; +}; + export default { getQAccDonationCap, + validDonationAmountBasedOnKYCAndScore, }; From 04409f7d2767c16368138a79c02def20661f818a Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 25 Nov 2024 03:25:32 +0330 Subject: [PATCH 09/33] call validate donation based on kyc and score function in validate donation in qacc service --- src/utils/qacc.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/utils/qacc.ts b/src/utils/qacc.ts index 81522caf7..fe5eb676a 100644 --- a/src/utils/qacc.ts +++ b/src/utils/qacc.ts @@ -9,12 +9,19 @@ import { findUserByWalletAddress, } from '../repositories/userRepository'; import qAccService from '../services/qAccService'; +import { findActiveQfRound } from '../repositories/qfRoundRepository'; +import config from '../config'; const isEarlyAccessRound = async () => { const earlyAccessRound = await findActiveEarlyAccessRound(); return !!earlyAccessRound; }; +const isQfRound = async () => { + const qfRound = await findActiveQfRound(); + return !!qfRound; +}; + const validateDonation = async (params: { projectId: number; userAddress: string; @@ -66,6 +73,15 @@ const validateDonation = async (params: { ) { throw new Error(i18n.__(translationErrorMessagesKeys.NOT_NFT_HOLDER)); } + } else if ( + Boolean(config.get('ACTIVATE_GITCOIN_SCORE_CHECK')) && + (await isQfRound()) + ) { + await qAccService.validDonationAmountBasedOnKYCAndScore({ + user, + projectId, + amount: params.amount, + }); } return cap >= params.amount; From ed00f0d52376bc2da4dd176d47dac9ea3a8ba5cc Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 25 Nov 2024 03:26:04 +0330 Subject: [PATCH 10/33] add validate donation query to donation resolver --- src/resolvers/donationResolver.ts | 67 +++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index c3e5ed05a..1069f835c 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -678,6 +678,73 @@ export class DonationResolver { }; } + @Query(_returns => Boolean) + async validateDonation( + @Arg('amount') amount: number, + @Arg('token') token: string, + @Arg('transactionNetworkId') transactionNetworkId: number, + @Arg('projectId') projectId: number, + @Ctx() ctx: ApolloContext, + ): Promise { + const logData = { + amount, + transactionNetworkId, + token, + projectId, + userId: ctx?.req?.user?.userId, + }; + logger.debug( + 'validateDonation() resolver has been called with this data', + logData, + ); + try { + const userId = ctx?.req?.user?.userId; + if (!userId) { + throw new Error(i18n.__(translationErrorMessagesKeys.UN_AUTHORIZED)); + } + // Fetch user data + const donorUser = await findUserById(userId); + if (!donorUser) { + throw new Error(i18n.__(translationErrorMessagesKeys.UN_AUTHORIZED)); + } + const chainType = detectAddressChainType(donorUser.walletAddress!); + const networkId = getAppropriateNetworkId({ + networkId: transactionNetworkId, + chainType, + }); + const project = await findProjectById(projectId); + + if (!project) + throw new Error( + i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND), + ); + if (project.status.id !== ProjStatus.active) { + throw new Error( + i18n.__( + translationErrorMessagesKeys.JUST_ACTIVE_PROJECTS_ACCEPT_DONATION, + ), + ); + } + const donateTime = new Date(); + + return await qacc.validateDonation({ + projectId, + networkId, + tokenSymbol: token, + userAddress: donorUser.walletAddress!, + amount, + donateTime, + }); + } catch (e) { + SentryLogger.captureException(e); + logger.error('validateDonation() error', { + error: e, + inputData: logData, + }); + throw e; + } + } + @Mutation(_returns => Number) async createDonation( @Arg('amount') amount: number, From a18e827753f4bef6a871d4904c135d5cffc55601 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 25 Nov 2024 03:26:29 +0330 Subject: [PATCH 11/33] add test cases for validate donation resolver --- src/resolvers/donationResolver.test.ts | 188 +++++++++++++++++++++++++ test/graphqlQueries.ts | 16 +++ 2 files changed, 204 insertions(+) diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 4eb65a784..5ab35323a 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -42,6 +42,7 @@ import { fetchNewDonorsCount, fetchNewDonorsDonationTotalUsd, fetchDonationMetricsQuery, + validateDonationQuery, } from '../../test/graphqlQueries'; import { NETWORK_IDS, QACC_NETWORK_ID } from '../provider'; import { User } from '../entities/user'; @@ -76,6 +77,7 @@ describe('donationsByProjectId() test cases', donationsByProjectIdTestCases); describe('donationByUserId() test cases', donationsByUserIdTestCases); describe('donationsByDonor() test cases', donationsByDonorTestCases); describe('createDonation() test cases', createDonationTestCases); +describe('validateDonation() test cases', validateDonationTestCases); // describe('updateDonationStatus() test cases', updateDonationStatusTestCases); describe('donationsToWallets() test cases', donationsToWalletsTestCases); describe('donationsFromWallets() test cases', donationsFromWalletsTestCases); @@ -2721,6 +2723,192 @@ function createDonationTestCases() { }); } +function validateDonationTestCases() { + let project; + let user; + + before(async () => { + // Set up a project and user before each test case + project = await saveProjectDirectlyToDb(createProjectData()); + user = await User.create({ + walletAddress: generateRandomEtheriumAddress(), + loginType: 'wallet', + firstName: 'Test User', + }).save(); + }); + + beforeEach(async () => { + sinon.stub(qAccService, 'getQAccDonationCap').resolves(10000); + }); + + afterEach(async () => { + sinon.restore(); + }); + + it('should return true if donation is valid', async () => { + // Mocking valid conditions for donation + const amount = 100; + const token = QACC_DONATION_TOKEN_SYMBOL; + const transactionNetworkId = QACC_NETWORK_ID; + const projectId = project.id; + + const accessToken = await generateTestAccessToken(user.id); + + const validateDonationResponse = await axios.post( + graphqlUrl, + { + query: validateDonationQuery, + variables: { + amount, + token, + transactionNetworkId, + projectId, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + assert.isTrue(validateDonationResponse.data.data.validateDonation); + }); + + it('should return false if donation amount exceeds project cap', async () => { + // Test case for exceeding donation cap + const amount = 1000000; // Large donation amount + const token = QACC_DONATION_TOKEN_SYMBOL; + const transactionNetworkId = QACC_NETWORK_ID; + const projectId = project.id; + + const accessToken = await generateTestAccessToken(user.id); + + const validateDonationResponse = await axios.post( + graphqlUrl, + { + query: validateDonationQuery, + variables: { + amount, + token, + transactionNetworkId, + projectId, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + assert.isFalse(validateDonationResponse.data.data.validateDonation); + }); + + it('should return false if user is unauthorized', async () => { + // Test case for unauthorized user + const amount = 100; + const token = QACC_DONATION_TOKEN_SYMBOL; + const transactionNetworkId = QACC_NETWORK_ID; + const projectId = project.id; + + const accessToken = 'InvalidAccessToken'; // Invalid token + + try { + await axios.post( + graphqlUrl, + { + query: validateDonationQuery, + variables: { + amount, + token, + transactionNetworkId, + projectId, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + } catch (e) { + assert.include(e.response.data.errors[0].message, 'Unauthorized'); + } + }); + + it('should throw error if donation is invalid due to network mismatch', async () => { + // Test case for invalid network ID + const amount = 100; + const token = QACC_DONATION_TOKEN_SYMBOL; + const transactionNetworkId = 999; // Invalid network ID + const projectId = project.id; + + const accessToken = await generateTestAccessToken(user.id); + + const validateDonationResponse = await axios.post( + graphqlUrl, + { + query: validateDonationQuery, + variables: { + amount, + token, + transactionNetworkId, + projectId, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + assert.isTrue( + (validateDonationResponse.data.errors[0].message as string).startsWith( + 'Invalid tokenAddress', + ), + ); + }); + + it('should throw error if project is not active', async () => { + // Test case for project not being active + const amount = 100; + const token = QACC_DONATION_TOKEN_SYMBOL; + const transactionNetworkId = QACC_NETWORK_ID; + const projectId = project.id; + + project.status.id = ProjStatus.deactive; // Setting project status to deactive + await project.save(); + + const accessToken = await generateTestAccessToken(user.id); + + const validateDonationResponse = await axios.post( + graphqlUrl, + { + query: validateDonationQuery, + variables: { + amount, + token, + transactionNetworkId, + projectId, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + assert.isTrue( + (validateDonationResponse.data.errors[0].message as string).startsWith( + 'Just active projects accept donation', + ), + ); + }); +} + function donationsFromWalletsTestCases() { it('should find donations with special source successfully', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 65f17efc0..858683512 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -28,6 +28,22 @@ export const createDonationMutation = ` } `; +export const validateDonationQuery = ` + query ( + $amount: Float! + $token: String! + $transactionNetworkId: Float! + $projectId: Float! + ) { + validateDonation( + amount: $amount + token: $token + transactionNetworkId: $transactionNetworkId + projectId: $projectId + ) + } +`; + export const scoreUserAddressMutation = ` query ( $address: String! From 3d2839d643bb2968998f85f70772911224d58cf0 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 25 Nov 2024 03:51:33 +0330 Subject: [PATCH 12/33] add score valid timestamp --- config/example.env | 2 ++ .../1732494017630-addScoreTimestampToUser.ts | 19 +++++++++++++++++++ src/entities/user.ts | 4 ++++ src/resolvers/userResolver.ts | 1 + src/services/qAccService.ts | 7 ++++++- src/services/userService.ts | 1 + 6 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 migration/1732494017630-addScoreTimestampToUser.ts diff --git a/config/example.env b/config/example.env index f4560eee4..ac19ddc56 100644 --- a/config/example.env +++ b/config/example.env @@ -308,5 +308,7 @@ MONGO_DB_REPORT_DB_NAME= # Gitcoin score GITCOIN_MIN_SCORE= +# 1 day +VALID_SCORE_TIMESTAMP=86400000 MAX_AMOUNT_NO_KYC= ACTIVATE_GITCOIN_SCORE_CHECK= diff --git a/migration/1732494017630-addScoreTimestampToUser.ts b/migration/1732494017630-addScoreTimestampToUser.ts new file mode 100644 index 000000000..278422c0e --- /dev/null +++ b/migration/1732494017630-addScoreTimestampToUser.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddScoreTimestampToUser1732494017630 + implements MigrationInterface +{ + name = 'AddScoreTimestampToUser1732494017630'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ADD "passportScoreUpdateTimestamp" integer`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" DROP COLUMN "passportScoreUpdateTimestamp"`, + ); + } +} diff --git a/src/entities/user.ts b/src/entities/user.ts index 3cd9c64c8..4270336d4 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -117,6 +117,10 @@ export class User extends BaseEntity { @Column({ type: 'real', nullable: true, default: null }) passportScore?: number; + @Field(_type => Number, { nullable: true }) + @Column({ nullable: true, default: null }) + passportScoreUpdateTimestamp?: number; + @Field(_type => Number, { nullable: true }) @Column({ nullable: true, default: null }) passportStamps?: number; diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 133e20e8c..b262921dd 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -126,6 +126,7 @@ export class UserResolver { if (passportScore && passportScore?.score) { const score = Number(passportScore.score); foundUser.passportScore = score; + foundUser.passportScoreUpdateTimestamp = Date.now(); } if (passportStamps) foundUser.passportStamps = passportStamps.items.length; diff --git a/src/services/qAccService.ts b/src/services/qAccService.ts index 862575888..8b5df7b5b 100644 --- a/src/services/qAccService.ts +++ b/src/services/qAccService.ts @@ -210,7 +210,12 @@ const validDonationAmountBasedOnKYCAndScore = async ({ if (user.privadoVerified) { return true; } - if (!user.passportScore) { + if ( + !user.passportScore || + !user.passportScoreUpdateTimestamp || + user.passportScoreUpdateTimestamp > + Date.now() - (Number(config.get('VALID_SCORE_TIMESTAMP')) || 86400000) // default value is 1 day + ) { await updateUserGitcoinScore(user); } if ( diff --git a/src/services/userService.ts b/src/services/userService.ts index 1dc06aee6..3fcc3fac5 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -93,6 +93,7 @@ export const updateUserGitcoinScore = async (user: User) => { if (passportScore && passportScore?.score) { const score = Number(passportScore.score); user.passportScore = score; + user.passportScoreUpdateTimestamp = Date.now(); await user.save(); } }; From 46996f6b20b3ec96da2132ded8ab690550daedda Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 25 Nov 2024 04:09:17 +0330 Subject: [PATCH 13/33] update timestamp to bigint in the DB --- ...mpToUser.ts => 1732495066795-addScoreTimestampToUser.ts} | 6 +++--- src/entities/user.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename migration/{1732494017630-addScoreTimestampToUser.ts => 1732495066795-addScoreTimestampToUser.ts} (68%) diff --git a/migration/1732494017630-addScoreTimestampToUser.ts b/migration/1732495066795-addScoreTimestampToUser.ts similarity index 68% rename from migration/1732494017630-addScoreTimestampToUser.ts rename to migration/1732495066795-addScoreTimestampToUser.ts index 278422c0e..864b442e8 100644 --- a/migration/1732494017630-addScoreTimestampToUser.ts +++ b/migration/1732495066795-addScoreTimestampToUser.ts @@ -1,13 +1,13 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddScoreTimestampToUser1732494017630 +export class AddScoreTimestampToUser1732495066795 implements MigrationInterface { - name = 'AddScoreTimestampToUser1732494017630'; + name = 'AddScoreTimestampToUser1732495066795'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `ALTER TABLE "user" ADD "passportScoreUpdateTimestamp" integer`, + `ALTER TABLE "user" ADD "passportScoreUpdateTimestamp" bigint`, ); } diff --git a/src/entities/user.ts b/src/entities/user.ts index 4270336d4..1369ea595 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -117,8 +117,8 @@ export class User extends BaseEntity { @Column({ type: 'real', nullable: true, default: null }) passportScore?: number; - @Field(_type => Number, { nullable: true }) - @Column({ nullable: true, default: null }) + @Field(_type => BigInt, { nullable: true }) + @Column('bigint', { nullable: true, default: null }) passportScoreUpdateTimestamp?: number; @Field(_type => Number, { nullable: true }) From df4f1d72f534a74a627f214e1beac9e315e0edf9 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 25 Nov 2024 04:17:40 +0330 Subject: [PATCH 14/33] change timestamp field type to string --- src/entities/user.ts | 2 +- src/resolvers/userResolver.ts | 2 +- src/services/qAccService.ts | 2 +- src/services/userService.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/entities/user.ts b/src/entities/user.ts index 1369ea595..2ee52433a 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -119,7 +119,7 @@ export class User extends BaseEntity { @Field(_type => BigInt, { nullable: true }) @Column('bigint', { nullable: true, default: null }) - passportScoreUpdateTimestamp?: number; + passportScoreUpdateTimestamp?: string; @Field(_type => Number, { nullable: true }) @Column({ nullable: true, default: null }) diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index b262921dd..5ea6b8ddb 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -126,7 +126,7 @@ export class UserResolver { if (passportScore && passportScore?.score) { const score = Number(passportScore.score); foundUser.passportScore = score; - foundUser.passportScoreUpdateTimestamp = Date.now(); + foundUser.passportScoreUpdateTimestamp = Date.now().toString(); } if (passportStamps) foundUser.passportStamps = passportStamps.items.length; diff --git a/src/services/qAccService.ts b/src/services/qAccService.ts index 8b5df7b5b..e129288fb 100644 --- a/src/services/qAccService.ts +++ b/src/services/qAccService.ts @@ -213,7 +213,7 @@ const validDonationAmountBasedOnKYCAndScore = async ({ if ( !user.passportScore || !user.passportScoreUpdateTimestamp || - user.passportScoreUpdateTimestamp > + +user.passportScoreUpdateTimestamp > Date.now() - (Number(config.get('VALID_SCORE_TIMESTAMP')) || 86400000) // default value is 1 day ) { await updateUserGitcoinScore(user); diff --git a/src/services/userService.ts b/src/services/userService.ts index 3fcc3fac5..6582311f8 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -93,7 +93,7 @@ export const updateUserGitcoinScore = async (user: User) => { if (passportScore && passportScore?.score) { const score = Number(passportScore.score); user.passportScore = score; - user.passportScoreUpdateTimestamp = Date.now(); + user.passportScoreUpdateTimestamp = Date.now().toString(); await user.save(); } }; From 8e5f2aa41e0eaed0bcc115d6af4d8e58b5a3fb35 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 25 Nov 2024 04:23:04 +0330 Subject: [PATCH 15/33] change timestamp field type to string --- ...mpToUser.ts => 1732495872789-addScoreTimestampToUser.ts} | 6 +++--- src/entities/user.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename migration/{1732495066795-addScoreTimestampToUser.ts => 1732495872789-addScoreTimestampToUser.ts} (67%) diff --git a/migration/1732495066795-addScoreTimestampToUser.ts b/migration/1732495872789-addScoreTimestampToUser.ts similarity index 67% rename from migration/1732495066795-addScoreTimestampToUser.ts rename to migration/1732495872789-addScoreTimestampToUser.ts index 864b442e8..4046ad976 100644 --- a/migration/1732495066795-addScoreTimestampToUser.ts +++ b/migration/1732495872789-addScoreTimestampToUser.ts @@ -1,13 +1,13 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddScoreTimestampToUser1732495066795 +export class AddScoreTimestampToUser1732495872789 implements MigrationInterface { - name = 'AddScoreTimestampToUser1732495066795'; + name = 'AddScoreTimestampToUser1732495872789'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `ALTER TABLE "user" ADD "passportScoreUpdateTimestamp" bigint`, + `ALTER TABLE "user" ADD "passportScoreUpdateTimestamp" character varying`, ); } diff --git a/src/entities/user.ts b/src/entities/user.ts index 2ee52433a..60a9183be 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -117,8 +117,8 @@ export class User extends BaseEntity { @Column({ type: 'real', nullable: true, default: null }) passportScore?: number; - @Field(_type => BigInt, { nullable: true }) - @Column('bigint', { nullable: true, default: null }) + @Field(_type => String, { nullable: true }) + @Column({ nullable: true }) passportScoreUpdateTimestamp?: string; @Field(_type => Number, { nullable: true }) From 3470966b4beb52d925f31954e562475322eb7a6a Mon Sep 17 00:00:00 2001 From: Ali Ebrahimi <65724329+ae2079@users.noreply.github.com> Date: Tue, 26 Nov 2024 02:45:46 +0330 Subject: [PATCH 16/33] Apply suggestions from code review change env variables names Co-authored-by: Amin Latifi --- config/example.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/example.env b/config/example.env index ac19ddc56..282b65dc4 100644 --- a/config/example.env +++ b/config/example.env @@ -307,8 +307,8 @@ MONGO_DB_URI= MONGO_DB_REPORT_DB_NAME= # Gitcoin score -GITCOIN_MIN_SCORE= +GITCOIN_PASSPORT_MIN_VALID_SCORE= # 1 day -VALID_SCORE_TIMESTAMP=86400000 -MAX_AMOUNT_NO_KYC= +GITCOINT_PASSPORT_EXPIRATION_PERIOD_MS=86400000 +MAX_CONTRIBUTION_WITH_GITCOING_PASSPORT_ONLY= ACTIVATE_GITCOIN_SCORE_CHECK= From 1c38188661ac20e481e191a35e937cdc09071daf Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 26 Nov 2024 04:50:49 +0330 Subject: [PATCH 17/33] apply review changes --- config/example.env | 4 +- ... 1732582914845-addScoreTimestampToUser.ts} | 6 +- src/constants/qacc.ts | 7 + src/entities/user.ts | 16 +- src/resolvers/donationResolver.test.ts | 188 ------------------ src/resolvers/donationResolver.ts | 67 ------- src/resolvers/userResolver.ts | 2 +- src/services/qAccService.ts | 16 +- src/services/userService.ts | 2 +- test/graphqlQueries.ts | 16 -- 10 files changed, 37 insertions(+), 287 deletions(-) rename migration/{1732495872789-addScoreTimestampToUser.ts => 1732582914845-addScoreTimestampToUser.ts} (66%) diff --git a/config/example.env b/config/example.env index 282b65dc4..d80772651 100644 --- a/config/example.env +++ b/config/example.env @@ -309,6 +309,6 @@ MONGO_DB_REPORT_DB_NAME= # Gitcoin score GITCOIN_PASSPORT_MIN_VALID_SCORE= # 1 day -GITCOINT_PASSPORT_EXPIRATION_PERIOD_MS=86400000 -MAX_CONTRIBUTION_WITH_GITCOING_PASSPORT_ONLY= +GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS=86400000 +MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY= ACTIVATE_GITCOIN_SCORE_CHECK= diff --git a/migration/1732495872789-addScoreTimestampToUser.ts b/migration/1732582914845-addScoreTimestampToUser.ts similarity index 66% rename from migration/1732495872789-addScoreTimestampToUser.ts rename to migration/1732582914845-addScoreTimestampToUser.ts index 4046ad976..f57b7d109 100644 --- a/migration/1732495872789-addScoreTimestampToUser.ts +++ b/migration/1732582914845-addScoreTimestampToUser.ts @@ -1,13 +1,13 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddScoreTimestampToUser1732495872789 +export class AddScoreTimestampToUser1732582914845 implements MigrationInterface { - name = 'AddScoreTimestampToUser1732495872789'; + name = 'AddScoreTimestampToUser1732582914845'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `ALTER TABLE "user" ADD "passportScoreUpdateTimestamp" character varying`, + `ALTER TABLE "user" ADD "passportScoreUpdateTimestamp" TIMESTAMP WITH TIME ZONE`, ); } diff --git a/src/constants/qacc.ts b/src/constants/qacc.ts index c1a9a3139..1d2bed444 100644 --- a/src/constants/qacc.ts +++ b/src/constants/qacc.ts @@ -15,3 +15,10 @@ export const QACC_DONATION_TOKEN_COINGECKO_ID = (config.get('QACC_DONATION_TOKEN_COINGECKO_ID') as string) || 'matic-network'; export const QACC_PRICE_FETCH_LEAD_TIME_IN_SECONDS = (+config.get('QACC_PRICE_FETCH_LEAD_TIME_IN_SECONDS') as number) || 300; // 5 minutes +export const GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS = + (+config.get('GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS') as number) || 86400000; // 1 day +export const GITCOIN_PASSPORT_MIN_VALID_SCORE = + (+config.get('GITCOIN_PASSPORT_MIN_VALID_SCORE') as number) || 50; +export const MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY = + (+config.get('MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY') as number) || + 1000; diff --git a/src/entities/user.ts b/src/entities/user.ts index 60a9183be..02a5f76b0 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -19,6 +19,7 @@ import { ProjectVerificationForm } from './projectVerificationForm'; import { ReferredEvent } from './referredEvent'; import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; import { PrivadoAdapter } from '../adapters/privado/privadoAdapter'; +import config from '../config'; export const publicSelectionFields = [ 'user.id', @@ -117,9 +118,9 @@ export class User extends BaseEntity { @Column({ type: 'real', nullable: true, default: null }) passportScore?: number; - @Field(_type => String, { nullable: true }) - @Column({ nullable: true }) - passportScoreUpdateTimestamp?: string; + @Field(_type => Date, { nullable: true }) + @Column({ type: 'timestamptz', nullable: true }) + passportScoreUpdateTimestamp?: Date; @Field(_type => Number, { nullable: true }) @Column({ nullable: true, default: null }) @@ -232,6 +233,15 @@ export class User extends BaseEntity { ); } + @Field(_type => Boolean, { nullable: true }) + get hasEnoughPassportScore(): boolean { + return !!( + this.passportScore && + this.passportScore >= + Number(config.get('GITCOIN_PASSPORT_MIN_VALID_SCORE')) + ); + } + @Field(_type => Int, { nullable: true }) async donationsCount() { return await Donation.createQueryBuilder('donation') diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 5ab35323a..4eb65a784 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -42,7 +42,6 @@ import { fetchNewDonorsCount, fetchNewDonorsDonationTotalUsd, fetchDonationMetricsQuery, - validateDonationQuery, } from '../../test/graphqlQueries'; import { NETWORK_IDS, QACC_NETWORK_ID } from '../provider'; import { User } from '../entities/user'; @@ -77,7 +76,6 @@ describe('donationsByProjectId() test cases', donationsByProjectIdTestCases); describe('donationByUserId() test cases', donationsByUserIdTestCases); describe('donationsByDonor() test cases', donationsByDonorTestCases); describe('createDonation() test cases', createDonationTestCases); -describe('validateDonation() test cases', validateDonationTestCases); // describe('updateDonationStatus() test cases', updateDonationStatusTestCases); describe('donationsToWallets() test cases', donationsToWalletsTestCases); describe('donationsFromWallets() test cases', donationsFromWalletsTestCases); @@ -2723,192 +2721,6 @@ function createDonationTestCases() { }); } -function validateDonationTestCases() { - let project; - let user; - - before(async () => { - // Set up a project and user before each test case - project = await saveProjectDirectlyToDb(createProjectData()); - user = await User.create({ - walletAddress: generateRandomEtheriumAddress(), - loginType: 'wallet', - firstName: 'Test User', - }).save(); - }); - - beforeEach(async () => { - sinon.stub(qAccService, 'getQAccDonationCap').resolves(10000); - }); - - afterEach(async () => { - sinon.restore(); - }); - - it('should return true if donation is valid', async () => { - // Mocking valid conditions for donation - const amount = 100; - const token = QACC_DONATION_TOKEN_SYMBOL; - const transactionNetworkId = QACC_NETWORK_ID; - const projectId = project.id; - - const accessToken = await generateTestAccessToken(user.id); - - const validateDonationResponse = await axios.post( - graphqlUrl, - { - query: validateDonationQuery, - variables: { - amount, - token, - transactionNetworkId, - projectId, - }, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - - assert.isTrue(validateDonationResponse.data.data.validateDonation); - }); - - it('should return false if donation amount exceeds project cap', async () => { - // Test case for exceeding donation cap - const amount = 1000000; // Large donation amount - const token = QACC_DONATION_TOKEN_SYMBOL; - const transactionNetworkId = QACC_NETWORK_ID; - const projectId = project.id; - - const accessToken = await generateTestAccessToken(user.id); - - const validateDonationResponse = await axios.post( - graphqlUrl, - { - query: validateDonationQuery, - variables: { - amount, - token, - transactionNetworkId, - projectId, - }, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - - assert.isFalse(validateDonationResponse.data.data.validateDonation); - }); - - it('should return false if user is unauthorized', async () => { - // Test case for unauthorized user - const amount = 100; - const token = QACC_DONATION_TOKEN_SYMBOL; - const transactionNetworkId = QACC_NETWORK_ID; - const projectId = project.id; - - const accessToken = 'InvalidAccessToken'; // Invalid token - - try { - await axios.post( - graphqlUrl, - { - query: validateDonationQuery, - variables: { - amount, - token, - transactionNetworkId, - projectId, - }, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - } catch (e) { - assert.include(e.response.data.errors[0].message, 'Unauthorized'); - } - }); - - it('should throw error if donation is invalid due to network mismatch', async () => { - // Test case for invalid network ID - const amount = 100; - const token = QACC_DONATION_TOKEN_SYMBOL; - const transactionNetworkId = 999; // Invalid network ID - const projectId = project.id; - - const accessToken = await generateTestAccessToken(user.id); - - const validateDonationResponse = await axios.post( - graphqlUrl, - { - query: validateDonationQuery, - variables: { - amount, - token, - transactionNetworkId, - projectId, - }, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - - assert.isTrue( - (validateDonationResponse.data.errors[0].message as string).startsWith( - 'Invalid tokenAddress', - ), - ); - }); - - it('should throw error if project is not active', async () => { - // Test case for project not being active - const amount = 100; - const token = QACC_DONATION_TOKEN_SYMBOL; - const transactionNetworkId = QACC_NETWORK_ID; - const projectId = project.id; - - project.status.id = ProjStatus.deactive; // Setting project status to deactive - await project.save(); - - const accessToken = await generateTestAccessToken(user.id); - - const validateDonationResponse = await axios.post( - graphqlUrl, - { - query: validateDonationQuery, - variables: { - amount, - token, - transactionNetworkId, - projectId, - }, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - - assert.isTrue( - (validateDonationResponse.data.errors[0].message as string).startsWith( - 'Just active projects accept donation', - ), - ); - }); -} - function donationsFromWalletsTestCases() { it('should find donations with special source successfully', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 1069f835c..c3e5ed05a 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -678,73 +678,6 @@ export class DonationResolver { }; } - @Query(_returns => Boolean) - async validateDonation( - @Arg('amount') amount: number, - @Arg('token') token: string, - @Arg('transactionNetworkId') transactionNetworkId: number, - @Arg('projectId') projectId: number, - @Ctx() ctx: ApolloContext, - ): Promise { - const logData = { - amount, - transactionNetworkId, - token, - projectId, - userId: ctx?.req?.user?.userId, - }; - logger.debug( - 'validateDonation() resolver has been called with this data', - logData, - ); - try { - const userId = ctx?.req?.user?.userId; - if (!userId) { - throw new Error(i18n.__(translationErrorMessagesKeys.UN_AUTHORIZED)); - } - // Fetch user data - const donorUser = await findUserById(userId); - if (!donorUser) { - throw new Error(i18n.__(translationErrorMessagesKeys.UN_AUTHORIZED)); - } - const chainType = detectAddressChainType(donorUser.walletAddress!); - const networkId = getAppropriateNetworkId({ - networkId: transactionNetworkId, - chainType, - }); - const project = await findProjectById(projectId); - - if (!project) - throw new Error( - i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND), - ); - if (project.status.id !== ProjStatus.active) { - throw new Error( - i18n.__( - translationErrorMessagesKeys.JUST_ACTIVE_PROJECTS_ACCEPT_DONATION, - ), - ); - } - const donateTime = new Date(); - - return await qacc.validateDonation({ - projectId, - networkId, - tokenSymbol: token, - userAddress: donorUser.walletAddress!, - amount, - donateTime, - }); - } catch (e) { - SentryLogger.captureException(e); - logger.error('validateDonation() error', { - error: e, - inputData: logData, - }); - throw e; - } - } - @Mutation(_returns => Number) async createDonation( @Arg('amount') amount: number, diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 5ea6b8ddb..57876889d 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -126,7 +126,7 @@ export class UserResolver { if (passportScore && passportScore?.score) { const score = Number(passportScore.score); foundUser.passportScore = score; - foundUser.passportScoreUpdateTimestamp = Date.now().toString(); + foundUser.passportScoreUpdateTimestamp = new Date(); } if (passportStamps) foundUser.passportStamps = passportStamps.items.length; diff --git a/src/services/qAccService.ts b/src/services/qAccService.ts index e129288fb..9f209a386 100644 --- a/src/services/qAccService.ts +++ b/src/services/qAccService.ts @@ -8,8 +8,12 @@ import { findActiveEarlyAccessRound } from '../repositories/earlyAccessRoundRepo import { updateOrCreateProjectRoundRecord } from '../repositories/projectRoundRecordRepository'; import { updateOrCreateProjectUserRecord } from '../repositories/projectUserRecordRepository'; import { findActiveQfRound } from '../repositories/qfRoundRepository'; -import config from '../config'; import { updateUserGitcoinScore } from './userService'; +import { + GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS, + GITCOIN_PASSPORT_MIN_VALID_SCORE, + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY, +} from '../constants/qacc'; const getEaProjectRoundRecord = async ({ projectId, @@ -213,17 +217,17 @@ const validDonationAmountBasedOnKYCAndScore = async ({ if ( !user.passportScore || !user.passportScoreUpdateTimestamp || - +user.passportScoreUpdateTimestamp > - Date.now() - (Number(config.get('VALID_SCORE_TIMESTAMP')) || 86400000) // default value is 1 day + user.passportScoreUpdateTimestamp.getTime() > + Date.now() - GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS ) { await updateUserGitcoinScore(user); } if ( !user.passportScore || - user.passportScore < Number(config.get('GITCOIN_MIN_SCORE')) + user.passportScore < GITCOIN_PASSPORT_MIN_VALID_SCORE ) { throw new Error( - `passport score is less than ${config.get('GITCOIN_MIN_SCORE')}`, + `passport score is less than ${GITCOIN_PASSPORT_MIN_VALID_SCORE}`, ); } const userRecord = await getUserProjectRecord({ @@ -232,7 +236,7 @@ const validDonationAmountBasedOnKYCAndScore = async ({ }); const qfTotalDonationAmount = userRecord.qfTotalDonationAmount; const remainedCap = - Number(config.get('MAX_AMOUNT_NO_KYC')) - qfTotalDonationAmount; + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY - qfTotalDonationAmount; if (amount > remainedCap) { throw new Error('amount is more than allowed cap with gitcoin score'); } diff --git a/src/services/userService.ts b/src/services/userService.ts index 6582311f8..347f4278f 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -93,7 +93,7 @@ export const updateUserGitcoinScore = async (user: User) => { if (passportScore && passportScore?.score) { const score = Number(passportScore.score); user.passportScore = score; - user.passportScoreUpdateTimestamp = Date.now().toString(); + user.passportScoreUpdateTimestamp = new Date(); await user.save(); } }; diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 858683512..65f17efc0 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -28,22 +28,6 @@ export const createDonationMutation = ` } `; -export const validateDonationQuery = ` - query ( - $amount: Float! - $token: String! - $transactionNetworkId: Float! - $projectId: Float! - ) { - validateDonation( - amount: $amount - token: $token - transactionNetworkId: $transactionNetworkId - projectId: $projectId - ) - } -`; - export const scoreUserAddressMutation = ` query ( $address: String! From 9511bcca2f7635ea36a1a5e3abfb8d9edfa91f5a Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 26 Nov 2024 05:03:01 +0330 Subject: [PATCH 18/33] use analysis score instead of gitcoin score --- .../1732584356154-addAnalysisScoreToUser.ts | 13 +++++++++++++ src/entities/user.ts | 13 ++++++++----- src/resolvers/userResolver.ts | 4 ++++ src/services/qAccService.ts | 13 +++++-------- src/services/userService.ts | 18 ++++++++++-------- 5 files changed, 40 insertions(+), 21 deletions(-) create mode 100644 migration/1732584356154-addAnalysisScoreToUser.ts diff --git a/migration/1732584356154-addAnalysisScoreToUser.ts b/migration/1732584356154-addAnalysisScoreToUser.ts new file mode 100644 index 000000000..9543a1439 --- /dev/null +++ b/migration/1732584356154-addAnalysisScoreToUser.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddAnalysisScoreToUser1732584356154 implements MigrationInterface { + name = 'AddAnalysisScoreToUser1732584356154'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" ADD "analysisScore" real`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "analysisScore"`); + } +} diff --git a/src/entities/user.ts b/src/entities/user.ts index 02a5f76b0..2db2edab1 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -19,7 +19,7 @@ import { ProjectVerificationForm } from './projectVerificationForm'; import { ReferredEvent } from './referredEvent'; import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; import { PrivadoAdapter } from '../adapters/privado/privadoAdapter'; -import config from '../config'; +import { GITCOIN_PASSPORT_MIN_VALID_SCORE } from '../constants/qacc'; export const publicSelectionFields = [ 'user.id', @@ -118,6 +118,10 @@ export class User extends BaseEntity { @Column({ type: 'real', nullable: true, default: null }) passportScore?: number; + @Field(_type => Float, { nullable: true }) + @Column({ type: 'real', nullable: true, default: null }) + analysisScore?: number; + @Field(_type => Date, { nullable: true }) @Column({ type: 'timestamptz', nullable: true }) passportScoreUpdateTimestamp?: Date; @@ -234,11 +238,10 @@ export class User extends BaseEntity { } @Field(_type => Boolean, { nullable: true }) - get hasEnoughPassportScore(): boolean { + get hasEnoughAnalysisScore(): boolean { return !!( - this.passportScore && - this.passportScore >= - Number(config.get('GITCOIN_PASSPORT_MIN_VALID_SCORE')) + this.analysisScore && + this.analysisScore >= GITCOIN_PASSPORT_MIN_VALID_SCORE ); } diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 57876889d..c99807951 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -123,6 +123,9 @@ export class UserResolver { const passportStamps = await getGitcoinAdapter().getPassportStamps(address); + const analysisScore = + await getGitcoinAdapter().getUserAnalysisScore(address); + if (passportScore && passportScore?.score) { const score = Number(passportScore.score); foundUser.passportScore = score; @@ -137,6 +140,7 @@ export class UserResolver { if (activeQFMBDScore) { foundUser.activeQFMBDScore = activeQFMBDScore; } + foundUser.analysisScore = analysisScore; await foundUser.save(); } catch (e) { logger.error(`refreshUserScores Error with address ${address}: `, e); diff --git a/src/services/qAccService.ts b/src/services/qAccService.ts index 9f209a386..67244e5b6 100644 --- a/src/services/qAccService.ts +++ b/src/services/qAccService.ts @@ -8,7 +8,7 @@ import { findActiveEarlyAccessRound } from '../repositories/earlyAccessRoundRepo import { updateOrCreateProjectRoundRecord } from '../repositories/projectRoundRecordRepository'; import { updateOrCreateProjectUserRecord } from '../repositories/projectUserRecordRepository'; import { findActiveQfRound } from '../repositories/qfRoundRepository'; -import { updateUserGitcoinScore } from './userService'; +import { updateUserGitcoinAnalysisScore } from './userService'; import { GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS, GITCOIN_PASSPORT_MIN_VALID_SCORE, @@ -215,19 +215,16 @@ const validDonationAmountBasedOnKYCAndScore = async ({ return true; } if ( - !user.passportScore || + !user.analysisScore || !user.passportScoreUpdateTimestamp || user.passportScoreUpdateTimestamp.getTime() > Date.now() - GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS ) { - await updateUserGitcoinScore(user); + await updateUserGitcoinAnalysisScore(user); } - if ( - !user.passportScore || - user.passportScore < GITCOIN_PASSPORT_MIN_VALID_SCORE - ) { + if (!user.hasEnoughAnalysisScore) { throw new Error( - `passport score is less than ${GITCOIN_PASSPORT_MIN_VALID_SCORE}`, + `analysis score is less than ${GITCOIN_PASSPORT_MIN_VALID_SCORE}`, ); } const userRecord = await getUserProjectRecord({ diff --git a/src/services/userService.ts b/src/services/userService.ts index 347f4278f..d331ac2a6 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -86,14 +86,16 @@ export const fetchAdminAndValidatePassword = async (params: { } }; -export const updateUserGitcoinScore = async (user: User) => { - const passportScore = await getGitcoinAdapter().getWalletAddressScore( +export const updateUserGitcoinAnalysisScore = async (user: User) => { + // const passportScore = await getGitcoinAdapter().getWalletAddressScore( + // user.walletAddress as string, + // ); + // if (passportScore && passportScore?.score) { + // user.passportScore = Number(passportScore.score); + // user.passportScoreUpdateTimestamp = new Date(); + // } + user.analysisScore = await getGitcoinAdapter().getUserAnalysisScore( user.walletAddress as string, ); - if (passportScore && passportScore?.score) { - const score = Number(passportScore.score); - user.passportScore = score; - user.passportScoreUpdateTimestamp = new Date(); - await user.save(); - } + await user.save(); }; From b6e6cd7239a80f7cd81b184410a5957701526f01 Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 26 Nov 2024 06:05:16 +0330 Subject: [PATCH 19/33] add new endpoint to get caps --- src/resolvers/qAccResolver.ts | 64 +++++++++++++++++++++++++++++++++++ src/services/qAccService.ts | 31 ++++++++++++----- src/services/userService.ts | 13 ++++++- test/graphqlQueries.ts | 14 ++++++++ 4 files changed, 112 insertions(+), 10 deletions(-) diff --git a/src/resolvers/qAccResolver.ts b/src/resolvers/qAccResolver.ts index c8eeb98f6..930aef72d 100644 --- a/src/resolvers/qAccResolver.ts +++ b/src/resolvers/qAccResolver.ts @@ -12,6 +12,7 @@ import { getProjectUserRecordAmount } from '../repositories/projectUserRecordRep import qAccService from '../services/qAccService'; import { ApolloContext } from '../types/ApolloContext'; import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; +import { getUserById } from '../services/userService'; @ObjectType() class ProjectUserRecordAmounts { @@ -24,6 +25,31 @@ class ProjectUserRecordAmounts { @Field(_type => Float) qfTotalDonationAmount: number; } + +@ObjectType() +class GitcoinPassportResponse { + @Field(_type => Float) + unusedCapped: number; +} + +@ObjectType() +class ZkIdResponse { + @Field(_type => Float) + unusedCapped: number; +} + +@ObjectType() +class QAccResponse { + @Field(_type => Float) + qAccCap: number; + + @Field(_type => GitcoinPassportResponse, { nullable: true }) + gitcoinPassport?: GitcoinPassportResponse; + + @Field(_type => ZkIdResponse, { nullable: true }) + zkId?: ZkIdResponse; +} + @Resolver() export class QAccResolver { @Query(_returns => ProjectUserRecordAmounts) @@ -54,4 +80,42 @@ export class QAccResolver { userId: user.userId, }); } + + @Query(_returns => QAccResponse) + async userCaps( + @Arg('projectId', _type => Int, { nullable: false }) projectId: number, + @Ctx() { req: { user } }: ApolloContext, + ): Promise { + if (!user) + throw new Error( + i18n.__(translationErrorMessagesKeys.AUTHENTICATION_REQUIRED), + ); + + const dbUser = await getUserById(user.userId); + + const qAccCap = await qAccService.getQAccDonationCap({ + projectId, + userId: user.userId, + }); + + const response: QAccResponse = { + qAccCap, + }; + + if (dbUser.privadoVerified) { + response.zkId = { + unusedCapped: qAccCap, + }; + } else if (dbUser.hasEnoughAnalysisScore) { + const cap = await qAccService.getUserRemainedCapBasedOnGitcoinScore({ + projectId, + user: dbUser, + }); + response.gitcoinPassport = { + unusedCapped: cap, + }; + } + + return response; + } } diff --git a/src/services/qAccService.ts b/src/services/qAccService.ts index 67244e5b6..66591d88f 100644 --- a/src/services/qAccService.ts +++ b/src/services/qAccService.ts @@ -202,18 +202,13 @@ const getQAccDonationCap = async ({ } }; -const validDonationAmountBasedOnKYCAndScore = async ({ +const getUserRemainedCapBasedOnGitcoinScore = async ({ projectId, user, - amount, }: { projectId: number; user: User; - amount: number; -}): Promise => { - if (user.privadoVerified) { - return true; - } +}): Promise => { if ( !user.analysisScore || !user.passportScoreUpdateTimestamp || @@ -232,8 +227,25 @@ const validDonationAmountBasedOnKYCAndScore = async ({ userId: user.id, }); const qfTotalDonationAmount = userRecord.qfTotalDonationAmount; - const remainedCap = - MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY - qfTotalDonationAmount; + return MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY - qfTotalDonationAmount; +}; + +const validDonationAmountBasedOnKYCAndScore = async ({ + projectId, + user, + amount, +}: { + projectId: number; + user: User; + amount: number; +}): Promise => { + if (user.privadoVerified) { + return true; + } + const remainedCap = await getUserRemainedCapBasedOnGitcoinScore({ + projectId, + user, + }); if (amount > remainedCap) { throw new Error('amount is more than allowed cap with gitcoin score'); } @@ -243,4 +255,5 @@ const validDonationAmountBasedOnKYCAndScore = async ({ export default { getQAccDonationCap, validDonationAmountBasedOnKYCAndScore, + getUserRemainedCapBasedOnGitcoinScore, }; diff --git a/src/services/userService.ts b/src/services/userService.ts index d331ac2a6..398152881 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -1,7 +1,10 @@ import { User } from '../entities/user'; import { Donation } from '../entities/donation'; import { logger } from '../utils/logger'; -import { findAdminUserByEmail } from '../repositories/userRepository'; +import { + findAdminUserByEmail, + findUserById, +} from '../repositories/userRepository'; import { getGitcoinAdapter } from '../adapters/adaptersFactory'; // eslint-disable-next-line @typescript-eslint/no-var-requires const bcrypt = require('bcrypt'); @@ -99,3 +102,11 @@ export const updateUserGitcoinAnalysisScore = async (user: User) => { ); await user.save(); }; + +export const getUserById = async (userId: number) => { + const foundedUser = await findUserById(userId); + if (foundedUser) { + return foundedUser; + } + throw new Error(`user not found with id ${userId}`); +}; diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 65f17efc0..6c4f78f5c 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -2159,3 +2159,17 @@ export const projectUserDonationCap = ` projectUserDonationCap(projectId: $projectId) } `; + +export const userCaps = ` + query UserCaps($projectId: Int!) { + userCaps(projectId: $projectId) { + qAccCap + gitcoinPassport { + unusedCapped + } + zkId { + unusedCapped + } + } + } +`; From 80818e48c8e72c81343e0f407de963a657d543b8 Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 26 Nov 2024 16:32:03 +0330 Subject: [PATCH 20/33] Resolve review changes --- config/example.env | 2 +- src/constants/gitcoin.ts | 9 +++++++++ src/constants/qacc.ts | 7 ------- src/entities/user.ts | 6 +++--- src/resolvers/qAccResolver.ts | 24 +++++++++--------------- src/services/qAccService.ts | 10 +++++----- src/services/userService.ts | 2 +- 7 files changed, 28 insertions(+), 32 deletions(-) create mode 100644 src/constants/gitcoin.ts diff --git a/config/example.env b/config/example.env index d80772651..8ca1f1f40 100644 --- a/config/example.env +++ b/config/example.env @@ -307,7 +307,7 @@ MONGO_DB_URI= MONGO_DB_REPORT_DB_NAME= # Gitcoin score -GITCOIN_PASSPORT_MIN_VALID_SCORE= +GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE= # 1 day GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS=86400000 MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY= diff --git a/src/constants/gitcoin.ts b/src/constants/gitcoin.ts new file mode 100644 index 000000000..f4a4ef7e5 --- /dev/null +++ b/src/constants/gitcoin.ts @@ -0,0 +1,9 @@ +import config from '../config'; + +export const GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS = + (+config.get('GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS') as number) || 86400000; // 1 day +export const GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE = + (+config.get('GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE') as number) || 50; +export const MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY = + (+config.get('MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY') as number) || + 1000; diff --git a/src/constants/qacc.ts b/src/constants/qacc.ts index 1d2bed444..c1a9a3139 100644 --- a/src/constants/qacc.ts +++ b/src/constants/qacc.ts @@ -15,10 +15,3 @@ export const QACC_DONATION_TOKEN_COINGECKO_ID = (config.get('QACC_DONATION_TOKEN_COINGECKO_ID') as string) || 'matic-network'; export const QACC_PRICE_FETCH_LEAD_TIME_IN_SECONDS = (+config.get('QACC_PRICE_FETCH_LEAD_TIME_IN_SECONDS') as number) || 300; // 5 minutes -export const GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS = - (+config.get('GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS') as number) || 86400000; // 1 day -export const GITCOIN_PASSPORT_MIN_VALID_SCORE = - (+config.get('GITCOIN_PASSPORT_MIN_VALID_SCORE') as number) || 50; -export const MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY = - (+config.get('MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY') as number) || - 1000; diff --git a/src/entities/user.ts b/src/entities/user.ts index 2db2edab1..a2cb5f073 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -19,7 +19,7 @@ import { ProjectVerificationForm } from './projectVerificationForm'; import { ReferredEvent } from './referredEvent'; import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; import { PrivadoAdapter } from '../adapters/privado/privadoAdapter'; -import { GITCOIN_PASSPORT_MIN_VALID_SCORE } from '../constants/qacc'; +import { GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE } from '../constants/gitcoin'; export const publicSelectionFields = [ 'user.id', @@ -238,10 +238,10 @@ export class User extends BaseEntity { } @Field(_type => Boolean, { nullable: true }) - get hasEnoughAnalysisScore(): boolean { + get hasEnoughGitcoinAnalysisScore(): boolean { return !!( this.analysisScore && - this.analysisScore >= GITCOIN_PASSPORT_MIN_VALID_SCORE + this.analysisScore >= GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE ); } diff --git a/src/resolvers/qAccResolver.ts b/src/resolvers/qAccResolver.ts index 930aef72d..c961d6751 100644 --- a/src/resolvers/qAccResolver.ts +++ b/src/resolvers/qAccResolver.ts @@ -27,15 +27,9 @@ class ProjectUserRecordAmounts { } @ObjectType() -class GitcoinPassportResponse { +class UnusedCapResponse { @Field(_type => Float) - unusedCapped: number; -} - -@ObjectType() -class ZkIdResponse { - @Field(_type => Float) - unusedCapped: number; + unusedCap: number; } @ObjectType() @@ -43,11 +37,11 @@ class QAccResponse { @Field(_type => Float) qAccCap: number; - @Field(_type => GitcoinPassportResponse, { nullable: true }) - gitcoinPassport?: GitcoinPassportResponse; + @Field(_type => UnusedCapResponse, { nullable: true }) + gitcoinPassport?: UnusedCapResponse; - @Field(_type => ZkIdResponse, { nullable: true }) - zkId?: ZkIdResponse; + @Field(_type => UnusedCapResponse, { nullable: true }) + zkId?: UnusedCapResponse; } @Resolver() @@ -104,15 +98,15 @@ export class QAccResolver { if (dbUser.privadoVerified) { response.zkId = { - unusedCapped: qAccCap, + unusedCap: qAccCap, }; - } else if (dbUser.hasEnoughAnalysisScore) { + } else if (dbUser.hasEnoughGitcoinAnalysisScore) { const cap = await qAccService.getUserRemainedCapBasedOnGitcoinScore({ projectId, user: dbUser, }); response.gitcoinPassport = { - unusedCapped: cap, + unusedCap: cap, }; } diff --git a/src/services/qAccService.ts b/src/services/qAccService.ts index 66591d88f..65402d4a9 100644 --- a/src/services/qAccService.ts +++ b/src/services/qAccService.ts @@ -11,9 +11,9 @@ import { findActiveQfRound } from '../repositories/qfRoundRepository'; import { updateUserGitcoinAnalysisScore } from './userService'; import { GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS, - GITCOIN_PASSPORT_MIN_VALID_SCORE, + GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE, MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY, -} from '../constants/qacc'; +} from '../constants/gitcoin'; const getEaProjectRoundRecord = async ({ projectId, @@ -212,14 +212,14 @@ const getUserRemainedCapBasedOnGitcoinScore = async ({ if ( !user.analysisScore || !user.passportScoreUpdateTimestamp || - user.passportScoreUpdateTimestamp.getTime() > + user.passportScoreUpdateTimestamp.getTime() < Date.now() - GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS ) { await updateUserGitcoinAnalysisScore(user); } - if (!user.hasEnoughAnalysisScore) { + if (!user.hasEnoughGitcoinAnalysisScore) { throw new Error( - `analysis score is less than ${GITCOIN_PASSPORT_MIN_VALID_SCORE}`, + `analysis score is less than ${GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE}`, ); } const userRecord = await getUserProjectRecord({ diff --git a/src/services/userService.ts b/src/services/userService.ts index 398152881..b3ab2586a 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -95,11 +95,11 @@ export const updateUserGitcoinAnalysisScore = async (user: User) => { // ); // if (passportScore && passportScore?.score) { // user.passportScore = Number(passportScore.score); - // user.passportScoreUpdateTimestamp = new Date(); // } user.analysisScore = await getGitcoinAdapter().getUserAnalysisScore( user.walletAddress as string, ); + user.passportScoreUpdateTimestamp = new Date(); await user.save(); }; From 6632272ef922f7af42dfcc6589c09d322a1d408d Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 26 Nov 2024 16:50:24 +0330 Subject: [PATCH 21/33] Add tests for userCaps resolver --- src/resolvers/qAccResolver.test.ts | 163 +++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/src/resolvers/qAccResolver.test.ts b/src/resolvers/qAccResolver.test.ts index 9f5b02956..8cf118097 100644 --- a/src/resolvers/qAccResolver.test.ts +++ b/src/resolvers/qAccResolver.test.ts @@ -24,9 +24,11 @@ import { import { projectUserDonationCap, projectUserTotalDonationAmounts, + userCaps, } from '../../test/graphqlQueries'; import { ProjectRoundRecord } from '../entities/projectRoundRecord'; import { EarlyAccessRound } from '../entities/earlyAccessRound'; +import { GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE } from '../constants/gitcoin'; describe( 'projectUserTotalDonationAmount() test cases', @@ -38,6 +40,8 @@ describe( projectUserDonationCapTestCases, ); +describe('userCaps() test cases', userCapsTestCases); + function projectUserTotalDonationAmountTestCases() { it('should return total donation amount of a user for a project', async () => { it('should return total donation amount of a user for a project', async () => { @@ -236,3 +240,162 @@ function projectUserDonationCapTestCases() { ); }); } + +function userCapsTestCases() { + let project; + let user; + let accessToken; + let qfRound1: QfRound; + beforeEach(async () => { + project = await saveProjectDirectlyToDb(createProjectData()); + + user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + accessToken = await generateTestAccessToken(user.id); + + qfRound1 = await QfRound.create({ + roundNumber: 1, + isActive: true, + name: new Date().toString() + ' - 1', + allocatedFund: 100, + minimumPassportScore: 12, + slug: new Date().getTime().toString() + ' - 1', + beginDate: new Date('2001-01-14'), + endDate: new Date('2001-01-16'), + roundUSDCapPerProject: 10000, + roundUSDCapPerUserPerProject: 2500, + tokenPrice: 0.5, + }).save(); + sinon.useFakeTimers({ + now: qfRound1.beginDate.getTime(), + }); + }); + afterEach(async () => { + // Clean up the database after each test + await ProjectRoundRecord.delete({}); + await Donation.delete({ projectId: project.id }); + await QfRound.delete(qfRound1.id); + + sinon.restore(); + }); + it('should return correct caps for a user with GitcoinPassport', async () => { + // Save donations + const donationAmount = 100; + await saveDonationDirectlyToDb( + { + ...createDonationData(), + amount: donationAmount, + status: DONATION_STATUS.VERIFIED, + qfRoundId: qfRound1.id, + }, + user.id, + project.id, + ); + + // Simulate valid GitcoinPassport score + sinon.stub(user, 'analysisScore').value(80); + sinon.stub(user, 'passportScoreUpdateTimestamp').value(new Date()); + sinon.stub(user, 'hasEnoughAnalysisScore').value(true); + + // Act: Call the resolver through a GraphQL query + const response: ExecutionResult<{ + userCaps: { + qAccCap: number; + gitcoinPassport?: { + unusedCap: number; + }; + zkId?: { + unusedCap: number; + }; + }; + }> = await axios.post( + graphqlUrl, + { + query: userCaps, + variables: { projectId: project.id }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert: Verify the response matches expected values + assert.equal(response.data?.userCaps?.qAccCap, 4900); // Adjust based on logic + assert.equal(response.data?.userCaps?.gitcoinPassport?.unusedCap, 1200); + assert.isUndefined(response.data?.userCaps?.zkId); + }); + + it('should return correct caps for a user with ZkId', async () => { + // Save donations + const donationAmount = 500; + await saveDonationDirectlyToDb( + { + ...createDonationData(), + amount: donationAmount, + status: DONATION_STATUS.VERIFIED, + }, + user.id, + project.id, + ); + + sinon.stub(user, 'privadoVerified').value(true); + + // Act: Call the resolver through a GraphQL query + const response: ExecutionResult<{ + userCaps: { + qAccCap: number; + gitcoinPassport?: { + unusedCap: number; + }; + zkId?: { + unusedCap: number; + }; + }; + }> = await axios.post( + graphqlUrl, + { + query: userCaps, + variables: { projectId: project.id }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert: Verify the response matches expected values + assert.equal(response.data?.userCaps?.qAccCap, 4500); // Adjust based on logic + assert.equal(response.data?.userCaps?.zkId?.unusedCap, 900); + assert.isUndefined(response.data?.userCaps?.gitcoinPassport); + }); + + it('should throw an error if the user does not meet the minimum analysis score', async () => { + // Simulate invalid GitcoinPassport score + sinon.stub(user, 'analysisScore').value(40); // Below threshold + sinon.stub(user, 'hasEnoughAnalysisScore').value(false); + + // Act: Call the resolver through a GraphQL query and expect an error + try { + await axios.post( + graphqlUrl, + { + query: userCaps, + variables: { projectId: project.id }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + } catch (error: any) { + // Assert: Verify the error message + assert.equal( + error.response.data.errors[0].message, + `analysis score is less than ${GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE}`, + ); + } + }); +} From 31ad1c372bed52636339d72b32f9da6b207aa74a Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 26 Nov 2024 16:54:36 +0330 Subject: [PATCH 22/33] fix input of test --- src/resolvers/qAccResolver.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/resolvers/qAccResolver.test.ts b/src/resolvers/qAccResolver.test.ts index 8cf118097..195d8e469 100644 --- a/src/resolvers/qAccResolver.test.ts +++ b/src/resolvers/qAccResolver.test.ts @@ -334,6 +334,7 @@ function userCapsTestCases() { ...createDonationData(), amount: donationAmount, status: DONATION_STATUS.VERIFIED, + qfRoundId: qfRound1.id, }, user.id, project.id, From 3ebd200e3d9b9a3109b5215a3b8be5638842cfdc Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 26 Nov 2024 17:26:21 +0330 Subject: [PATCH 23/33] change field name based on changes --- src/resolvers/qAccResolver.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resolvers/qAccResolver.test.ts b/src/resolvers/qAccResolver.test.ts index 195d8e469..f9b565e87 100644 --- a/src/resolvers/qAccResolver.test.ts +++ b/src/resolvers/qAccResolver.test.ts @@ -294,7 +294,7 @@ function userCapsTestCases() { // Simulate valid GitcoinPassport score sinon.stub(user, 'analysisScore').value(80); sinon.stub(user, 'passportScoreUpdateTimestamp').value(new Date()); - sinon.stub(user, 'hasEnoughAnalysisScore').value(true); + sinon.stub(user, 'hasEnoughGitcoinAnalysisScore').value(true); // Act: Call the resolver through a GraphQL query const response: ExecutionResult<{ @@ -375,7 +375,7 @@ function userCapsTestCases() { it('should throw an error if the user does not meet the minimum analysis score', async () => { // Simulate invalid GitcoinPassport score sinon.stub(user, 'analysisScore').value(40); // Below threshold - sinon.stub(user, 'hasEnoughAnalysisScore').value(false); + sinon.stub(user, 'hasEnoughGitcoinAnalysisScore').value(false); // Act: Call the resolver through a GraphQL query and expect an error try { From 312d2acc6b804487dbc41a83e87be8d611b915c2 Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 26 Nov 2024 17:36:12 +0330 Subject: [PATCH 24/33] Remove redundant function --- src/resolvers/qAccResolver.ts | 7 +++++-- src/services/userService.ts | 13 +------------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/resolvers/qAccResolver.ts b/src/resolvers/qAccResolver.ts index c961d6751..8411af51a 100644 --- a/src/resolvers/qAccResolver.ts +++ b/src/resolvers/qAccResolver.ts @@ -12,7 +12,7 @@ import { getProjectUserRecordAmount } from '../repositories/projectUserRecordRep import qAccService from '../services/qAccService'; import { ApolloContext } from '../types/ApolloContext'; import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; -import { getUserById } from '../services/userService'; +import { findUserById } from '../repositories/userRepository'; @ObjectType() class ProjectUserRecordAmounts { @@ -85,7 +85,10 @@ export class QAccResolver { i18n.__(translationErrorMessagesKeys.AUTHENTICATION_REQUIRED), ); - const dbUser = await getUserById(user.userId); + const dbUser = await findUserById(user.userId); + if (!dbUser) { + throw new Error(`user not found with id ${user.userId}`); + } const qAccCap = await qAccService.getQAccDonationCap({ projectId, diff --git a/src/services/userService.ts b/src/services/userService.ts index b3ab2586a..b4994def1 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -1,10 +1,7 @@ import { User } from '../entities/user'; import { Donation } from '../entities/donation'; import { logger } from '../utils/logger'; -import { - findAdminUserByEmail, - findUserById, -} from '../repositories/userRepository'; +import { findAdminUserByEmail } from '../repositories/userRepository'; import { getGitcoinAdapter } from '../adapters/adaptersFactory'; // eslint-disable-next-line @typescript-eslint/no-var-requires const bcrypt = require('bcrypt'); @@ -102,11 +99,3 @@ export const updateUserGitcoinAnalysisScore = async (user: User) => { user.passportScoreUpdateTimestamp = new Date(); await user.save(); }; - -export const getUserById = async (userId: number) => { - const foundedUser = await findUserById(userId); - if (foundedUser) { - return foundedUser; - } - throw new Error(`user not found with id ${userId}`); -}; From 0fa648d18fb95264c8355d6e0230573b7242990c Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 26 Nov 2024 17:36:42 +0330 Subject: [PATCH 25/33] Fix a bug in converting usd to pol --- config/example.env | 2 +- src/constants/gitcoin.ts | 7 ++++--- src/services/qAccService.ts | 12 ++++++++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/config/example.env b/config/example.env index 8ca1f1f40..2610c1a28 100644 --- a/config/example.env +++ b/config/example.env @@ -310,5 +310,5 @@ MONGO_DB_REPORT_DB_NAME= GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE= # 1 day GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS=86400000 -MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY= +MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD= ACTIVATE_GITCOIN_SCORE_CHECK= diff --git a/src/constants/gitcoin.ts b/src/constants/gitcoin.ts index f4a4ef7e5..34c4cdce5 100644 --- a/src/constants/gitcoin.ts +++ b/src/constants/gitcoin.ts @@ -4,6 +4,7 @@ export const GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS = (+config.get('GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS') as number) || 86400000; // 1 day export const GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE = (+config.get('GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE') as number) || 50; -export const MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY = - (+config.get('MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY') as number) || - 1000; +export const MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD = + (+config.get( + 'MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD', + ) as number) || 1000; diff --git a/src/services/qAccService.ts b/src/services/qAccService.ts index 65402d4a9..729a63a4d 100644 --- a/src/services/qAccService.ts +++ b/src/services/qAccService.ts @@ -12,7 +12,7 @@ import { updateUserGitcoinAnalysisScore } from './userService'; import { GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS, GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE, - MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY, + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD, } from '../constants/gitcoin'; const getEaProjectRoundRecord = async ({ @@ -226,8 +226,16 @@ const getUserRemainedCapBasedOnGitcoinScore = async ({ projectId, userId: user.id, }); + const activeQfRound = await findActiveQfRound(); const qfTotalDonationAmount = userRecord.qfTotalDonationAmount; - return MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY - qfTotalDonationAmount; + if (!activeQfRound?.tokenPrice) { + throw new Error('active qf round does not have token price!'); + } + return ( + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD / + activeQfRound?.tokenPrice - + qfTotalDonationAmount + ); }; const validDonationAmountBasedOnKYCAndScore = async ({ From acbe033f3077673907a27ce4730736b0132f5ccb Mon Sep 17 00:00:00 2001 From: Amin Latifi Date: Tue, 26 Nov 2024 17:40:54 +0330 Subject: [PATCH 26/33] Renamed variable --- config/example.env | 2 +- src/constants/gitcoin.ts | 7 +++---- src/services/qAccService.ts | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/config/example.env b/config/example.env index 2610c1a28..bab348d2b 100644 --- a/config/example.env +++ b/config/example.env @@ -310,5 +310,5 @@ MONGO_DB_REPORT_DB_NAME= GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE= # 1 day GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS=86400000 -MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD= +MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD= ACTIVATE_GITCOIN_SCORE_CHECK= diff --git a/src/constants/gitcoin.ts b/src/constants/gitcoin.ts index 34c4cdce5..31df962ee 100644 --- a/src/constants/gitcoin.ts +++ b/src/constants/gitcoin.ts @@ -4,7 +4,6 @@ export const GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS = (+config.get('GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS') as number) || 86400000; // 1 day export const GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE = (+config.get('GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE') as number) || 50; -export const MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD = - (+config.get( - 'MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD', - ) as number) || 1000; +export const MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD = + (+config.get('MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD') as number) || + 1000; diff --git a/src/services/qAccService.ts b/src/services/qAccService.ts index 729a63a4d..e24b3d208 100644 --- a/src/services/qAccService.ts +++ b/src/services/qAccService.ts @@ -12,7 +12,7 @@ import { updateUserGitcoinAnalysisScore } from './userService'; import { GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS, GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE, - MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD, + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD, } from '../constants/gitcoin'; const getEaProjectRoundRecord = async ({ @@ -232,7 +232,7 @@ const getUserRemainedCapBasedOnGitcoinScore = async ({ throw new Error('active qf round does not have token price!'); } return ( - MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD / + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD / activeQfRound?.tokenPrice - qfTotalDonationAmount ); From 7f9628afbdf9f64d55ff36b3d8135c6cc622a834 Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 26 Nov 2024 17:43:48 +0330 Subject: [PATCH 27/33] make variables dynamic --- src/resolvers/qAccResolver.test.ts | 36 +++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/resolvers/qAccResolver.test.ts b/src/resolvers/qAccResolver.test.ts index f9b565e87..485614bf0 100644 --- a/src/resolvers/qAccResolver.test.ts +++ b/src/resolvers/qAccResolver.test.ts @@ -28,7 +28,10 @@ import { } from '../../test/graphqlQueries'; import { ProjectRoundRecord } from '../entities/projectRoundRecord'; import { EarlyAccessRound } from '../entities/earlyAccessRound'; -import { GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE } from '../constants/gitcoin'; +import { + GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE, + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD, +} from '../constants/gitcoin'; describe( 'projectUserTotalDonationAmount() test cases', @@ -296,7 +299,6 @@ function userCapsTestCases() { sinon.stub(user, 'passportScoreUpdateTimestamp').value(new Date()); sinon.stub(user, 'hasEnoughGitcoinAnalysisScore').value(true); - // Act: Call the resolver through a GraphQL query const response: ExecutionResult<{ userCaps: { qAccCap: number; @@ -320,9 +322,18 @@ function userCapsTestCases() { }, ); - // Assert: Verify the response matches expected values - assert.equal(response.data?.userCaps?.qAccCap, 4900); // Adjust based on logic - assert.equal(response.data?.userCaps?.gitcoinPassport?.unusedCap, 1200); + assert.equal( + response.data?.userCaps?.qAccCap, + Number(qfRound1.roundUSDCapPerUserPerProject) / + Number(qfRound1.tokenPrice) - + donationAmount, + ); + assert.equal( + response.data?.userCaps?.gitcoinPassport?.unusedCap, + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD / + Number(qfRound1.tokenPrice) - + donationAmount, + ); assert.isUndefined(response.data?.userCaps?.zkId); }); @@ -342,7 +353,6 @@ function userCapsTestCases() { sinon.stub(user, 'privadoVerified').value(true); - // Act: Call the resolver through a GraphQL query const response: ExecutionResult<{ userCaps: { qAccCap: number; @@ -367,8 +377,18 @@ function userCapsTestCases() { ); // Assert: Verify the response matches expected values - assert.equal(response.data?.userCaps?.qAccCap, 4500); // Adjust based on logic - assert.equal(response.data?.userCaps?.zkId?.unusedCap, 900); + assert.equal( + response.data?.userCaps?.qAccCap, + Number(qfRound1.roundUSDCapPerUserPerProject) / + Number(qfRound1.tokenPrice) - + donationAmount, + ); + assert.equal( + response.data?.userCaps?.zkId?.unusedCap, + Number(qfRound1.roundUSDCapPerUserPerProject) / + Number(qfRound1.tokenPrice) - + donationAmount, + ); assert.isUndefined(response.data?.userCaps?.gitcoinPassport); }); From 069635f1d66402b2538a67fd67b41e155217303c Mon Sep 17 00:00:00 2001 From: Amin Latifi Date: Tue, 26 Nov 2024 17:50:02 +0330 Subject: [PATCH 28/33] Renamed variable --- src/resolvers/qAccResolver.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resolvers/qAccResolver.test.ts b/src/resolvers/qAccResolver.test.ts index 485614bf0..382a764e0 100644 --- a/src/resolvers/qAccResolver.test.ts +++ b/src/resolvers/qAccResolver.test.ts @@ -30,7 +30,7 @@ import { ProjectRoundRecord } from '../entities/projectRoundRecord'; import { EarlyAccessRound } from '../entities/earlyAccessRound'; import { GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE, - MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD, + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD, } from '../constants/gitcoin'; describe( @@ -330,7 +330,7 @@ function userCapsTestCases() { ); assert.equal( response.data?.userCaps?.gitcoinPassport?.unusedCap, - MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD / + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD / Number(qfRound1.tokenPrice) - donationAmount, ); From 4ace67219cc437f3d138fcd12cc031202b4f1b30 Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 26 Nov 2024 18:00:21 +0330 Subject: [PATCH 29/33] fix variable names --- src/resolvers/qAccResolver.test.ts | 4 ++-- test/graphqlQueries.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/resolvers/qAccResolver.test.ts b/src/resolvers/qAccResolver.test.ts index 485614bf0..382a764e0 100644 --- a/src/resolvers/qAccResolver.test.ts +++ b/src/resolvers/qAccResolver.test.ts @@ -30,7 +30,7 @@ import { ProjectRoundRecord } from '../entities/projectRoundRecord'; import { EarlyAccessRound } from '../entities/earlyAccessRound'; import { GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE, - MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD, + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD, } from '../constants/gitcoin'; describe( @@ -330,7 +330,7 @@ function userCapsTestCases() { ); assert.equal( response.data?.userCaps?.gitcoinPassport?.unusedCap, - MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD / + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD / Number(qfRound1.tokenPrice) - donationAmount, ); diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 6c4f78f5c..ef9a5f732 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -2165,10 +2165,10 @@ export const userCaps = ` userCaps(projectId: $projectId) { qAccCap gitcoinPassport { - unusedCapped + unusedCap } zkId { - unusedCapped + unusedCap } } } From d93517228e403aaf9253d68ee85e5aff01c20d6a Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 27 Nov 2024 04:26:47 +0330 Subject: [PATCH 30/33] fix tests --- src/resolvers/qAccResolver.test.ts | 56 +++++++++++++++++------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/resolvers/qAccResolver.test.ts b/src/resolvers/qAccResolver.test.ts index 382a764e0..723552ae5 100644 --- a/src/resolvers/qAccResolver.test.ts +++ b/src/resolvers/qAccResolver.test.ts @@ -32,6 +32,7 @@ import { GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE, MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD, } from '../constants/gitcoin'; +import { PrivadoAdapter } from '../adapters/privado/privadoAdapter'; describe( 'projectUserTotalDonationAmount() test cases', @@ -269,7 +270,7 @@ function userCapsTestCases() { tokenPrice: 0.5, }).save(); sinon.useFakeTimers({ - now: qfRound1.beginDate.getTime(), + now: new Date('2001-01-15').getTime(), }); }); afterEach(async () => { @@ -295,18 +296,20 @@ function userCapsTestCases() { ); // Simulate valid GitcoinPassport score - sinon.stub(user, 'analysisScore').value(80); - sinon.stub(user, 'passportScoreUpdateTimestamp').value(new Date()); - sinon.stub(user, 'hasEnoughGitcoinAnalysisScore').value(true); + user.analysisScore = 80; + user.passportScoreUpdateTimestamp = new Date(); + await user.save(); const response: ExecutionResult<{ - userCaps: { - qAccCap: number; - gitcoinPassport?: { - unusedCap: number; - }; - zkId?: { - unusedCap: number; + data: { + userCaps: { + qAccCap: number; + gitcoinPassport?: { + unusedCap: number; + }; + zkId?: { + unusedCap: number; + }; }; }; }> = await axios.post( @@ -323,18 +326,18 @@ function userCapsTestCases() { ); assert.equal( - response.data?.userCaps?.qAccCap, + response.data?.data.userCaps?.qAccCap, Number(qfRound1.roundUSDCapPerUserPerProject) / Number(qfRound1.tokenPrice) - donationAmount, ); assert.equal( - response.data?.userCaps?.gitcoinPassport?.unusedCap, + response.data?.data.userCaps?.gitcoinPassport?.unusedCap, MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD / Number(qfRound1.tokenPrice) - donationAmount, ); - assert.isUndefined(response.data?.userCaps?.zkId); + assert.isNull(response.data?.data.userCaps?.zkId); }); it('should return correct caps for a user with ZkId', async () => { @@ -351,16 +354,19 @@ function userCapsTestCases() { project.id, ); - sinon.stub(user, 'privadoVerified').value(true); + user.privadoVerifiedRequestIds = [PrivadoAdapter.privadoRequestId]; + await user.save(); const response: ExecutionResult<{ - userCaps: { - qAccCap: number; - gitcoinPassport?: { - unusedCap: number; - }; - zkId?: { - unusedCap: number; + data: { + userCaps: { + qAccCap: number; + gitcoinPassport?: { + unusedCap: number; + }; + zkId?: { + unusedCap: number; + }; }; }; }> = await axios.post( @@ -378,18 +384,18 @@ function userCapsTestCases() { // Assert: Verify the response matches expected values assert.equal( - response.data?.userCaps?.qAccCap, + response.data?.data.userCaps?.qAccCap, Number(qfRound1.roundUSDCapPerUserPerProject) / Number(qfRound1.tokenPrice) - donationAmount, ); assert.equal( - response.data?.userCaps?.zkId?.unusedCap, + response.data?.data.userCaps?.zkId?.unusedCap, Number(qfRound1.roundUSDCapPerUserPerProject) / Number(qfRound1.tokenPrice) - donationAmount, ); - assert.isUndefined(response.data?.userCaps?.gitcoinPassport); + assert.isNull(response.data?.data.userCaps?.gitcoinPassport); }); it('should throw an error if the user does not meet the minimum analysis score', async () => { From 0737e3f88cb4f058df8cc3cb4a2b8cb86d3a7b36 Mon Sep 17 00:00:00 2001 From: ali Date: Thu, 28 Nov 2024 04:25:58 +0330 Subject: [PATCH 31/33] add gitcoin passport scorer check to cap calculation flow --- config/example.env | 1 + src/constants/gitcoin.ts | 2 ++ src/entities/user.ts | 13 ++++++++++++- src/resolvers/qAccResolver.ts | 5 ++++- src/services/qAccService.ts | 16 +++++++++++----- src/services/userService.ts | 14 +++++++------- 6 files changed, 37 insertions(+), 14 deletions(-) diff --git a/config/example.env b/config/example.env index bab348d2b..aa9ca0efa 100644 --- a/config/example.env +++ b/config/example.env @@ -308,6 +308,7 @@ MONGO_DB_REPORT_DB_NAME= # Gitcoin score GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE= +GITCOIN_PASSPORT_MIN_VALID_SCORER_SCORE= # 1 day GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS=86400000 MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD= diff --git a/src/constants/gitcoin.ts b/src/constants/gitcoin.ts index 31df962ee..dff805189 100644 --- a/src/constants/gitcoin.ts +++ b/src/constants/gitcoin.ts @@ -4,6 +4,8 @@ export const GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS = (+config.get('GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS') as number) || 86400000; // 1 day export const GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE = (+config.get('GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE') as number) || 50; +export const GITCOIN_PASSPORT_MIN_VALID_SCORER_SCORE = + (+config.get('GITCOIN_PASSPORT_MIN_VALID_SCORER_SCORE') as number) || 15; export const MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD = (+config.get('MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD') as number) || 1000; diff --git a/src/entities/user.ts b/src/entities/user.ts index a2cb5f073..d13083560 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -19,7 +19,10 @@ import { ProjectVerificationForm } from './projectVerificationForm'; import { ReferredEvent } from './referredEvent'; import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; import { PrivadoAdapter } from '../adapters/privado/privadoAdapter'; -import { GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE } from '../constants/gitcoin'; +import { + GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE, + GITCOIN_PASSPORT_MIN_VALID_SCORER_SCORE, +} from '../constants/gitcoin'; export const publicSelectionFields = [ 'user.id', @@ -245,6 +248,14 @@ export class User extends BaseEntity { ); } + @Field(_type => Boolean, { nullable: true }) + get hasEnoughGitcoinPassportScore(): boolean { + return !!( + this.passportScore && + this.passportScore >= GITCOIN_PASSPORT_MIN_VALID_SCORER_SCORE + ); + } + @Field(_type => Int, { nullable: true }) async donationsCount() { return await Donation.createQueryBuilder('donation') diff --git a/src/resolvers/qAccResolver.ts b/src/resolvers/qAccResolver.ts index 8411af51a..6ffabf1d4 100644 --- a/src/resolvers/qAccResolver.ts +++ b/src/resolvers/qAccResolver.ts @@ -103,7 +103,10 @@ export class QAccResolver { response.zkId = { unusedCap: qAccCap, }; - } else if (dbUser.hasEnoughGitcoinAnalysisScore) { + } else if ( + dbUser.hasEnoughGitcoinAnalysisScore || + dbUser.hasEnoughGitcoinPassportScore + ) { const cap = await qAccService.getUserRemainedCapBasedOnGitcoinScore({ projectId, user: dbUser, diff --git a/src/services/qAccService.ts b/src/services/qAccService.ts index e24b3d208..96910126f 100644 --- a/src/services/qAccService.ts +++ b/src/services/qAccService.ts @@ -8,10 +8,11 @@ import { findActiveEarlyAccessRound } from '../repositories/earlyAccessRoundRepo import { updateOrCreateProjectRoundRecord } from '../repositories/projectRoundRecordRepository'; import { updateOrCreateProjectUserRecord } from '../repositories/projectUserRecordRepository'; import { findActiveQfRound } from '../repositories/qfRoundRepository'; -import { updateUserGitcoinAnalysisScore } from './userService'; +import { updateUserGitcoinScores } from './userService'; import { GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS, GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE, + GITCOIN_PASSPORT_MIN_VALID_SCORER_SCORE, MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD, } from '../constants/gitcoin'; @@ -210,16 +211,21 @@ const getUserRemainedCapBasedOnGitcoinScore = async ({ user: User; }): Promise => { if ( - !user.analysisScore || + user.passportScore === null || + user.analysisScore === null || !user.passportScoreUpdateTimestamp || user.passportScoreUpdateTimestamp.getTime() < Date.now() - GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS ) { - await updateUserGitcoinAnalysisScore(user); + await updateUserGitcoinScores(user); } - if (!user.hasEnoughGitcoinAnalysisScore) { + if ( + !user.hasEnoughGitcoinAnalysisScore && + !user.hasEnoughGitcoinPassportScore + ) { throw new Error( - `analysis score is less than ${GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE}`, + `analysis score is less than ${GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE} + and passport score is less than ${GITCOIN_PASSPORT_MIN_VALID_SCORER_SCORE}`, ); } const userRecord = await getUserProjectRecord({ diff --git a/src/services/userService.ts b/src/services/userService.ts index b4994def1..e652debae 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -86,13 +86,13 @@ export const fetchAdminAndValidatePassword = async (params: { } }; -export const updateUserGitcoinAnalysisScore = async (user: User) => { - // const passportScore = await getGitcoinAdapter().getWalletAddressScore( - // user.walletAddress as string, - // ); - // if (passportScore && passportScore?.score) { - // user.passportScore = Number(passportScore.score); - // } +export const updateUserGitcoinScores = async (user: User) => { + const passportScore = await getGitcoinAdapter().getWalletAddressScore( + user.walletAddress as string, + ); + if (passportScore && passportScore?.score) { + user.passportScore = Number(passportScore.score); + } user.analysisScore = await getGitcoinAdapter().getUserAnalysisScore( user.walletAddress as string, ); From 311292d65cf6a2b301f8df688d46c13a1cba78a9 Mon Sep 17 00:00:00 2001 From: ali Date: Thu, 28 Nov 2024 04:31:35 +0330 Subject: [PATCH 32/33] add unit test for scorer score check --- src/resolvers/qAccResolver.test.ts | 79 +++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/src/resolvers/qAccResolver.test.ts b/src/resolvers/qAccResolver.test.ts index 723552ae5..ddd3d17e2 100644 --- a/src/resolvers/qAccResolver.test.ts +++ b/src/resolvers/qAccResolver.test.ts @@ -30,6 +30,7 @@ import { ProjectRoundRecord } from '../entities/projectRoundRecord'; import { EarlyAccessRound } from '../entities/earlyAccessRound'; import { GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE, + GITCOIN_PASSPORT_MIN_VALID_SCORER_SCORE, MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD, } from '../constants/gitcoin'; import { PrivadoAdapter } from '../adapters/privado/privadoAdapter'; @@ -281,7 +282,7 @@ function userCapsTestCases() { sinon.restore(); }); - it('should return correct caps for a user with GitcoinPassport', async () => { + it('should return correct caps for a user with analysis score', async () => { // Save donations const donationAmount = 100; await saveDonationDirectlyToDb( @@ -295,8 +296,69 @@ function userCapsTestCases() { project.id, ); - // Simulate valid GitcoinPassport score + // Simulate valid analysis score user.analysisScore = 80; + user.passportScore = 0; + user.passportScoreUpdateTimestamp = new Date(); + await user.save(); + + const response: ExecutionResult<{ + data: { + userCaps: { + qAccCap: number; + gitcoinPassport?: { + unusedCap: number; + }; + zkId?: { + unusedCap: number; + }; + }; + }; + }> = await axios.post( + graphqlUrl, + { + query: userCaps, + variables: { projectId: project.id }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + assert.equal( + response.data?.data.userCaps?.qAccCap, + Number(qfRound1.roundUSDCapPerUserPerProject) / + Number(qfRound1.tokenPrice) - + donationAmount, + ); + assert.equal( + response.data?.data.userCaps?.gitcoinPassport?.unusedCap, + MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD / + Number(qfRound1.tokenPrice) - + donationAmount, + ); + assert.isNull(response.data?.data.userCaps?.zkId); + }); + + it('should return correct caps for a user with passport score', async () => { + // Save donations + const donationAmount = 100; + await saveDonationDirectlyToDb( + { + ...createDonationData(), + amount: donationAmount, + status: DONATION_STATUS.VERIFIED, + qfRoundId: qfRound1.id, + }, + user.id, + project.id, + ); + + // Simulate valid GitcoinPassport score + user.analysisScore = 0; + user.passportScore = 30; user.passportScoreUpdateTimestamp = new Date(); await user.save(); @@ -398,10 +460,12 @@ function userCapsTestCases() { assert.isNull(response.data?.data.userCaps?.gitcoinPassport); }); - it('should throw an error if the user does not meet the minimum analysis score', async () => { - // Simulate invalid GitcoinPassport score - sinon.stub(user, 'analysisScore').value(40); // Below threshold - sinon.stub(user, 'hasEnoughGitcoinAnalysisScore').value(false); + it('should throw an error if the user does not meet the minimum analysis score and passport score', async () => { + // Simulate invalid GitcoinPassport scores + user.analysisScore = 40; + user.passportScore = 10; + user.passportScoreUpdateTimestamp = new Date(); + await user.save(); // Act: Call the resolver through a GraphQL query and expect an error try { @@ -421,7 +485,8 @@ function userCapsTestCases() { // Assert: Verify the error message assert.equal( error.response.data.errors[0].message, - `analysis score is less than ${GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE}`, + `analysis score is less than ${GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE} + and passport score is less than ${GITCOIN_PASSPORT_MIN_VALID_SCORER_SCORE}`, ); } }); From b3c2c8373bdfe6ae5b8ee0113074ee018f0c68a4 Mon Sep 17 00:00:00 2001 From: ali Date: Thu, 28 Nov 2024 04:45:11 +0330 Subject: [PATCH 33/33] skip unused test --- src/resolvers/qfRoundResolver.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/resolvers/qfRoundResolver.test.ts b/src/resolvers/qfRoundResolver.test.ts index 07964c9b2..f593f8e41 100644 --- a/src/resolvers/qfRoundResolver.test.ts +++ b/src/resolvers/qfRoundResolver.test.ts @@ -23,7 +23,10 @@ import { generateRandomString } from '../utils/utils'; import { OrderDirection } from './projectResolver'; import { QfArchivedRoundsSortType } from '../repositories/qfRoundRepository'; -describe('Fetch estimatedMatching test cases', fetchEstimatedMatchingTestCases); +describe.skip( + 'Fetch estimatedMatching test cases', + fetchEstimatedMatchingTestCases, +); describe('Fetch qfRoundStats test cases', fetchQfRoundStatesTestCases); describe('Fetch archivedQFRounds test cases', fetchArchivedQFRoundsTestCases); describe('update scoreUserAddress test cases', scoreUserAddressTestCases);