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 10 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_MIN_SCORE=
ae2079 marked this conversation as resolved.
Show resolved Hide resolved
# 1 day
VALID_SCORE_TIMESTAMP=86400000
ae2079 marked this conversation as resolved.
Show resolved Hide resolved
MAX_AMOUNT_NO_KYC=
ae2079 marked this conversation as resolved.
Show resolved Hide resolved
ACTIVATE_GITCOIN_SCORE_CHECK=
19 changes: 19 additions & 0 deletions migration/1732495872789-addScoreTimestampToUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

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

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

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user" DROP COLUMN "passportScoreUpdateTimestamp"`,
);
}
}
4 changes: 4 additions & 0 deletions src/entities/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ 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 => String, { nullable: true })
@Column({ nullable: true })
passportScoreUpdateTimestamp?: string;
ae2079 marked this conversation as resolved.
Show resolved Hide resolved

@Field(_type => Number, { nullable: true })
@Column({ nullable: true, default: null })
passportStamps?: number;
Expand Down
188 changes: 188 additions & 0 deletions src/resolvers/donationResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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());
Expand Down
67 changes: 67 additions & 0 deletions src/resolvers/donationResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,73 @@ export class DonationResolver {
};
}

@Query(_returns => Boolean)
async validateDonation(
ae2079 marked this conversation as resolved.
Show resolved Hide resolved
@Arg('amount') amount: number,
@Arg('token') token: string,
@Arg('transactionNetworkId') transactionNetworkId: number,
@Arg('projectId') projectId: number,
@Ctx() ctx: ApolloContext,
): Promise<boolean> {
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,
Expand Down
1 change: 1 addition & 0 deletions src/resolvers/userResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export class UserResolver {
if (passportScore && passportScore?.score) {
const score = Number(passportScore.score);
foundUser.passportScore = score;
foundUser.passportScoreUpdateTimestamp = Date.now().toString();
}
if (passportStamps)
foundUser.passportStamps = passportStamps.items.length;
Expand Down
45 changes: 45 additions & 0 deletions src/services/qAccService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -195,6 +198,48 @@ const getQAccDonationCap = async ({
}
};

const validDonationAmountBasedOnKYCAndScore = async ({
projectId,
user,
amount,
}: {
projectId: number;
user: User;
amount: number;
}): Promise<boolean> => {
if (user.privadoVerified) {
return true;
}
if (
!user.passportScore ||
!user.passportScoreUpdateTimestamp ||
+user.passportScoreUpdateTimestamp >
Date.now() - (Number(config.get('VALID_SCORE_TIMESTAMP')) || 86400000) // default value is 1 day
ae2079 marked this conversation as resolved.
Show resolved Hide resolved
) {
await updateUserGitcoinScore(user);
}
if (
!user.passportScore ||
user.passportScore < Number(config.get('GITCOIN_MIN_SCORE'))
ae2079 marked this conversation as resolved.
Show resolved Hide resolved
) {
throw new Error(
`passport score is less than ${config.get('GITCOIN_MIN_SCORE')}`,
ae2079 marked this conversation as resolved.
Show resolved Hide resolved
);
}
const userRecord = await getUserProjectRecord({
projectId,
userId: user.id,
});
const qfTotalDonationAmount = userRecord.qfTotalDonationAmount;
const remainedCap =
Number(config.get('MAX_AMOUNT_NO_KYC')) - qfTotalDonationAmount;
ae2079 marked this conversation as resolved.
Show resolved Hide resolved
if (amount > remainedCap) {
throw new Error('amount is more than allowed cap with gitcoin score');
}
return true;
};

export default {
getQAccDonationCap,
validDonationAmountBasedOnKYCAndScore,
};
Loading
Loading