From 2c8d77add7624ebae9bc2498e79b51f22e55e437 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Tue, 5 Dec 2023 15:55:06 +0330 Subject: [PATCH 1/7] Insert donations to db based on distributed funds for qfRound projects related to #1186 --- config/example.env | 3 + config/test.env | 2 + ...89359018-add_fields_to_qf_round_history.ts | 69 +++++++ .../1701756190381-create_donationeth_user.ts | 25 +++ src/entities/donation.ts | 9 + src/entities/qfRoundHistory.ts | 13 ++ .../qfRoundHistoryRepository.test.ts | 184 +++++++++++++++++- src/repositories/qfRoundHistoryRepository.ts | 21 ++ src/server/adminJs/tabs/qfRoundHistoryTab.ts | 27 +++ src/services/donationService.test.ts | 176 ++++++++++++++++- src/services/donationService.ts | 96 ++++++++- src/services/qfRoundHistoryService.ts | 16 ++ test/pre-test-scripts.ts | 2 + 13 files changed, 632 insertions(+), 11 deletions(-) create mode 100644 migration/1701689359018-add_fields_to_qf_round_history.ts create mode 100644 migration/1701756190381-create_donationeth_user.ts create mode 100644 src/services/qfRoundHistoryService.ts diff --git a/config/example.env b/config/example.env index 978c531d6..b2ad9c716 100644 --- a/config/example.env +++ b/config/example.env @@ -223,4 +223,7 @@ MORDOR_ETC_TESTNET_SCAN_API_URL=https://etc-mordor.blockscout.com/api/v1 # https://chainlist.org/chain/63 MORDOR_ETC_TESTNET_NODE_HTTP_URL=https://rpc.mordor.etccooperative.org + +# This is the address behind donation.eth +MATCHING_FUND_DONATIONS_FROM_ADDRESS=0x6e8873085530406995170Da467010565968C7C62 QF_ROUND_MAX_REWARD=0.2 diff --git a/config/test.env b/config/test.env index 5704b7eb3..fa5f504b4 100644 --- a/config/test.env +++ b/config/test.env @@ -189,5 +189,7 @@ MORDOR_ETC_TESTNET_NODE_HTTP_URL=https://rpc.mordor.etccooperative.org # MORDOR ETC TESTNET Node URL MORDOR_ETC_TESTNET_SCAN_API_URL=https://etc-mordor.blockscout.com/api/v1 +# This is the address behind donation.eth +MATCHING_FUND_DONATIONS_FROM_ADDRESS=0x6e8873085530406995170Da467010565968C7C62 diff --git a/migration/1701689359018-add_fields_to_qf_round_history.ts b/migration/1701689359018-add_fields_to_qf_round_history.ts new file mode 100644 index 000000000..8ae006cb7 --- /dev/null +++ b/migration/1701689359018-add_fields_to_qf_round_history.ts @@ -0,0 +1,69 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addFieldsToQfRoundHistory1701689359018 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "qf_round_history" + ADD COLUMN "matchingFundAmount" real , + ADD COLUMN "matchingFundPriceUsd" real , + ADD COLUMN "matchingFundCurrency" text ; + `); + + await queryRunner.query(` + ALTER TABLE "donation" + ADD COLUMN "distributedFundQfRoundId" integer; + + -- If you have a foreign key constraint to enforce the relationship + ALTER TABLE "donation" + ADD CONSTRAINT "FK_donation_qfRound" + FOREIGN KEY ("distributedFundQfRoundId") REFERENCES "qf_round"("id"); + `); + + // These rounds are in Production but I didnt set any condition for that + // because I want this part of code be executed in staging ENV + + // Alpha round in production + await queryRunner.query(` + UPDATE qf_round_history + SET + "matchingFundAmount" = "matchingFund", + "matchingFundPriceUsd" = 1, + "matchingFundCurrency" = 'WXDAI' + WHERE + id = 2 AND "matchingFund" IS NOT NULL; + + `); + + // Optimism round in production + await queryRunner.query(` + UPDATE qf_round_history + SET + "matchingFundAmount" = "matchingFund", + "matchingFundPriceUsd" = 1, + "matchingFundCurrency" = 'DAI' + WHERE + id = 4 AND "matchingFund" IS NOT NULL; + + `); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "qf_round_history" + DROP COLUMN "matchingFundAmount", + DROP COLUMN "matchingFundPriceUsd", + DROP COLUMN "matchingFundCurrency"; + `); + + await queryRunner.query(` + -- If you added a foreign key constraint, remove it first + ALTER TABLE "donation" + DROP CONSTRAINT IF EXISTS "FK_donation_qfRound"; + + ALTER TABLE "donation" + DROP COLUMN "distributedFundQfRoundId"; + `); + } +} diff --git a/migration/1701756190381-create_donationeth_user.ts b/migration/1701756190381-create_donationeth_user.ts new file mode 100644 index 000000000..a8ace6cd5 --- /dev/null +++ b/migration/1701756190381-create_donationeth_user.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +const matchingFundDonationsFromAddress = + (process.env.MATCHING_FUND_DONATIONS_FROM_ADDRESS as string) || + '0x6e8873085530406995170Da467010565968C7C62'; // Address behind donation.eth ENS address; + +export class createDonationethUser1701756190381 implements MigrationInterface { + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO public."user" ("walletAddress", "name", "loginType", "role") + VALUES ('${matchingFundDonationsFromAddress}', 'Donation.eth', 'wallet', 'restricted') + ON CONFLICT ("walletAddress") DO NOTHING + `); + } + + async down(queryRunner: QueryRunner): Promise { + if (!matchingFundDonationsFromAddress) { + throw new Error('Wallet address is not defined in the configuration.'); + } + + await queryRunner.query( + `DELETE FROM public."user" WHERE "walletAddress" = '${matchingFundDonationsFromAddress}'`, + ); + } +} diff --git a/src/entities/donation.ts b/src/entities/donation.ts index ae1b5c09a..d9c11d04f 100644 --- a/src/entities/donation.ts +++ b/src/entities/donation.ts @@ -173,6 +173,15 @@ export class Donation extends BaseEntity { @Column({ nullable: true }) qfRoundId: number; + @Index() + @Field(type => QfRound, { nullable: true }) + @ManyToOne(type => QfRound, { eager: true }) + distributedFundQfRound: QfRound; + + @RelationId((donation: Donation) => donation.distributedFundQfRound) + @Column({ nullable: true }) + distributedFundQfRoundId: number; + @Index() @Field(type => User, { nullable: true }) @ManyToOne(type => User, { eager: true, nullable: true }) diff --git a/src/entities/qfRoundHistory.ts b/src/entities/qfRoundHistory.ts index 7dbefb969..2e9c57606 100644 --- a/src/entities/qfRoundHistory.ts +++ b/src/entities/qfRoundHistory.ts @@ -59,10 +59,23 @@ export class QfRoundHistory extends BaseEntity { @Column({ type: 'real', nullable: true, default: 0 }) raisedFundInUsd: number; + // usd value of matching fund @Field(type => Float, { nullable: true }) @Column({ type: 'real', nullable: true, default: 0 }) matchingFund: number; + @Field(type => Float, { nullable: true }) + @Column({ type: 'real', nullable: true }) + matchingFundAmount?: number; + + @Field(type => Float, { nullable: true }) + @Column({ type: 'real', nullable: true }) + matchingFundPriceUsd?: number; + + @Field(type => String, { nullable: true }) + @Column({ nullable: true }) + matchingFundCurrency?: string; + @Field(type => String, { nullable: true }) @Column({ nullable: true }) distributedFundTxHash: string; diff --git a/src/repositories/qfRoundHistoryRepository.test.ts b/src/repositories/qfRoundHistoryRepository.test.ts index da2899077..244142bc4 100644 --- a/src/repositories/qfRoundHistoryRepository.test.ts +++ b/src/repositories/qfRoundHistoryRepository.test.ts @@ -2,28 +2,27 @@ import { createDonationData, createProjectData, generateRandomEtheriumAddress, + generateRandomTxHash, saveDonationDirectlyToDb, saveProjectDirectlyToDb, saveUserDirectlyToDb, } from '../../test/testUtils'; import { QfRound } from '../entities/qfRound'; import { assert, expect } from 'chai'; -import { - getProjectDonationsSqrtRootSum, - getQfRoundTotalProjectsDonationsSum, -} from './qfRoundRepository'; import { Project } from '../entities/project'; import moment from 'moment'; -import { - refreshProjectDonationSummaryView, - refreshProjectEstimatedMatchingView, -} from '../services/projectViewsService'; + import { fillQfRoundHistory, + getQfRoundHistoriesThatDontHaveRelatedDonations, getQfRoundHistory, } from './qfRoundHistoryRepository'; describe('fillQfRoundHistory test cases', fillQfRoundHistoryTestCases); +describe( + 'getQfRoundHistoriesThatDontHaveRelatedDonations test cases', + getQfRoundHistoriesThatDontHaveRelatedDonationsTestCases, +); // TODO need to have more test cases for this conditions: // when there is no donation for a project @@ -326,3 +325,172 @@ function fillQfRoundHistoryTestCases() { assert.equal(foundQfRoundHistory?.raisedFundInUsd, 10); }); } + +function getQfRoundHistoriesThatDontHaveRelatedDonationsTestCases() { + let qfRound: QfRound; + let firstProject: Project; + let secondProject: Project; + beforeEach(async () => { + await QfRound.update({}, { isActive: false }); + qfRound = QfRound.create({ + isActive: true, + name: 'test', + allocatedFund: 100, + minimumPassportScore: 8, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }); + await qfRound.save(); + firstProject = await saveProjectDirectlyToDb(createProjectData()); + secondProject = await saveProjectDirectlyToDb(createProjectData()); + + firstProject.qfRounds = [qfRound]; + secondProject.qfRounds = [qfRound]; + + await firstProject.save(); + await secondProject.save(); + }); + + afterEach(async () => { + qfRound.isActive = false; + await qfRound.save(); + }); + + it('should return correct value for single project', async () => { + const inCompleteQfRoundHistories = + await getQfRoundHistoriesThatDontHaveRelatedDonations(); + const usersDonations: number[][] = [ + [1, 3], // 4 + [2, 23], // 25 + [3, 97], // 100 + ]; + + await Promise.all( + usersDonations.map(async valuesUsd => { + const user = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + user.passportScore = 10; + await user.save(); + return Promise.all( + valuesUsd.map(valueUsd => { + return saveDonationDirectlyToDb( + { + ...createDonationData(), + valueUsd, + qfRoundId: qfRound.id, + status: 'verified', + }, + user.id, + firstProject.id, + ); + }), + ); + }), + ); + + // if want to fill history round end date should be passed and be inactive + qfRound.endDate = moment().subtract(1, 'days').toDate(); + qfRound.isActive = false; + await qfRound.save(); + + await fillQfRoundHistory(); + const qfRoundHistory = await getQfRoundHistory({ + projectId: firstProject.id, + qfRoundId: qfRound.id, + }); + assert.isNotNull(qfRoundHistory); + qfRoundHistory!.distributedFundTxHash = generateRandomTxHash(); + qfRoundHistory!.distributedFundNetwork = '100'; + qfRoundHistory!.matchingFundAmount = 1000; + qfRoundHistory!.matchingFundCurrency = 'DAI'; + qfRoundHistory!.matchingFund = 1000; + await qfRoundHistory?.save(); + + const inCompleteQfRoundHistories2 = + await getQfRoundHistoriesThatDontHaveRelatedDonations(); + assert.equal( + inCompleteQfRoundHistories2.length - inCompleteQfRoundHistories.length, + 1, + ); + }); + it('should return correct value for two projects', async () => { + const usersDonations: number[][] = [ + [1, 3], // 4 + [2, 23], // 25 + [3, 97], // 100 + ]; + + await Promise.all( + usersDonations.map(async valuesUsd => { + const user = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + user.passportScore = 10; + await user.save(); + return Promise.all( + valuesUsd.map(async valueUsd => { + await saveDonationDirectlyToDb( + { + ...createDonationData(), + valueUsd, + qfRoundId: qfRound.id, + status: 'verified', + }, + user.id, + firstProject.id, + ); + await saveDonationDirectlyToDb( + { + ...createDonationData(), + valueUsd, + qfRoundId: qfRound.id, + status: 'verified', + }, + user.id, + secondProject.id, + ); + }), + ); + }), + ); + + // if want to fill history round end date should be passed and be inactive + qfRound.endDate = moment().subtract(1, 'days').toDate(); + qfRound.isActive = false; + await qfRound.save(); + const inCompleteQfRoundHistories = + await getQfRoundHistoriesThatDontHaveRelatedDonations(); + + await fillQfRoundHistory(); + const qfRoundHistory = await getQfRoundHistory({ + projectId: firstProject.id, + qfRoundId: qfRound.id, + }); + assert.isNotNull(qfRoundHistory); + qfRoundHistory!.distributedFundTxHash = generateRandomTxHash(); + qfRoundHistory!.distributedFundNetwork = '100'; + qfRoundHistory!.matchingFundAmount = 1000; + qfRoundHistory!.matchingFundCurrency = 'DAI'; + qfRoundHistory!.matchingFund = 1000; + await qfRoundHistory?.save(); + + const qfRoundHistory2 = await getQfRoundHistory({ + projectId: secondProject.id, + qfRoundId: qfRound.id, + }); + assert.isNotNull(qfRoundHistory); + qfRoundHistory2!.distributedFundTxHash = generateRandomTxHash(); + qfRoundHistory2!.distributedFundNetwork = '100'; + qfRoundHistory2!.matchingFundAmount = 1000; + qfRoundHistory2!.matchingFundCurrency = 'DAI'; + qfRoundHistory2!.matchingFund = 1000; + await qfRoundHistory2?.save(); + const inCompleteQfRoundHistories2 = + await getQfRoundHistoriesThatDontHaveRelatedDonations(); + assert.equal( + inCompleteQfRoundHistories2.length - inCompleteQfRoundHistories.length, + 2, + ); + }); +} diff --git a/src/repositories/qfRoundHistoryRepository.ts b/src/repositories/qfRoundHistoryRepository.ts index 2e83a5d86..ff02057b2 100644 --- a/src/repositories/qfRoundHistoryRepository.ts +++ b/src/repositories/qfRoundHistoryRepository.ts @@ -38,3 +38,24 @@ export const getQfRoundHistory = async (params: { const { projectId, qfRoundId } = params; return QfRoundHistory.findOne({ where: { projectId, qfRoundId } }); }; + +export const getQfRoundHistoriesThatDontHaveRelatedDonations = + async (): Promise => { + try { + return QfRoundHistory.createQueryBuilder('q') + .innerJoin('qf_round', 'qr', 'qr.id = q.qfRoundId') + .innerJoin('project', 'p', 'p.id = q.projectId') + .leftJoin( + 'donation', + 'd', + 'q.distributedFundTxHash = d.transactionId AND q.projectId = d.projectId AND d.distributedFundQfRoundId IS NOT NULL', + ) + .where( + 'd.id IS NULL AND q.matchingFund IS NOT NULL AND q.matchingFund != 0', + ) + .getMany(); + } catch (e) { + logger.error('getQfRoundHistoriesThatDontHaveRelatedDonations error', e); + throw e; + } + }; diff --git a/src/server/adminJs/tabs/qfRoundHistoryTab.ts b/src/server/adminJs/tabs/qfRoundHistoryTab.ts index 58415e690..54fd5e413 100644 --- a/src/server/adminJs/tabs/qfRoundHistoryTab.ts +++ b/src/server/adminJs/tabs/qfRoundHistoryTab.ts @@ -11,6 +11,7 @@ import { } from '../adminJs-types'; import { fillQfRoundHistory } from '../../../repositories/qfRoundHistoryRepository'; +import { insertDonationsFromQfRoundHistory } from '../../../services/donationService'; export const updateQfRoundHistory = async ( _request: AdminJsRequestInterface, @@ -27,6 +28,21 @@ export const updateQfRoundHistory = async ( }, }; }; +export const CreateRelatedDonationsForQfRoundHistoryRecords = async ( + _request: AdminJsRequestInterface, + _response, + _context: AdminJsContextInterface, +) => { + await insertDonationsFromQfRoundHistory(); + return { + redirectUrl: '/admin/resources/QfRoundHistory', + record: {}, + notice: { + message: `Related donations for qfRoundHistory has been added`, + type: 'success', + }, + }; +}; export const qfRoundHistoryTab = { resource: QfRoundHistory, @@ -130,6 +146,17 @@ export const qfRoundHistoryTab = { handler: updateQfRoundHistory, component: false, }, + RelateDonationsWithDistributedFunds: { + actionType: 'resource', + isVisible: true, + isAccessible: ({ currentAdmin }) => + canAccessQfRoundHistoryAction( + { currentAdmin }, + ResourceActions.UPDATE_QF_ROUND_HISTORIES, + ), + handler: CreateRelatedDonationsForQfRoundHistoryRecords, + component: false, + }, }, }, }; diff --git a/src/services/donationService.test.ts b/src/services/donationService.test.ts index 64f86992c..19ba11a0f 100644 --- a/src/services/donationService.test.ts +++ b/src/services/donationService.test.ts @@ -6,6 +6,7 @@ import { syncDonationStatusWithBlockchainNetwork, updateTotalDonationsOfProject, updateDonationPricesAndValues, + insertDonationsFromQfRoundHistory, } from './donationService'; import { NETWORK_IDS } from '../provider'; import { @@ -13,6 +14,7 @@ import { createProjectData, DONATION_SEED_DATA, generateRandomEtheriumAddress, + generateRandomTxHash, saveDonationDirectlyToDb, saveProjectDirectlyToDb, saveUserDirectlyToDb, @@ -26,7 +28,19 @@ import { errorMessages } from '../utils/errorMessages'; import { findDonationById } from '../repositories/donationRepository'; import { findProjectById } from '../repositories/projectRepository'; import { CHAIN_ID } from '@giveth/monoswap/dist/src/sdk/sdkFactory'; -import { findUserById } from '../repositories/userRepository'; +import { + findUserById, + findUserByWalletAddress, +} from '../repositories/userRepository'; +import { QfRound } from '../entities/qfRound'; +import moment from 'moment'; +import { + fillQfRoundHistory, + getQfRoundHistoriesThatDontHaveRelatedDonations, + getQfRoundHistory, +} from '../repositories/qfRoundHistoryRepository'; +import { logger } from '../utils/logger'; +import { User } from '../entities/user'; describe('isProjectAcceptToken test cases', isProjectAcceptTokenTestCases); describe( @@ -47,6 +61,11 @@ describe( sendSegmentEventForDonationTestCases, ); +describe( + 'insertDonationsFromQfRoundHistory test cases', + insertDonationsFromQfRoundHistoryTestCases, +); + function sendSegmentEventForDonationTestCases() { it('should make segmentNotified true for donation', async () => { const donation = await saveDonationDirectlyToDb( @@ -831,3 +850,158 @@ function fillOldStableCoinDonationsPriceTestCases() { expect(donation.valueUsd).to.gt(0); }); } + +function insertDonationsFromQfRoundHistoryTestCases() { + // We should write lots of test cases to cover all edge cases, now I just has written + // one test case because this task has high priority and I must have doe it soon + let qfRound: QfRound; + let firstProject: Project; + let projectOwner: User; + beforeEach(async () => { + await QfRound.update({}, { isActive: false }); + qfRound = QfRound.create({ + isActive: true, + name: 'test', + allocatedFund: 100, + minimumPassportScore: 8, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }); + await qfRound.save(); + projectOwner = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + firstProject = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + + firstProject.qfRounds = [qfRound]; + + await firstProject.save(); + }); + + afterEach(async () => { + qfRound.isActive = false; + await qfRound.save(); + }); + + it('should return correct value for single project', async () => { + const usersDonations: number[][] = [ + [1, 3], // 4 + [2, 23], // 25 + [3, 97], // 100 + ]; + + await Promise.all( + usersDonations.map(async valuesUsd => { + const user = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + user.passportScore = 10; + await user.save(); + return Promise.all( + valuesUsd.map(valueUsd => { + return saveDonationDirectlyToDb( + { + ...createDonationData(), + valueUsd, + qfRoundId: qfRound.id, + status: 'verified', + }, + user.id, + firstProject.id, + ); + }), + ); + }), + ); + + // if want to fill history round end date should be passed and be inactive + qfRound.endDate = moment().subtract(1, 'days').toDate(); + qfRound.isActive = false; + await qfRound.save(); + + await fillQfRoundHistory(); + const matchingFundFromAddress = process.env + .MATCHING_FUND_DONATIONS_FROM_ADDRESS as string; + const matchingFundFromUser = await findUserByWalletAddress( + matchingFundFromAddress, + ); + assert.isNotNull( + matchingFundFromAddress, + 'Should define MATCHING_FUND_DONATIONS_FROM_ADDRESS in process.env', + ); + const qfRoundHistory = await getQfRoundHistory({ + projectId: firstProject.id, + qfRoundId: qfRound.id, + }); + assert.isNotNull(qfRoundHistory); + qfRoundHistory!.distributedFundTxHash = generateRandomTxHash(); + qfRoundHistory!.distributedFundNetwork = '100'; + qfRoundHistory!.matchingFundAmount = 1000; + qfRoundHistory!.matchingFundCurrency = 'DAI'; + qfRoundHistory!.matchingFund = 1000; + await qfRoundHistory?.save(); + + const inCompleteQfRoundHistories = + await getQfRoundHistoriesThatDontHaveRelatedDonations(); + assert.isNotNull( + inCompleteQfRoundHistories.find( + item => + item.projectId === firstProject.id && item.qfRoundId === qfRound.id, + ), + ); + assert.equal(matchingFundFromUser?.totalDonated, 0); + assert.equal(matchingFundFromUser?.totalReceived, 0); + + await insertDonationsFromQfRoundHistory(); + + const updatedMatchingFundFromUser = await findUserByWalletAddress( + matchingFundFromAddress, + ); + assert.equal( + updatedMatchingFundFromUser?.totalDonated, + qfRoundHistory?.matchingFund, + ); + assert.equal(updatedMatchingFundFromUser?.totalReceived, 0); + + const inCompleteQfRoundHistories2 = + await getQfRoundHistoriesThatDontHaveRelatedDonations(); + assert.isUndefined( + inCompleteQfRoundHistories2.find( + item => + item.projectId === firstProject.id && item.qfRoundId === qfRound.id, + ), + ); + + const donations = await Donation.find({ + where: { + fromWalletAddress: matchingFundFromAddress, + }, + }); + assert.equal(donations.length, 1); + assert.equal(donations[0].distributedFundQfRoundId, qfRound.id); + assert.equal(donations[0].projectId, firstProject.id); + assert.equal(donations[0].valueUsd, qfRoundHistory?.matchingFund); + assert.equal(donations[0].currency, qfRoundHistory?.matchingFundCurrency); + assert.equal(donations[0].amount, qfRoundHistory?.matchingFundAmount); + assert.equal( + donations[0].transactionNetworkId, + Number(qfRoundHistory?.distributedFundNetwork), + ); + assert.equal( + donations[0].transactionId, + qfRoundHistory?.distributedFundTxHash, + ); + + const updatedProject = await findProjectById(firstProject.id); + assert.equal( + updatedProject?.totalDonations, + 4 + 25 + 100 + qfRoundHistory!.matchingFund, + ); + assert.equal( + updatedProject?.adminUser?.totalReceived, + 4 + 25 + 100 + qfRoundHistory!.matchingFund, + ); + }); +} diff --git a/src/services/donationService.ts b/src/services/donationService.ts index 21e941083..5edc689d3 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -3,7 +3,10 @@ import { Token } from '../entities/token'; import { Donation, DONATION_STATUS } from '../entities/donation'; import { TransakOrder } from './transak/order'; import { logger } from '../utils/logger'; -import { findUserById } from '../repositories/userRepository'; +import { + findUserById, + findUserByWalletAddress, +} from '../repositories/userRepository'; import { errorMessages, i18n, @@ -32,6 +35,9 @@ import { import { MonoswapPriceAdapter } from '../adapters/price/MonoswapPriceAdapter'; import { CryptoComparePriceAdapter } from '../adapters/price/CryptoComparePriceAdapter'; import { CoingeckoPriceAdapter } from '../adapters/price/CoingeckoPriceAdapter'; +import { AppDataSource } from '../orm'; +import { getQfRoundHistoriesThatDontHaveRelatedDonations } from '../repositories/qfRoundHistoryRepository'; +import { getPowerRound } from '../repositories/powerRoundRepository'; export const TRANSAK_COMPLETED_STATUS = 'COMPLETED'; @@ -188,7 +194,9 @@ export const updateDonationByTransakData = async ( refreshProjectDonationSummaryView(); }; -export const updateTotalDonationsOfProject = async (projectId: number) => { +export const updateTotalDonationsOfProject = async ( + projectId: number, +): Promise => { try { await Project.query( ` @@ -436,3 +444,87 @@ export const sendSegmentEventForDonation = async (params: { }); } }; + +export const insertDonationsFromQfRoundHistory = async (): Promise => { + const qfRoundHistories = + await getQfRoundHistoriesThatDontHaveRelatedDonations(); + const powerRound = (await getPowerRound())?.round || 1; + if (qfRoundHistories.length === 0) { + logger.debug( + 'insertDonationsFromQfRoundHistory There is not any qfRoundHistories in DB that doesnt have related donation', + ); + return; + } + logger.debug( + `insertDonationsFromQfRoundHistory Filling ${qfRoundHistories.length} qfRoundHistory info ...`, + ); + + const matchingFundFromAddress = + (process.env.MATCHING_FUND_DONATIONS_FROM_ADDRESS as string) || + '0x6e8873085530406995170Da467010565968C7C62'; // Address behind donation.eth ENS address; + const user = await findUserByWalletAddress(matchingFundFromAddress); + if (!user) { + logger.error( + 'insertDonationsFromQfRoundHistory User with walletAddress MATCHING_FUND_DONATIONS_FROM_ADDRESS doesnt exist', + ); + return; + } + await AppDataSource.getDataSource().query(` + INSERT INTO "donation" ( + "transactionId", + "transactionNetworkId", + "status", + "toWalletAddress", + "fromWalletAddress", + "currency", + "amount", + "valueUsd", + "priceUsd", + "powerRound", + "projectId", + "distributedFundQfRoundId", + "segmentNotified", + "userId", + "createdAt" + ) + SELECT + q."distributedFundTxHash", + CAST(q."distributedFundNetwork" AS INTEGER), + 'verified', + pa."address", -- Using address from project_address table + u."walletAddress", + q."matchingFundCurrency", + q."matchingFundAmount", + q."matchingFund", + q."matchingFundPriceUsd", + ${powerRound}, + q."projectId", + q."qfRoundId", + true, -- If we want send email for project owner for these donations we should set this to false + ${user.id}, -- Make sure this substitution is correctly handled in your code + NOW() -- Current timestamp + FROM + "qf_round_history" q + LEFT JOIN "project" p ON q."projectId" = p."id" + LEFT JOIN "user" u ON u."id" = ${user.id} + LEFT JOIN "project_address" pa ON pa."projectId" = p."id" AND pa."networkId" = CAST(q."distributedFundNetwork" AS INTEGER) + WHERE NOT EXISTS ( + SELECT 1 + FROM "donation" d + WHERE + d."transactionId" = q."distributedFundTxHash" AND + d."projectId" = q."projectId" AND + d."distributedFundQfRoundId" = q."qfRoundId" AND + q."matchingFund" IS NOT NULL AND + q."matchingFund" != 0 + ) + + `); + + for (const qfRoundHistory of qfRoundHistories) { + await updateTotalDonationsOfProject(qfRoundHistory.projectId); + const project = await findProjectById(qfRoundHistory.projectId); + await updateUserTotalReceived(project!.adminUser.id); + } + await updateUserTotalDonated(user.id); +}; diff --git a/src/services/qfRoundHistoryService.ts b/src/services/qfRoundHistoryService.ts new file mode 100644 index 000000000..d8b7b3071 --- /dev/null +++ b/src/services/qfRoundHistoryService.ts @@ -0,0 +1,16 @@ +import { AppDataSource } from '../orm'; + +export const getQfRoundHistoriesThatDontHaveRelatedDonations = async () => { + return AppDataSource.getDataSource().query( + ` + SELECT q.* + FROM qf_round_history q + LEFT JOIN donation d + ON q."txHash" = d."txHash" + AND q."projectId" = d."projectId" + AND d.distributedFundQfRoundId IS NOT NULL + WHERE d.id IS NULL; + + `, + ); +}; diff --git a/test/pre-test-scripts.ts b/test/pre-test-scripts.ts index d5f5bb21f..12b86bc20 100644 --- a/test/pre-test-scripts.ts +++ b/test/pre-test-scripts.ts @@ -37,6 +37,7 @@ import { redis } from '../src/redis'; import { logger } from '../src/utils/logger'; import { addCoingeckoIdAndCryptoCompareIdToEtcTokens1697959345387 } from '../migration/1697959345387-addCoingeckoIdAndCryptoCompareIdToEtcTokens'; import { addIsStableCoinFieldToTokenTable1696421249293 } from '../migration/1696421249293-add_isStableCoin_field_to_token_table'; +import { createDonationethUser1701756190381 } from '../migration/1701756190381-create_donationeth_user'; async function seedDb() { await seedUsers(); @@ -408,6 +409,7 @@ async function runMigrations() { await new addCoingeckoIdAndCryptoCompareIdToEtcTokens1697959345387().up( queryRunner, ); + await new createDonationethUser1701756190381().up(queryRunner); } catch (e) { throw e; } finally { From 372f3f3a0a0105b0b4b7cf5916211c5bd11f92ec Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Tue, 5 Dec 2023 16:22:00 +0330 Subject: [PATCH 2/7] Empty commit to trigger CI/CD --- src/services/donationService.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/donationService.test.ts b/src/services/donationService.test.ts index 19ba11a0f..cdfba2042 100644 --- a/src/services/donationService.test.ts +++ b/src/services/donationService.test.ts @@ -773,7 +773,6 @@ function fillOldStableCoinDonationsPriceTestCases() { const project = (await Project.findOne({ where: { id: SEED_DATA.FIRST_PROJECT.id }, })) as Project; - await updateDonationPricesAndValues( donation, project, From 513699686dfc7e4163ccc6f8b213404967bee1c1 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Tue, 5 Dec 2023 16:42:55 +0330 Subject: [PATCH 3/7] Fix query to just add donation for qfRound histories that have full data --- src/repositories/qfRoundHistoryRepository.ts | 10 +++++++--- src/services/donationService.ts | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/repositories/qfRoundHistoryRepository.ts b/src/repositories/qfRoundHistoryRepository.ts index ff02057b2..06257eaac 100644 --- a/src/repositories/qfRoundHistoryRepository.ts +++ b/src/repositories/qfRoundHistoryRepository.ts @@ -50,9 +50,13 @@ export const getQfRoundHistoriesThatDontHaveRelatedDonations = 'd', 'q.distributedFundTxHash = d.transactionId AND q.projectId = d.projectId AND d.distributedFundQfRoundId IS NOT NULL', ) - .where( - 'd.id IS NULL AND q.matchingFund IS NOT NULL AND q.matchingFund != 0', - ) + .where('d.id IS NULL') + .andWhere('q.matchingFund IS NOT NULL') + .andWhere('q.matchingFund != 0') + .andWhere('q.distributedFundTxHash IS NOT NUL') + .andWhere('q.distributedFundNetwork IS NOT NUL') + .andWhere('q.matchingFundCurrency IS NOT NUL') + .andWhere('q.matchingFundAmount IS NOT NUL') .getMany(); } catch (e) { logger.error('getQfRoundHistoriesThatDontHaveRelatedDonations error', e); diff --git a/src/services/donationService.ts b/src/services/donationService.ts index 5edc689d3..3fdbfefb6 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -515,6 +515,10 @@ export const insertDonationsFromQfRoundHistory = async (): Promise => { d."transactionId" = q."distributedFundTxHash" AND d."projectId" = q."projectId" AND d."distributedFundQfRoundId" = q."qfRoundId" AND + q."matchingFundAmount" IS NOT NULL AND + q."matchingFundCurrency" IS NOT NULL AND + q."distributedFundNetwork" IS NOT NULL AND + q."distributedFundTxHash" IS NOT NULL AND q."matchingFund" IS NOT NULL AND q."matchingFund" != 0 ) From 65fa7ef25a20f14e13b8610f90dfdc50dd6c131d Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Tue, 5 Dec 2023 16:59:24 +0330 Subject: [PATCH 4/7] Fix typo error --- src/repositories/qfRoundHistoryRepository.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/repositories/qfRoundHistoryRepository.ts b/src/repositories/qfRoundHistoryRepository.ts index 06257eaac..1b6bef427 100644 --- a/src/repositories/qfRoundHistoryRepository.ts +++ b/src/repositories/qfRoundHistoryRepository.ts @@ -53,10 +53,10 @@ export const getQfRoundHistoriesThatDontHaveRelatedDonations = .where('d.id IS NULL') .andWhere('q.matchingFund IS NOT NULL') .andWhere('q.matchingFund != 0') - .andWhere('q.distributedFundTxHash IS NOT NUL') - .andWhere('q.distributedFundNetwork IS NOT NUL') - .andWhere('q.matchingFundCurrency IS NOT NUL') - .andWhere('q.matchingFundAmount IS NOT NUL') + .andWhere('q.distributedFundTxHash IS NOT NULL') + .andWhere('q.distributedFundNetwork IS NOT NULL') + .andWhere('q.matchingFundCurrency IS NOT NULL') + .andWhere('q.matchingFundAmount IS NOT NULL') .getMany(); } catch (e) { logger.error('getQfRoundHistoriesThatDontHaveRelatedDonations error', e); From 16e106c52f38835406a1367d1f09ff2d57f31f69 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Tue, 5 Dec 2023 17:38:36 +0330 Subject: [PATCH 5/7] Fix insertDonationsFromQfRoundHistory query --- src/services/donationService.ts | 45 ++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/services/donationService.ts b/src/services/donationService.ts index 3fdbfefb6..7e80c6eb9 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -491,44 +491,47 @@ export const insertDonationsFromQfRoundHistory = async (): Promise => { q."distributedFundTxHash", CAST(q."distributedFundNetwork" AS INTEGER), 'verified', - pa."address", -- Using address from project_address table + pa."address", u."walletAddress", q."matchingFundCurrency", q."matchingFundAmount", q."matchingFund", q."matchingFundPriceUsd", - ${powerRound}, + ${powerRound}, q."projectId", q."qfRoundId", - true, -- If we want send email for project owner for these donations we should set this to false - ${user.id}, -- Make sure this substitution is correctly handled in your code - NOW() -- Current timestamp + true, + ${user.id}, + NOW() FROM "qf_round_history" q LEFT JOIN "project" p ON q."projectId" = p."id" LEFT JOIN "user" u ON u."id" = ${user.id} LEFT JOIN "project_address" pa ON pa."projectId" = p."id" AND pa."networkId" = CAST(q."distributedFundNetwork" AS INTEGER) - WHERE NOT EXISTS ( - SELECT 1 - FROM "donation" d - WHERE - d."transactionId" = q."distributedFundTxHash" AND - d."projectId" = q."projectId" AND - d."distributedFundQfRoundId" = q."qfRoundId" AND - q."matchingFundAmount" IS NOT NULL AND - q."matchingFundCurrency" IS NOT NULL AND - q."distributedFundNetwork" IS NOT NULL AND - q."distributedFundTxHash" IS NOT NULL AND - q."matchingFund" IS NOT NULL AND - q."matchingFund" != 0 - ) - + WHERE + q."distributedFundTxHash" IS NOT NULL AND + q."matchingFundAmount" IS NOT NULL AND + q."matchingFundCurrency" IS NOT NULL AND + q."distributedFundNetwork" IS NOT NULL AND + q."distributedFundTxHash" IS NOT NULL AND + q."matchingFund" IS NOT NULL AND + q."matchingFund" != 0 AND + NOT EXISTS ( + SELECT 1 + FROM "donation" d + WHERE + d."transactionId" = q."distributedFundTxHash" AND + d."projectId" = q."projectId" AND + d."distributedFundQfRoundId" = q."qfRoundId" + ) `); for (const qfRoundHistory of qfRoundHistories) { await updateTotalDonationsOfProject(qfRoundHistory.projectId); const project = await findProjectById(qfRoundHistory.projectId); - await updateUserTotalReceived(project!.adminUser.id); + if (project) { + await updateUserTotalReceived(project.adminUser.id); + } } await updateUserTotalDonated(user.id); }; From 78be225d985717422379b255282844e7fad0b18a Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Tue, 5 Dec 2023 17:51:36 +0330 Subject: [PATCH 6/7] Move ens address to constant parameter --- migration/1701756190381-create_donationeth_user.ts | 4 ++-- src/services/donationService.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/migration/1701756190381-create_donationeth_user.ts b/migration/1701756190381-create_donationeth_user.ts index a8ace6cd5..4c43ba553 100644 --- a/migration/1701756190381-create_donationeth_user.ts +++ b/migration/1701756190381-create_donationeth_user.ts @@ -1,8 +1,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; - +const donationDotEthAddress = '0x6e8873085530406995170Da467010565968C7C62'; // Address behind donation.eth ENS address; const matchingFundDonationsFromAddress = (process.env.MATCHING_FUND_DONATIONS_FROM_ADDRESS as string) || - '0x6e8873085530406995170Da467010565968C7C62'; // Address behind donation.eth ENS address; + donationDotEthAddress; export class createDonationethUser1701756190381 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { diff --git a/src/services/donationService.ts b/src/services/donationService.ts index 7e80c6eb9..2d333c95a 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -448,6 +448,7 @@ export const sendSegmentEventForDonation = async (params: { export const insertDonationsFromQfRoundHistory = async (): Promise => { const qfRoundHistories = await getQfRoundHistoriesThatDontHaveRelatedDonations(); + const donationDotEthAddress = '0x6e8873085530406995170Da467010565968C7C62'; // Address behind donation.eth ENS address; const powerRound = (await getPowerRound())?.round || 1; if (qfRoundHistories.length === 0) { logger.debug( @@ -461,7 +462,7 @@ export const insertDonationsFromQfRoundHistory = async (): Promise => { const matchingFundFromAddress = (process.env.MATCHING_FUND_DONATIONS_FROM_ADDRESS as string) || - '0x6e8873085530406995170Da467010565968C7C62'; // Address behind donation.eth ENS address; + donationDotEthAddress; const user = await findUserByWalletAddress(matchingFundFromAddress); if (!user) { logger.error( From 5de022593c393428ac331365b50e111f37b1d403 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Tue, 5 Dec 2023 18:08:49 +0330 Subject: [PATCH 7/7] Fix integration tests for creating donations with matchingFund data --- src/services/donationService.test.ts | 10 ++++++---- src/services/donationService.ts | 1 - 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/services/donationService.test.ts b/src/services/donationService.test.ts index cdfba2042..b17ca5c72 100644 --- a/src/services/donationService.test.ts +++ b/src/services/donationService.test.ts @@ -950,8 +950,6 @@ function insertDonationsFromQfRoundHistoryTestCases() { item.projectId === firstProject.id && item.qfRoundId === qfRound.id, ), ); - assert.equal(matchingFundFromUser?.totalDonated, 0); - assert.equal(matchingFundFromUser?.totalReceived, 0); await insertDonationsFromQfRoundHistory(); @@ -960,9 +958,13 @@ function insertDonationsFromQfRoundHistoryTestCases() { ); assert.equal( updatedMatchingFundFromUser?.totalDonated, - qfRoundHistory?.matchingFund, + (Number(matchingFundFromUser?.totalDonated) as number) + + Number(qfRoundHistory?.matchingFund), + ); + assert.equal( + updatedMatchingFundFromUser?.totalReceived, + matchingFundFromUser?.totalReceived, ); - assert.equal(updatedMatchingFundFromUser?.totalReceived, 0); const inCompleteQfRoundHistories2 = await getQfRoundHistoriesThatDontHaveRelatedDonations(); diff --git a/src/services/donationService.ts b/src/services/donationService.ts index 2d333c95a..4ca6d4d85 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -514,7 +514,6 @@ export const insertDonationsFromQfRoundHistory = async (): Promise => { q."matchingFundAmount" IS NOT NULL AND q."matchingFundCurrency" IS NOT NULL AND q."distributedFundNetwork" IS NOT NULL AND - q."distributedFundTxHash" IS NOT NULL AND q."matchingFund" IS NOT NULL AND q."matchingFund" != 0 AND NOT EXISTS (