Skip to content

Commit

Permalink
exclude projects in qf round recpient donations from actual matching (#…
Browse files Browse the repository at this point in the history
…1417)

* Confirms donations from recipients of non-verified projects that are in that qfRound are excluded

related to Giveth/giveth-dapps-v2#3857

* Add test case for project view service

* Fix giveth project link in googlesheet

* Add test cases and fix for calculating project weight for actual matching fund

related to Giveth/giveth-dapps-v2#3838
  • Loading branch information
mohammadranjbarz authored Mar 21, 2024
1 parent 8627a1d commit e651c0b
Show file tree
Hide file tree
Showing 6 changed files with 712 additions and 50 deletions.
178 changes: 178 additions & 0 deletions migration/1710768644383-project_actual_matching_v13_.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class ProjectActualMatchingV13_1710768644383
implements MigrationInterface
{
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DROP MATERIALIZED VIEW IF EXISTS project_actual_matching_view;
CREATE MATERIALIZED VIEW project_actual_matching_view AS
WITH ProjectsAndRounds AS (
SELECT
p.id AS "projectId",
p.slug,
p.title,
qr.id as "qfId",
qr."minimumPassportScore",
qr."eligibleNetworks",
STRING_AGG(DISTINCT CONCAT(pa."networkId", '-', pa."address"), ', ') AS "networkAddresses"
FROM
public.project p
INNER JOIN project_qf_rounds_qf_round pqrq ON pqrq."projectId" = p.id
INNER JOIN public.qf_round qr on qr.id = pqrq."qfRoundId"
LEFT JOIN project_address pa ON pa."projectId" = p.id AND pa."networkId" = ANY(qr."eligibleNetworks") AND pa."isRecipient" = true
group by
p.id,
qr.id
),
DonationsBeforeAnalysis AS (
SELECT
par."projectId",
par.slug,
par.title,
par."qfId",
par."networkAddresses",
par."minimumPassportScore" as "minimumPassportScore",
COALESCE(SUM(d."valueUsd"), 0) AS "allUsdReceived",
COUNT(DISTINCT CASE WHEN d."fromWalletAddress" IS NOT NULL THEN d."fromWalletAddress" END) AS "totalDonors",
ARRAY_AGG(DISTINCT d.id) FILTER (WHERE d.id IS NOT NULL) AS "donationIdsBeforeAnalysis"
FROM
ProjectsAndRounds par
LEFT JOIN public.donation d ON d."projectId" = par."projectId" AND d."qfRoundId" = par."qfId" AND d."status" = 'verified' AND d."transactionNetworkId" = ANY(par."eligibleNetworks")
GROUP BY
par."projectId",
par.title,
par."networkAddresses",
par.slug,
par."qfId",
par."minimumPassportScore"
),
UserProjectDonations AS (
SELECT
par."projectId",
par."qfId" AS "qfRoundId",
d2."userId",
d2."fromWalletAddress",
d2."qfRoundUserScore",
COALESCE(SUM(d2."valueUsd"), 0) AS "totalValueUsd",
ARRAY_AGG(DISTINCT d2.id) FILTER (WHERE d2.id IS NOT NULL) AS "userDonationIds"
FROM
ProjectsAndRounds par
LEFT JOIN public.donation d2 ON d2."projectId" = par."projectId" AND d2."qfRoundId" = par."qfId" AND d2."status" = 'verified' AND d2."transactionNetworkId" = ANY(par."eligibleNetworks")
GROUP BY
par."projectId",
par."qfId",
d2."userId",
d2."fromWalletAddress",
d2."qfRoundUserScore"
),
QualifiedUserDonations AS (
SELECT
upd."userId",
upd."fromWalletAddress",
upd."projectId",
upd."qfRoundId",
upd."totalValueUsd",
upd."userDonationIds",
upd."qfRoundUserScore"
FROM
UserProjectDonations upd
WHERE
upd."totalValueUsd" >= (SELECT "minimumValidUsdValue" FROM public.qf_round WHERE id = upd."qfRoundId")
AND upd."qfRoundUserScore" >= (SELECT "minimumPassportScore" FROM public.qf_round WHERE id = upd."qfRoundId")
AND NOT EXISTS (
SELECT 1
FROM project_fraud pf
WHERE pf."projectId" = upd."projectId"
AND pf."qfRoundId" = upd."qfRoundId"
)
AND NOT EXISTS (
SELECT 1
FROM sybil s
WHERE s."userId" = upd."userId"
AND s."qfRoundId" = upd."qfRoundId"
)
AND NOT EXISTS (
SELECT 1
FROM project verified_project
JOIN project_address ON verified_project."id" = project_address."projectId"
WHERE verified_project.verified = true
AND lower(project_address."address") = lower(upd."fromWalletAddress")
)
AND NOT EXISTS (
SELECT 1
FROM project_address pa
INNER JOIN project_qf_rounds_qf_round pqrq ON pa."projectId" = pqrq."projectId"
WHERE pqrq."qfRoundId" = upd."qfRoundId" -- Ensuring we're looking at the same QF round
AND lower(pa."address") = lower(upd."fromWalletAddress")
AND pa."isRecipient" = true
)
),
DonationIDsAggregated AS (
SELECT
qud."projectId",
qud."qfRoundId",
ARRAY_AGG(DISTINCT unnested_ids) AS uniqueDonationIds
FROM
QualifiedUserDonations qud,
LATERAL UNNEST(qud."userDonationIds") AS unnested_ids
GROUP BY qud."projectId", qud."qfRoundId"
),
DonationsAfterAnalysis AS (
SELECT
da."projectId",
da.slug,
da.title,
da."qfId",
COALESCE(SUM(qud."totalValueUsd"), 0) AS "allUsdReceivedAfterSybilsAnalysis",
COUNT(DISTINCT qud."fromWalletAddress") AS "uniqueQualifiedDonors",
SUM(SQRT(qud."totalValueUsd")) AS "donationsSqrtRootSum",
POWER(SUM(SQRT(qud."totalValueUsd")), 2) as "donationsSqrtRootSumSquared",
dia.uniqueDonationIds AS "donationIdsAfterAnalysis",
ARRAY_AGG(DISTINCT qud."userId") AS "uniqueUserIdsAfterAnalysis",
ARRAY_AGG(qud."totalValueUsd") AS "totalValuesOfUserDonationsAfterAnalysis"
FROM
DonationsBeforeAnalysis da
LEFT JOIN QualifiedUserDonations qud ON da."projectId" = qud."projectId" AND da."qfId" = qud."qfRoundId"
LEFT JOIN DonationIDsAggregated dia ON da."projectId" = dia."projectId" AND da."qfId" = dia."qfRoundId"
GROUP BY
da."projectId",
da.slug,
da.title,
da."qfId",
dia."uniquedonationids",
da."networkAddresses"
)
SELECT
da."projectId",
da.title,
da.slug,
da."networkAddresses",
da."qfId" AS "qfRoundId",
da."donationIdsBeforeAnalysis",
da."allUsdReceived",
da."totalDonors",
daa."donationIdsAfterAnalysis",
daa."allUsdReceivedAfterSybilsAnalysis",
daa."uniqueQualifiedDonors",
daa."donationsSqrtRootSum",
daa."donationsSqrtRootSumSquared",
daa."uniqueUserIdsAfterAnalysis",
daa."totalValuesOfUserDonationsAfterAnalysis"
FROM
DonationsBeforeAnalysis da
INNER JOIN DonationsAfterAnalysis daa ON da."projectId" = daa."projectId" AND da."qfId" = daa."qfId";
CREATE INDEX idx_project_actual_matching_project_id ON project_actual_matching_view USING hash ("projectId");
CREATE INDEX idx_project_actual_matching_qf_round_id ON project_actual_matching_view USING hash ("qfRoundId");
`);
}

async down(queryRunner: QueryRunner): Promise<void> {
//
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
"test:reactionsService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/reactionsService.test.ts",
"test:powerSnapshotService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/powerSnapshotServices.test.ts",
"test:transactionsService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts src/services/chains/index.test.ts",
"test:projectViewService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts src/services/projectViewService.test.ts",
"test:transactionsService:evm": "NODE_ENV=test mocha ./test/pre-test-scripts.ts src/services/chains/evm/transactionService.test.ts",
"test:transactionsService:solana": "NODE_ENV=test mocha ./test/pre-test-scripts.ts src/services/chains/solana/transactionService.test.ts",
"test:projectUpdatesService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/projectUpdatesService.test.ts",
Expand Down
150 changes: 150 additions & 0 deletions src/services/actualMatchingFundView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,156 @@ function getActualMatchingFundTests() {
// qfRound has 4 networks so we just recipient addresses for those networks
assert.equal(actualMatchingFund?.networkAddresses?.split(',').length, 4);
});
it('Confirms donations from recipients of non-verified projects are included', async () => {
const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress());
user.passportScore = qfRound.minimumPassportScore;
await user.save();
const nonVerified = await saveProjectDirectlyToDb({
...createProjectData(),
walletAddress: user.walletAddress as string,
listed: true,
verified: false,
});
const donation = await saveDonationDirectlyToDb(
{
...createDonationData(),
status: 'verified',
valueUsd: 100,
qfRoundId: qfRound.id,
qfRoundUserScore: user.passportScore,
},
user.id,
project.id,
);
await refreshProjectActualMatchingView();

const actualMatchingFund = await ProjectActualMatchingView.findOne({
where: {
projectId: project.id,
qfRoundId: qfRound.id,
},
});
assert.equal(actualMatchingFund?.projectId, project.id);
assert.equal(actualMatchingFund?.donationIdsBeforeAnalysis.length, 1);
assert.equal(actualMatchingFund?.donationIdsBeforeAnalysis[0], donation.id);
assert.equal(actualMatchingFund?.donationIdsAfterAnalysis.length, 1);
assert.equal(actualMatchingFund?.allUsdReceived, donation.valueUsd);
assert.equal(
actualMatchingFund?.allUsdReceivedAfterSybilsAnalysis,
donation.valueUsd,
);
assert.equal(actualMatchingFund?.uniqueQualifiedDonors, 1);
assert.equal(actualMatchingFund?.totalDonors, 1);

// qfRound has 4 networks so we just recipient addresses for those networks
assert.equal(actualMatchingFund?.networkAddresses?.split(',').length, 4);
});
it('Confirms donations from recipients of non-verified projects that are in another qfRound are included', async () => {
const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress());
user.passportScore = qfRound.minimumPassportScore;
await user.save();
const projectInAnotherQfRound = await saveProjectDirectlyToDb({
...createProjectData(),
walletAddress: user.walletAddress as string,
listed: true,
verified: false,
});
const qfRound2 = QfRound.create({
isActive: true,
name: 'test',
allocatedFund: 100,
minimumPassportScore: 8,
minimumValidUsdValue: 1,
slug: new Date().getTime().toString(),
eligibleNetworks: [
NETWORK_IDS.XDAI,
NETWORK_IDS.OPTIMISTIC,
NETWORK_IDS.POLYGON,
NETWORK_IDS.MAIN_NET,
],
beginDate: new Date(),
endDate: moment().add(10, 'days').toDate(),
});

projectInAnotherQfRound.qfRounds = [qfRound2];
await projectInAnotherQfRound.save();
const donation = await saveDonationDirectlyToDb(
{
...createDonationData(),
status: 'verified',
valueUsd: 100,
qfRoundId: qfRound.id,
qfRoundUserScore: user.passportScore,
},
user.id,
project.id,
);
await refreshProjectActualMatchingView();

const actualMatchingFund = await ProjectActualMatchingView.findOne({
where: {
projectId: project.id,
qfRoundId: qfRound.id,
},
});
assert.equal(actualMatchingFund?.projectId, project.id);
assert.equal(actualMatchingFund?.donationIdsBeforeAnalysis.length, 1);
assert.equal(actualMatchingFund?.donationIdsBeforeAnalysis[0], donation.id);
assert.equal(actualMatchingFund?.donationIdsAfterAnalysis.length, 1);
assert.equal(actualMatchingFund?.allUsdReceived, donation.valueUsd);
assert.equal(
actualMatchingFund?.allUsdReceivedAfterSybilsAnalysis,
donation.valueUsd,
);
assert.equal(actualMatchingFund?.uniqueQualifiedDonors, 1);
assert.equal(actualMatchingFund?.totalDonors, 1);

// qfRound has 4 networks so we just recipient addresses for those networks
assert.equal(actualMatchingFund?.networkAddresses?.split(',').length, 4);
});
it('Confirms donations from recipients of non-verified projects that are in that qfRound are excluded', async () => {
const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress());
user.passportScore = qfRound.minimumPassportScore;
await user.save();
const projectInQfRound = await saveProjectDirectlyToDb({
...createProjectData(),
walletAddress: user.walletAddress as string,
listed: true,
verified: false,
});
projectInQfRound.qfRounds = [qfRound];
await projectInQfRound.save();
const donation = await saveDonationDirectlyToDb(
{
...createDonationData(),
status: 'verified',
valueUsd: 100,
qfRoundId: qfRound.id,
qfRoundUserScore: user.passportScore,
},
user.id,
project.id,
);
await refreshProjectActualMatchingView();

const actualMatchingFund = await ProjectActualMatchingView.findOne({
where: {
projectId: project.id,
qfRoundId: qfRound.id,
},
});
assert.equal(actualMatchingFund?.projectId, project.id);
assert.equal(actualMatchingFund?.donationIdsBeforeAnalysis.length, 1);
assert.equal(actualMatchingFund?.donationIdsBeforeAnalysis[0], donation.id);
assert.isNotOk(actualMatchingFund?.donationIdsAfterAnalysis);
assert.equal(actualMatchingFund?.allUsdReceived, donation.valueUsd);
assert.equal(actualMatchingFund?.allUsdReceivedAfterSybilsAnalysis, 0);
assert.equal(actualMatchingFund?.uniqueQualifiedDonors, 0);
assert.equal(actualMatchingFund?.totalDonors, 1);

// qfRound has 4 networks so we just recipient addresses for those networks
assert.equal(actualMatchingFund?.networkAddresses?.split(',').length, 4);
});
it('Validates correct aggregation of multiple donations to a project', async () => {
const valuesUsd = [4, 25, 100, 1024];
await Promise.all(
Expand Down
Loading

0 comments on commit e651c0b

Please sign in to comment.