Skip to content

Commit

Permalink
Merge pull request #142 from GeneralMagicio/addGitCionScoreToDonation…
Browse files Browse the repository at this point in the history
…Flow

Add git cion score to donation flow
  • Loading branch information
ae2079 authored Nov 26, 2024
2 parents 6e98c99 + acbe033 commit 14104b4
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 0 deletions.
7 changes: 7 additions & 0 deletions config/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,10 @@ ANKR_SYNC_CRONJOB_EXPRESSION=
# Reports database
MONGO_DB_URI=
MONGO_DB_REPORT_DB_NAME=

# Gitcoin score
GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE=
# 1 day
GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS=86400000
MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD=
ACTIVATE_GITCOIN_SCORE_CHECK=
19 changes: 19 additions & 0 deletions migration/1732582914845-addScoreTimestampToUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddScoreTimestampToUser1732582914845
implements MigrationInterface
{
name = 'AddScoreTimestampToUser1732582914845';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user" ADD "passportScoreUpdateTimestamp" TIMESTAMP WITH TIME ZONE`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user" DROP COLUMN "passportScoreUpdateTimestamp"`,
);
}
}
13 changes: 13 additions & 0 deletions migration/1732584356154-addAnalysisScoreToUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddAnalysisScoreToUser1732584356154 implements MigrationInterface {
name = 'AddAnalysisScoreToUser1732584356154';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" ADD "analysisScore" real`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "analysisScore"`);
}
}
9 changes: 9 additions & 0 deletions src/constants/gitcoin.ts
Original file line number Diff line number Diff line change
@@ -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_USD =
(+config.get('MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD') as number) ||
1000;
17 changes: 17 additions & 0 deletions src/entities/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE } from '../constants/gitcoin';

export const publicSelectionFields = [
'user.id',
Expand Down Expand Up @@ -117,6 +118,14 @@ 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;

@Field(_type => Number, { nullable: true })
@Column({ nullable: true, default: null })
passportStamps?: number;
Expand Down Expand Up @@ -228,6 +237,14 @@ export class User extends BaseEntity {
);
}

@Field(_type => Boolean, { nullable: true })
get hasEnoughGitcoinAnalysisScore(): boolean {
return !!(
this.analysisScore &&
this.analysisScore >= GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE
);
}

@Field(_type => Int, { nullable: true })
async donationsCount() {
return await Donation.createQueryBuilder('donation')
Expand Down
61 changes: 61 additions & 0 deletions src/resolvers/qAccResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { findUserById } from '../repositories/userRepository';

@ObjectType()
class ProjectUserRecordAmounts {
Expand All @@ -24,6 +25,25 @@ class ProjectUserRecordAmounts {
@Field(_type => Float)
qfTotalDonationAmount: number;
}

@ObjectType()
class UnusedCapResponse {
@Field(_type => Float)
unusedCap: number;
}

@ObjectType()
class QAccResponse {
@Field(_type => Float)
qAccCap: number;

@Field(_type => UnusedCapResponse, { nullable: true })
gitcoinPassport?: UnusedCapResponse;

@Field(_type => UnusedCapResponse, { nullable: true })
zkId?: UnusedCapResponse;
}

@Resolver()
export class QAccResolver {
@Query(_returns => ProjectUserRecordAmounts)
Expand Down Expand Up @@ -54,4 +74,45 @@ export class QAccResolver {
userId: user.userId,
});
}

@Query(_returns => QAccResponse)
async userCaps(
@Arg('projectId', _type => Int, { nullable: false }) projectId: number,
@Ctx() { req: { user } }: ApolloContext,
): Promise<QAccResponse> {
if (!user)
throw new Error(
i18n.__(translationErrorMessagesKeys.AUTHENTICATION_REQUIRED),
);

const dbUser = await findUserById(user.userId);
if (!dbUser) {
throw new Error(`user not found with id ${user.userId}`);
}

const qAccCap = await qAccService.getQAccDonationCap({
projectId,
userId: user.userId,
});

const response: QAccResponse = {
qAccCap,
};

if (dbUser.privadoVerified) {
response.zkId = {
unusedCap: qAccCap,
};
} else if (dbUser.hasEnoughGitcoinAnalysisScore) {
const cap = await qAccService.getUserRemainedCapBasedOnGitcoinScore({
projectId,
user: dbUser,
});
response.gitcoinPassport = {
unusedCap: cap,
};
}

return response;
}
}
5 changes: 5 additions & 0 deletions src/resolvers/userResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,13 @@ 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;
foundUser.passportScoreUpdateTimestamp = new Date();
}
if (passportStamps)
foundUser.passportStamps = passportStamps.items.length;
Expand All @@ -136,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);
Expand Down
67 changes: 67 additions & 0 deletions src/services/qAccService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ 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 { updateUserGitcoinAnalysisScore } from './userService';
import {
GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS,
GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE,
MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD,
} from '../constants/gitcoin';

const getEaProjectRoundRecord = async ({
projectId,
Expand Down Expand Up @@ -195,6 +202,66 @@ const getQAccDonationCap = async ({
}
};

const getUserRemainedCapBasedOnGitcoinScore = async ({
projectId,
user,
}: {
projectId: number;
user: User;
}): Promise<number> => {
if (
!user.analysisScore ||
!user.passportScoreUpdateTimestamp ||
user.passportScoreUpdateTimestamp.getTime() <
Date.now() - GITCOIN_PASSPORT_EXPIRATION_PERIOD_MS
) {
await updateUserGitcoinAnalysisScore(user);
}
if (!user.hasEnoughGitcoinAnalysisScore) {
throw new Error(
`analysis score is less than ${GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE}`,
);
}
const userRecord = await getUserProjectRecord({
projectId,
userId: user.id,
});
const activeQfRound = await findActiveQfRound();
const qfTotalDonationAmount = userRecord.qfTotalDonationAmount;
if (!activeQfRound?.tokenPrice) {
throw new Error('active qf round does not have token price!');
}
return (
MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_USD /
activeQfRound?.tokenPrice -
qfTotalDonationAmount
);
};

const validDonationAmountBasedOnKYCAndScore = async ({
projectId,
user,
amount,
}: {
projectId: number;
user: User;
amount: number;
}): Promise<boolean> => {
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');
}
return true;
};

export default {
getQAccDonationCap,
validDonationAmountBasedOnKYCAndScore,
getUserRemainedCapBasedOnGitcoinScore,
};
15 changes: 15 additions & 0 deletions src/services/userService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -84,3 +85,17 @@ export const fetchAdminAndValidatePassword = async (params: {
return;
}
};

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.analysisScore = await getGitcoinAdapter().getUserAnalysisScore(
user.walletAddress as string,
);
user.passportScoreUpdateTimestamp = new Date();
await user.save();
};
16 changes: 16 additions & 0 deletions src/utils/qacc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions test/graphqlQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
`;

0 comments on commit 14104b4

Please sign in to comment.