Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add git cion score to donation flow #142

Merged
merged 18 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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_IN_USD=

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_USD=
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"`);
}
}
10 changes: 10 additions & 0 deletions src/constants/gitcoin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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_IN_USD =
(+config.get(
'MAX_CONTRIBUTION_WITH_GITCOIN_PASSPORT_ONLY_IN_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;

ae2079 marked this conversation as resolved.
Show resolved Hide resolved
@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_IN_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_IN_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')) &&
ae2079 marked this conversation as resolved.
Show resolved Hide resolved
(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
}
}
}
`;