diff --git a/config/test.env b/config/test.env index 0ccf9a7ad..4164b28ef 100644 --- a/config/test.env +++ b/config/test.env @@ -214,7 +214,7 @@ ZKEVM_MAINET_SCAN_API_KEY=0000000000000000000000000000000000 ZKEVM_CARDONA_SCAN_API_URL=https://api-cardona-zkevm.polygonscan.com/api ZKEVM_CARDONA_SCAN_API_KEY=0000000000000000000000000000000000 # ZKEVM MAINNET we should fill it as Infura doesnt support polygon zkevm, I found this rpc link from https://chainlist.org/chain/1101 -ZKEVM_MAINNET_NODE_HTTP_URL=https://polygon-zkevm.drpc.org +ZKEVM_MAINNET_NODE_HTTP_URL=https://zkevm-rpc.com # ZKEVM CARDONA we should fill it as Infura doesnt support polygon zkevm, I found this rpc link from https://chainlist.org/chain/2442 ZKEVM_CARDONA_NODE_HTTP_URL=https://rpc.cardona.zkevm-rpc.com @@ -230,4 +230,9 @@ PRIVADO_VERIFIER_NETWORK_ID=2442 INVERTER_GRAPHQL_ENDPOINT=https://indexer.bigdevenergy.link/a414bf3/v1/graphql -QACC_NETWORK_ID=11155420 \ No newline at end of file +QACC_NETWORK_ID=1101 + +QACC_DONATION_TOKEN_ADDRESS=0x22B21BedDef74FE62F031D2c5c8F7a9F8a4b304D +QACC_DONATION_TOKEN_DECIMALS=18 +QACC_DONATION_TOKEN_NAME=Polygon Ecosystem Token +QACC_DONATION_TOKEN_SYMBOL=POL \ No newline at end of file diff --git a/package.json b/package.json index 8d81f799a..5a2758df0 100644 --- a/package.json +++ b/package.json @@ -188,6 +188,7 @@ "start:test": "NODE_ENV=development ts-node-dev --project ./tsconfig.json --respawn ./test.ts", "serve": "pm2 startOrRestart ecosystem.config.js --node-args='--max-old-space-size=8192'", "db:migrate:run:test": "NODE_ENV=test npx typeorm-ts-node-commonjs migration:run -d ./src/ormconfig.ts", + "db:migrate:create:test": "NODE_ENV=test npx typeorm-ts-node-commonjs migration:generate migration/$MIGRATION_NAME -d ./src/ormconfig.ts", "db:migrate:revert:test": "NODE_ENV=test npx typeorm-ts-node-commonjs migration:revert -d ./src/ormconfig.ts", "db:migrate:run:local": "NODE_ENV=development npx typeorm-ts-node-commonjs migration:run -d ./src/ormconfig.ts", "db:migrate:create:local": "NODE_ENV=development npx typeorm-ts-node-commonjs migration:generate migration/$MIGRATION_NAME -d ./src/ormconfig.ts", diff --git a/src/entities/donation.ts b/src/entities/donation.ts index 190acda1b..56ebc15ff 100644 --- a/src/entities/donation.ts +++ b/src/entities/donation.ts @@ -166,11 +166,11 @@ export class Donation extends BaseEntity { @Index() @Field(_type => QfRound, { nullable: true }) @ManyToOne(_type => QfRound, { eager: true }) - qfRound: QfRound; + qfRound?: QfRound | null; @RelationId((donation: Donation) => donation.qfRound) @Column({ nullable: true }) - qfRoundId: number; + qfRoundId: number | null; @Index() @Field(_type => QfRound, { nullable: true }) @@ -267,7 +267,7 @@ export class Donation extends BaseEntity { @RelationId((donation: Donation) => donation.earlyAccessRound) @Column({ nullable: true }) - earlyAccessRoundId: number; + earlyAccessRoundId: number | null; @Field({ nullable: true }) @Column({ type: 'float', nullable: true }) diff --git a/src/repositories/donationRepository.test.ts b/src/repositories/donationRepository.test.ts index e108539f4..5a066ccb3 100644 --- a/src/repositories/donationRepository.test.ts +++ b/src/repositories/donationRepository.test.ts @@ -357,7 +357,7 @@ function findDonationByIdTestCases() { assert.equal(fetchedDonation?.id, donation.id); assert.isOk(fetchedDonation?.project); assert.equal(fetchedDonation?.project.id, project.id); - assert.equal(fetchedDonation?.qfRound.id, qfRound.id); + assert.equal(fetchedDonation?.qfRound?.id, qfRound.id); }); it('should not return donation with invalid id ', async () => { const fetchedDonation = await findDonationById(10000000); diff --git a/src/repositories/earlyAccessRoundRepository.ts b/src/repositories/earlyAccessRoundRepository.ts index 7a6519576..f9ab989c4 100644 --- a/src/repositories/earlyAccessRoundRepository.ts +++ b/src/repositories/earlyAccessRoundRepository.ts @@ -23,12 +23,16 @@ export const findAllEarlyAccessRounds = async (): Promise< // Find the currently active Early Access Round export const findActiveEarlyAccessRound = async ( - currentDate = new Date(), + date = new Date(), ): Promise => { try { const query = EarlyAccessRound.createQueryBuilder('earlyAccessRound') - .where('earlyAccessRound.startDate <= :currentDate', { currentDate }) - .andWhere('earlyAccessRound.endDate >= :currentDate', { currentDate }); + .where('earlyAccessRound.startDate <= :date', { + date, + }) + .andWhere('earlyAccessRound.endDate >= :date', { + date, + }); return query.getOne(); } catch (error) { diff --git a/src/repositories/projectRoundRecordRepository.ts b/src/repositories/projectRoundRecordRepository.ts index 90db761d9..3ad5aefa1 100644 --- a/src/repositories/projectRoundRecordRepository.ts +++ b/src/repositories/projectRoundRecordRepository.ts @@ -17,9 +17,10 @@ export async function updateOrCreateProjectRoundRecord( projectId: number, qfRoundId?: number | null, earlyAccessRoundId?: number | null, -): Promise { +): Promise { if (!qfRoundId && !earlyAccessRoundId) { - throw new Error('No round specified on updateOrCreateProjectRoundRecord'); + return null; + // throw new Error('No round specified on updateOrCreateProjectRoundRecord'); } try { let query = Donation.createQueryBuilder('donation') diff --git a/src/repositories/qfRoundRepository.ts b/src/repositories/qfRoundRepository.ts index 89b561f50..1253ee5d5 100644 --- a/src/repositories/qfRoundRepository.ts +++ b/src/repositories/qfRoundRepository.ts @@ -168,12 +168,18 @@ export const findArchivedQfRounds = async ( return fullRounds.slice(skip, skip + limit); }; -export const findActiveQfRound = async ( - noCache?: boolean, -): Promise => { +export const findActiveQfRound = async ({ + noCache = false, + date = new Date(), +}: { + noCache?: boolean; + date?: Date; +} = {}): Promise => { const query = QfRound.createQueryBuilder('qfRound') .where('"isActive" = true') - .andWhere('NOW() BETWEEN "qfRound"."beginDate" AND "qfRound"."endDate"'); + .andWhere(':date BETWEEN "qfRound"."beginDate" AND "qfRound"."endDate"', { + date, + }); if (noCache) { return query.getOne(); } diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index c4ec506bd..2101880f6 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -61,7 +61,6 @@ import { DRAFT_DONATION_STATUS, DraftDonation, } from '../entities/draftDonation'; -import qacc from '../utils/qacc'; import { QACC_DONATION_TOKEN_SYMBOL } from '../constants/qacc'; import { EarlyAccessRound } from '../entities/earlyAccessRound'; import { ProjectRoundRecord } from '../entities/projectRoundRecord'; @@ -919,7 +918,7 @@ function createDonationTestCases() { firstName: 'first name', }).save(); - const user2 = await User.create({ + await User.create({ walletAddress: referrerWalletAddress, loginType: 'wallet', firstName: 'first name', @@ -957,14 +956,14 @@ function createDonationTestCases() { }, }); // assert.isTrue(donation?.isTokenEligibleForGivback); - assert.equal(donation?.referrerWallet, user2.walletAddress); - assert.isOk(donation?.referralStartTimestamp); + // assert.equal(donation?.referrerWallet, user2.walletAddress); + // assert.isOk(donation?.referralStartTimestamp); assert.isNotOk(donation?.qfRound); // assert.isTrue(donation?.earlyAccessRound); }); it('should create a donation in an active qfRound', async () => { - sinon.stub(qacc, 'isEarlyAccessRound').resolves(false); try { + await EarlyAccessRound.delete({ id: ea.id }); const project = await saveProjectDirectlyToDb(createProjectData()); const qfRound = await QfRound.create({ isActive: true, @@ -1221,8 +1220,8 @@ function createDonationTestCases() { await qfRound.save(); }); it('should create a donation in an active qfRound, when project is not listed', async () => { - sinon.stub(qacc, 'isEarlyAccessRound').resolves(false); try { + await EarlyAccessRound.delete({ id: ea.id }); const project = await saveProjectDirectlyToDb(createProjectData()); const qfRound = await QfRound.create({ isActive: true, @@ -1293,8 +1292,8 @@ function createDonationTestCases() { } }); it('should create a donation in an active qfRound, when project is not verified', async () => { - sinon.stub(qacc, 'isEarlyAccessRound').resolves(false); try { + await EarlyAccessRound.delete({ id: ea.id }); const project = await saveProjectDirectlyToDb(createProjectData()); const qfRound = await QfRound.create({ isActive: true, diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 24d9a638e..dab76ff76 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -38,10 +38,7 @@ import { validateWithJoiSchema, } from '../utils/validators/graphqlQueryValidators'; import { logger } from '../utils/logger'; -import { - findUserById, - setUserAsReferrer, -} from '../repositories/userRepository'; +import { findUserById } from '../repositories/userRepository'; import { donationsNumberPerDateRange, donationsTotalAmountPerDateRange, @@ -60,8 +57,6 @@ import { findProjectRecipientAddressByNetworkId } from '../repositories/projectA import { MainCategory } from '../entities/mainCategory'; import { findProjectById } from '../repositories/projectRepository'; import { AppDataSource } from '../orm'; -import { getChainvineReferralInfoForDonation } from '../services/chainvineReferralService'; -import { relatedActiveQfRoundForProject } from '../services/qfRoundService'; import { detectAddressChainType } from '../utils/networks'; import { ChainType } from '../types/network'; import { getAppropriateNetworkId } from '../services/chains'; @@ -74,6 +69,7 @@ import qacc from '../utils/qacc'; import { findActiveEarlyAccessRound } from '../repositories/earlyAccessRoundRepository'; import { updateOrCreateProjectRoundRecord } from '../repositories/projectRoundRecordRepository'; import { updateOrCreateProjectUserRecord } from '../repositories/projectUserRecordRepository'; +import { findActiveQfRound } from '../repositories/qfRoundRepository'; const draftDonationEnabled = process.env.ENABLE_DRAFT_DONATION === 'true'; @ObjectType() @@ -792,22 +788,6 @@ export class DonationResolver { }); const isCustomToken = !tokenInDb; const isTokenEligibleForGivback = false; - // if (isCustomToken && !project.organization.supportCustomTokens) { - // throw new Error(i18n.__(translationErrorMessagesKeys.TOKEN_NOT_FOUND)); - // } else if (tokenInDb) { - // const acceptsToken = await isTokenAcceptableForProject({ - // projectId, - // tokenId: tokenInDb.id, - // }); - // if (!acceptsToken && !project.organization.supportCustomTokens) { - // throw new Error( - // i18n.__( - // translationErrorMessagesKeys.PROJECT_DOES_NOT_SUPPORT_THIS_TOKEN, - // ), - // ); - // } - // isTokenEligibleForGivback = tokenInDb.isGivbackEligible; - // } const projectRelatedAddress = await findProjectRecipientAddressByNetworkId({ @@ -832,17 +812,17 @@ export class DonationResolver { transactionTx = transactionId?.toLowerCase() as string; } - let donationPercentage = 0; - if (relevantDonationTxHash) { - const relevantDonation = await Donation.findOne({ - where: { transactionId: relevantDonationTxHash }, - }); + // const donationPercentage = 0; + // if (relevantDonationTxHash) { + // const relevantDonation = await Donation.findOne({ + // where: { transactionId: relevantDonationTxHash }, + // }); - if (relevantDonation) { - const totalValue = amount + relevantDonation.amount; - donationPercentage = (amount / totalValue) * 100; - } - } + // if (relevantDonation) { + // const totalValue = amount + relevantDonation.amount; + // donationPercentage = (amount / totalValue) * 100; + // } + // } const donation = Donation.create({ amount: Number(amount), transactionId: transactionTx, @@ -865,68 +845,58 @@ export class DonationResolver { chainType: chainType as ChainType, useDonationBox, relevantDonationTxHash, - donationPercentage, + // donationPercentage, }); - if (referrerId) { - // Fill referrer data if referrerId is valid - try { - const { - referralStartTimestamp, - isReferrerGivbackEligible, - referrerWalletAddress, - } = await getChainvineReferralInfoForDonation({ - referrerId, - fromAddress, - donorUserId: donorUser.id, - projectVerified: project.verified, - }); - donation.isReferrerGivbackEligible = isReferrerGivbackEligible; - donation.referrerWallet = referrerWalletAddress; - donation.referralStartTimestamp = referralStartTimestamp; - - await setUserAsReferrer(referrerWalletAddress); - } catch (e) { - logger.error('get chainvine wallet address error', e); - } - } - if (!(await qacc.isEarlyAccessRound())) { - const activeQfRoundForProject = - await relatedActiveQfRoundForProject(projectId); - if ( - activeQfRoundForProject && - activeQfRoundForProject.isEligibleNetwork(networkId) - ) { - donation.qfRound = activeQfRoundForProject; - } else { - throw new Error( - i18n.__(translationErrorMessagesKeys.ROUND_NOT_FOUND), - ); - } - if (draftDonationEnabled && draftDonationId) { - const draftDonation = await DraftDonation.findOne({ - where: { - id: draftDonationId, - status: DRAFT_DONATION_STATUS.MATCHED, - }, - select: ['matchedDonationId'], - }); - if (draftDonation?.createdAt) { - // Because if we dont set it donation createdAt might be later than tx.time and that will make a problem on verifying donation - // and would fail it - donation.createdAt = draftDonation?.createdAt; - } - if (draftDonation?.matchedDonationId) { - return draftDonation.matchedDonationId; - } - } - await donation.save(); + // if (referrerId) { + // // Fill referrer data if referrerId is valid + // try { + // const { + // referralStartTimestamp, + // isReferrerGivbackEligible, + // referrerWalletAddress, + // } = await getChainvineReferralInfoForDonation({ + // referrerId, + // fromAddress, + // donorUserId: donorUser.id, + // projectVerified: project.verified, + // }); + // donation.isReferrerGivbackEligible = isReferrerGivbackEligible; + // donation.referrerWallet = referrerWalletAddress; + // donation.referralStartTimestamp = referralStartTimestamp; + + // await setUserAsReferrer(referrerWalletAddress); + // } catch (e) { + // logger.error('get chainvine wallet address error', e); + // } + // } + const earlyAccessRound = await findActiveEarlyAccessRound(); + if (!earlyAccessRound) { + donation.qfRound = await findActiveQfRound(); } else { - donation.earlyAccessRound = await findActiveEarlyAccessRound(); - await donation.save(); + donation.earlyAccessRound = earlyAccessRound; } + await donation.save(); let priceChainId; + if (draftDonationEnabled && draftDonationId) { + const draftDonation = await DraftDonation.findOne({ + where: { + id: draftDonationId, + status: DRAFT_DONATION_STATUS.MATCHED, + }, + select: ['matchedDonationId'], + }); + if (draftDonation?.createdAt) { + // Because if we dont set it donation createdAt might be later than tx.time and that will make a problem on verifying donation + // and would fail it + donation.createdAt = draftDonation?.createdAt; + } + if (draftDonation?.matchedDonationId) { + return draftDonation.matchedDonationId; + } + } + switch (transactionNetworkId) { case NETWORK_IDS.ROPSTEN: priceChainId = NETWORK_IDS.MAIN_NET; @@ -952,26 +922,25 @@ export class DonationResolver { priceChainId, ); - await updateOrCreateProjectRoundRecord( - donation.projectId, - donation.qfRoundId, - donation.earlyAccessRoundId, - ); - await updateOrCreateProjectUserRecord({ - projectId: donation.projectId, - userId: donation.userId, - }); - - if (chainType === ChainType.EVM) { - await markDraftDonationStatusMatched({ + await Promise.all([ + updateOrCreateProjectRoundRecord( + donation.projectId, + donation.qfRoundId, + donation.earlyAccessRoundId, + ), + updateOrCreateProjectUserRecord({ + projectId: donation.projectId, + userId: donation.userId, + }), + markDraftDonationStatusMatched({ matchedDonationId: donation.id, fromWalletAddress: fromAddress, toWalletAddress: toAddress, currency: token, amount: Number(amount), networkId, - }); - } + }), + ]); return donation.id; } catch (e) { diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 68bbff4dc..96af011c0 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -2237,7 +2237,7 @@ export class ProjectResolver { qfRoundId, earlyAccessRoundId, ); - return [record]; + return record ? [record] : []; } return records; diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index df8aa470e..53c97a6f5 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -403,7 +403,7 @@ export const addProjectsToQfRound = async ( const projectIds = request?.query?.recordIds ?.split(',') ?.map(strId => Number(strId)) as number[]; - const qfRound = await findActiveQfRound(true); + const qfRound = await findActiveQfRound({ noCache: true }); if (qfRound) { await relateManyProjectsToQfRound({ projectIds, @@ -434,7 +434,7 @@ export const addSingleProjectToQfRound = async ( const { record, currentAdmin } = context; let message = messages.PROJECTS_RELATED_TO_ACTIVE_QF_ROUND_SUCCESSFULLY; const projectId = Number(request?.params?.recordId); - const qfRound = await findActiveQfRound(true); + const qfRound = await findActiveQfRound({ noCache: true }); if (qfRound) { await relateManyProjectsToQfRound({ projectIds: [projectId], diff --git a/src/services/donationService.test.ts b/src/services/donationService.test.ts index 1a2ceab89..99145da85 100644 --- a/src/services/donationService.test.ts +++ b/src/services/donationService.test.ts @@ -1,5 +1,6 @@ import { assert, expect } from 'chai'; import { CHAIN_ID } from '@giveth/monoswap/dist/src/sdk/sdkFactory'; +import sinon from 'sinon'; import moment from 'moment'; import { isTokenAcceptableForProject, @@ -13,6 +14,7 @@ import { createDonationData, createProjectData, DONATION_SEED_DATA, + generateEARoundNumber, generateRandomEtheriumAddress, saveDonationDirectlyToDb, saveProjectDirectlyToDb, @@ -23,7 +25,6 @@ import { Token } from '../entities/token'; import { ORGANIZATION_LABELS } from '../entities/organization'; import { Project } from '../entities/project'; import { Donation, DONATION_STATUS } from '../entities/donation'; -import { errorMessages } from '../utils/errorMessages'; import { findDonationById } from '../repositories/donationRepository'; import { findProjectById } from '../repositories/projectRepository'; import { findUserByWalletAddress } from '../repositories/userRepository'; @@ -36,6 +37,10 @@ import { import { User } from '../entities/user'; import { QfRoundHistory } from '../entities/qfRoundHistory'; import { updateProjectStatistics } from './projectService'; +import { EarlyAccessRound } from '../entities/earlyAccessRound'; +import * as chains from './chains'; +import { ProjectRoundRecord } from '../entities/projectRoundRecord'; +import { ProjectUserRecord } from '../entities/projectUserRecord'; describe('isProjectAcceptToken test cases', isProjectAcceptTokenTestCases); describe( @@ -47,7 +52,7 @@ describe( fillStableCoinDonationsPriceTestCases, ); -describe.skip( +describe.only( 'syncDonationStatusWithBlockchainNetwork test cases', syncDonationStatusWithBlockchainNetworkTestCases, ); @@ -80,116 +85,35 @@ function sendSegmentEventForDonationTestCases() { } function syncDonationStatusWithBlockchainNetworkTestCases() { - it('should verify a Polygon donation', async () => { - // https://polygonscan.com/tx/0x16f122ad45705dfa41bb323c3164b6d840cbb0e9fa8b8e58bd7435370f8bbfc8 - - const amount = 30_900; - - const transactionInfo = { - txHash: - '0x16f122ad45705dfa41bb323c3164b6d840cbb0e9fa8b8e58bd7435370f8bbfc8', - currency: 'MATIC', - networkId: NETWORK_IDS.POLYGON, - fromAddress: '0x9ead03f7136fc6b4bdb0780b00a1c14ae5a8b6d0', - toAddress: '0x4632e0bcf15db3f4663fea1a6dbf666e563598cd', - amount, - timestamp: 1677400082 * 1000, - }; - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ + const amount = 10; + const timestamp = 1706289475 * 1000; + + const transactionInfo = { + txHash: + '0x139504e0868ce12f615c711af95a8c043197cd2d5a9a0a7df85a196d9a1ab07e', + currency: 'POL', + networkId: NETWORK_IDS.ZKEVM_MAINNET, + fromAddress: '0xbdFF5cc1df5ffF6B01C4a8b0B8271328E92742Da', + toAddress: '0x193918F1Cb3e42007d613aaA99912aaeC4230e54', + amount, + timestamp, + }; + let user: User; + let project: Project; + let donation: Donation; + let ea: EarlyAccessRound | undefined; + let qf: QfRound | undefined; + + before(async () => { + user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + project = await saveProjectDirectlyToDb({ ...createProjectData(), walletAddress: transactionInfo.toAddress, }); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 100, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - const updateDonation = await syncDonationStatusWithBlockchainNetwork({ - donationId: donation.id, - }); - assert.isOk(updateDonation); - assert.equal(updateDonation.id, donation.id); - assert.isTrue(updateDonation.segmentNotified); - assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); - }); - - it('should verify a Celo donation', async () => { - // https://celoscan.io/tx/0xa2a282cf6a7dec8b166aa52ac3d00fcd15a370d414615e29a168cfbb592e3637 - - const amount = 0.999; - - const transactionInfo = { - txHash: - '0xa2a282cf6a7dec8b166aa52ac3d00fcd15a370d414615e29a168cfbb592e3637', - currency: 'CELO', - networkId: NETWORK_IDS.CELO, - fromAddress: '0xf6436829cf96ea0f8bc49d300c536fcc4f84c4ed', - toAddress: '0x95b75068b8bc97716a458bedcf4df1cace802c12', - amount, - timestamp: 1680072295 * 1000, - }; - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 100, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - const updateDonation = await syncDonationStatusWithBlockchainNetwork({ - donationId: donation.id, - }); - assert.isOk(updateDonation); - assert.equal(updateDonation.id, donation.id); - assert.isTrue(updateDonation.segmentNotified); - assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); }); - it('should verify a Arbitrum donation', async () => { - // https://arbiscan.io/tx/0xdaca7d68e784a60a6975fa9937abb6b287d7fe992ff806f8c375cb4c3b2152f3 - const amount = 0.0038; - - const transactionInfo = { - txHash: - '0xdaca7d68e784a60a6975fa9937abb6b287d7fe992ff806f8c375cb4c3b2152f3', - currency: 'ETH', - networkId: NETWORK_IDS.ARBITRUM_MAINNET, - fromAddress: '0x015e6fbce5119c32db66e7c544365749bb26cf8b', - toAddress: '0x5c66fef6ea22f37e7c1f7eee49e4e116d3fbfc68', - amount, - timestamp: 1708342629 * 1000, - }; - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const donation = await saveDonationDirectlyToDb( + beforeEach(async () => { + donation = await saveDonationDirectlyToDb( { amount: transactionInfo.amount, transactionNetworkId: transactionInfo.networkId, @@ -205,94 +129,28 @@ function syncDonationStatusWithBlockchainNetworkTestCases() { user.id, project.id, ); - const updateDonation = await syncDonationStatusWithBlockchainNetwork({ - donationId: donation.id, - }); - assert.isOk(updateDonation); - assert.equal(updateDonation.id, donation.id); - assert.isTrue(updateDonation.segmentNotified); - assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); }); - it('should verify a erc20 Arbitrum donation', async () => { - // https://arbiscan.io/tx/0xd7ba5a5d8149432217a161559e357904965620b58e776c4482b8b501e092e495 - - const amount = 999.2; - const transactionInfo = { - txHash: - '0xd7ba5a5d8149432217a161559e357904965620b58e776c4482b8b501e092e495', - currency: 'USDT', - networkId: NETWORK_IDS.ARBITRUM_MAINNET, - fromAddress: '0x62383739d68dd0f844103db8dfb05a7eded5bbe6', - toAddress: '0x513b8c84fb6e36512b641b67de55a18704118fe7', - amount, - timestamp: 1708343905 * 1000, - }; - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 1000, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - const updateDonation = await syncDonationStatusWithBlockchainNetwork({ - donationId: donation.id, - }); - assert.isOk(updateDonation); - assert.equal(updateDonation.id, donation.id); - assert.isTrue(updateDonation.segmentNotified); - assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); + afterEach(async () => { + await Donation.delete({ + id: donation.id, + }); + await ProjectRoundRecord.delete({}); + await ProjectUserRecord.delete({}); + if (ea) { + await ea.remove(); + ea = undefined; + } + if (qf) { + await qf.remove(); + qf = undefined; + } + sinon.restore(); }); - it('should verify a Arbitrum Sepolia donation', async () => { - // https://sepolia.arbiscan.io/tx/0x25f17541ccb7248d931f2a1e11058a51ffb4db4968ed3e1d4a019ddc2d44802c - const amount = 0.0069; + it('should verify a Polygon donation', async () => { + // https://polygonscan.com/tx/0x16f122ad45705dfa41bb323c3164b6d840cbb0e9fa8b8e58bd7435370f8bbfc8 - const transactionInfo = { - txHash: - '0x25f17541ccb7248d931f2a1e11058a51ffb4db4968ed3e1d4a019ddc2d44802c', - currency: 'ETH', - networkId: NETWORK_IDS.ARBITRUM_SEPOLIA, - fromAddress: '0xefc58dbf0e606c327868b55334998aacb27f9ef2', - toAddress: '0xc11c479473cd06618fc75816dd6b56be4ac80efd', - amount, - timestamp: 1708344659 * 1000, - }; - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 100, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); const updateDonation = await syncDonationStatusWithBlockchainNetwork({ donationId: donation.id, }); @@ -301,361 +159,61 @@ function syncDonationStatusWithBlockchainNetworkTestCases() { assert.isTrue(updateDonation.segmentNotified); assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); }); - it('should verify a erc20 Arbitrum Sepolia donation', async () => { - // https://sepolia.arbiscan.io/tx/0x5bcce1bac54ee92ff28e9913e8a002e6e8efc8e8632fdb8e6ebaa16d8c6fd4cb - - const amount = 100; - const transactionInfo = { - txHash: - '0x5bcce1bac54ee92ff28e9913e8a002e6e8efc8e8632fdb8e6ebaa16d8c6fd4cb', - currency: 'cETH', - networkId: NETWORK_IDS.ARBITRUM_SEPOLIA, - fromAddress: '0x6a446d9d0d153aa07811de2ac8096b87baad305b', - toAddress: '0xf888186663aae1600282c6fb23b764a61937b913', - amount, - timestamp: 1708344801 * 1000, - }; - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const donation = await saveDonationDirectlyToDb( + it('should associate donation to overlapping early access round after verification', async () => { + sinon.stub(chains, 'validateTransactionWithInputData'); + ea = await EarlyAccessRound.create({ + roundNumber: generateEARoundNumber(), + startDate: moment(timestamp).subtract(1, 'days').toDate(), + endDate: moment(timestamp).add(3, 'days').toDate(), + roundUSDCapPerProject: 1000000, + roundUSDCapPerUserPerProject: 50000, + tokenPrice: 0.1, + }).save(); + // update donation timestamp to after the early access round end date + await Donation.update( + { id: donation.id }, { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 1000, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, + createdAt: moment(timestamp).add(5, 'days').toDate(), }, - user.id, - project.id, ); - const updateDonation = await syncDonationStatusWithBlockchainNetwork({ - donationId: donation.id, - }); - assert.isOk(updateDonation); - assert.equal(updateDonation.id, donation.id); - assert.isTrue(updateDonation.segmentNotified); - assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); - }); - it('should verify a Optimistic donation', async () => { - // https://optimistic.etherscan.io/tx/0xc645bd4ebcb1cb249be4b3e4dad46075c973fd30649a39f27f5328ded15074e7 - - const amount = 0.001; - const transactionInfo = { - txHash: - '0xc645bd4ebcb1cb249be4b3e4dad46075c973fd30649a39f27f5328ded15074e7', - currency: 'ETH', - networkId: NETWORK_IDS.OPTIMISTIC, - fromAddress: '0xf23ea0b5f14afcbe532a1df273f7b233ebe41c78', - toAddress: '0xf23ea0b5f14afcbe532a1df273f7b233ebe41c78', - amount, - timestamp: 1679484540 * 1000, - }; - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 1.79, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); const updateDonation = await syncDonationStatusWithBlockchainNetwork({ donationId: donation.id, }); assert.isOk(updateDonation); assert.equal(updateDonation.id, donation.id); assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); - assert.isTrue(updateDonation.segmentNotified); + assert.equal(updateDonation.earlyAccessRoundId, ea.id); }); - it('should verify a Optimism Sepolia donation', async () => { - // https://sepolia-optimism.etherscan.io/tx/0x1b4e9489154a499cd7d0bd7a097e80758e671a32f98559be3b732553afb00809 - const amount = 0.01; + it('should associate donation to overlapping qf round after verification', async () => { + sinon.stub(chains, 'validateTransactionWithInputData'); + qf = await QfRound.create({ + isActive: true, + name: new Date().toString(), + minimumPassportScore: 8, + slug: new Date().getTime().toString(), + allocatedFund: 100, + beginDate: moment(timestamp).subtract(1, 'second'), + endDate: moment(timestamp).add(2, 'day'), + }).save(); - const transactionInfo = { - txHash: - '0x1b4e9489154a499cd7d0bd7a097e80758e671a32f98559be3b732553afb00809', - currency: 'ETH', - networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, - fromAddress: '0x625bcc1142e97796173104a6e817ee46c593b3c5', - toAddress: '0x73f9b3f48ebc96ac55cb76c11053b068669a8a67', - amount, - timestamp: 1708954960 * 1000, - }; - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const donation = await saveDonationDirectlyToDb( + // update donation timestamp to after the qf round end date + await Donation.update( + { id: donation.id }, { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 20.73, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, + createdAt: moment(timestamp).add(5, 'days').toDate(), }, - user.id, - project.id, ); - const updateDonation = await syncDonationStatusWithBlockchainNetwork({ - donationId: donation.id, - }); - assert.isOk(updateDonation); - assert.equal(updateDonation.id, donation.id); - assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); - assert.isTrue(updateDonation.segmentNotified); - }); - - // it('should verify a mainnet donation', async () => { - // // https://etherscan.io/tx/0x37765af1a7924fb6ee22c83668e55719c9ecb1b79928bd4b208c42dfff44da3a - // const transactionInfo = { - // txHash: - // '0x37765af1a7924fb6ee22c83668e55719c9ecb1b79928bd4b208c42dfff44da3a', - // currency: 'ETH', - // networkId: NETWORK_IDS.MAIN_NET, - // fromAddress: '0x839395e20bbB182fa440d08F850E6c7A8f6F0780', - // toAddress: '0x5ac583feb2b1f288c0a51d6cdca2e8c814bfe93b', - // timestamp: 1607360947 * 1000, - // amount: 0.04, - // }; - - // const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - // const project = await saveProjectDirectlyToDb({ - // ...createProjectData(), - // walletAddress: transactionInfo.toAddress, - // }); - // const donation = await saveDonationDirectlyToDb( - // { - // amount: transactionInfo.amount, - // transactionNetworkId: transactionInfo.networkId, - // transactionId: transactionInfo.txHash, - // currency: transactionInfo.currency, - // fromWalletAddress: transactionInfo.fromAddress, - // toWalletAddress: transactionInfo.toAddress, - // valueUsd: 100, - // anonymous: false, - // createdAt: new Date(transactionInfo.timestamp), - // status: DONATION_STATUS.PENDING, - // }, - // user.id, - // project.id, - // ); - // const updateDonation = await syncDonationStatusWithBlockchainNetwork({ - // donationId: donation.id, - // }); - // assert.isOk(updateDonation); - // assert.equal(updateDonation.id, donation.id); - // assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); - // assert.isTrue(updateDonation.segmentNotified); - // }); - it('should verify a gnosis donation', async () => { - // https://blockscout.com/xdai/mainnet/tx/0x57b913ac40b2027a08655bdb495befc50612b72a9dd1f2be81249c970503c734 - - const transactionInfo = { - txHash: - '0x57b913ac40b2027a08655bdb495befc50612b72a9dd1f2be81249c970503c734', - currency: 'XDAI', - networkId: NETWORK_IDS.XDAI, - fromAddress: '0xb20a327c9b4da091f454b1ce0e2e4dc5c128b5b4', - toAddress: '0x7ee789b7e6fa20eab7ecbce44626afa7f58a94b7', - amount: 0.001, - timestamp: 1621241124 * 1000, - }; - - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 100, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); const updateDonation = await syncDonationStatusWithBlockchainNetwork({ donationId: donation.id, }); assert.isOk(updateDonation); assert.equal(updateDonation.id, donation.id); assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); - assert.isTrue(updateDonation.segmentNotified); - }); - - it('should change status to failed when donation fromAddress is different with transaction fromAddress', async () => { - // https://blockscout.com/xdai/mainnet/tx/0x99e70642fe1aa03cb2db35c3e3909466e66b233840b7b1e0dd47296c878c16b4 - - const transactionInfo = { - txHash: - '0x99e70642fe1aa03cb2db35c3e3909466e66b233840b7b1e0dd47296c878c16b4', - currency: 'HNY', - networkId: NETWORK_IDS.XDAI, - fromAddress: '0x826976d7c600d45fb8287ca1d7c76fc8eb732000', - toAddress: '0x5A5a0732c1231D99DB8FFcA38DbEf1c8316fD3E1', - amount: 0.001, - timestamp: 1617903449 * 1000, - }; - - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 100, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - const updateDonation = await syncDonationStatusWithBlockchainNetwork({ - donationId: donation.id, - }); - assert.isOk(updateDonation); - assert.equal(updateDonation.id, donation.id); - assert.equal(updateDonation.status, DONATION_STATUS.FAILED); - assert.equal( - updateDonation?.verifyErrorMessage, - errorMessages.TRANSACTION_FROM_ADDRESS_IS_DIFFERENT_FROM_SENT_FROM_ADDRESS, - ); - }); - - it('should change status to failed when donation toAddress is different with transaction toAddress', async () => { - // https://blockscout.com/xdai/mainnet/tx/0xe3b05b89f71b63e385c4971be872a9becd18f696b1e8abaddbc29c1cce59da63 - const transactionInfo = { - txHash: - '0xe3b05b89f71b63e385c4971be872a9becd18f696b1e8abaddbc29c1cce59da63', - currency: 'GIV', - networkId: NETWORK_IDS.XDAI, - fromAddress: '0x89E12F054526B985188b946063dDc874a62fEd45', - toAddress: '0xECb179EA5910D652eDa6988E919c7930F5Ffcf00', - amount: 1500, - timestamp: 1640408645 * 1000, - }; - - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 100, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - const updateDonation = await syncDonationStatusWithBlockchainNetwork({ - donationId: donation.id, - }); - assert.isOk(updateDonation); - assert.equal(updateDonation.id, donation.id); - assert.equal(updateDonation.status, DONATION_STATUS.FAILED); - assert.equal( - updateDonation?.verifyErrorMessage, - errorMessages.TRANSACTION_TO_ADDRESS_IS_DIFFERENT_FROM_SENT_TO_ADDRESS, - ); - }); - - it('should change status to failed when donation is very newer than transaction', async () => { - // https://blockscout.com/xdai/mainnet/tx/0x00aef89fc40cea0cc0cb7ae5ac18c0e586dccb200b230a9caabca0e08ff7a36b - const transactionInfo = { - txHash: - '0x00aef89fc40cea0cc0cb7ae5ac18c0e586dccb200b230a9caabca0e08ff7a36b', - currency: 'USDC', - networkId: NETWORK_IDS.XDAI, - fromAddress: '0x826976d7c600d45fb8287ca1d7c76fc8eb732030', - toAddress: '0x87f1c862c166b0ceb79da7ad8d0864d53468d076', - amount: 1, - }; - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 100, - anonymous: false, - createdAt: new Date(), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - const updateDonation = await syncDonationStatusWithBlockchainNetwork({ - donationId: donation.id, - }); - assert.isOk(updateDonation); - assert.equal(updateDonation.id, donation.id); - assert.equal(updateDonation.status, DONATION_STATUS.FAILED); - assert.equal( - updateDonation?.verifyErrorMessage, - errorMessages.TRANSACTION_CANT_BE_OLDER_THAN_DONATION, - ); + assert.equal(updateDonation.qfRoundId, qf.id); }); } diff --git a/src/services/donationService.ts b/src/services/donationService.ts index bcb9d62ea..12ae5293a 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -41,6 +41,8 @@ import { CustomToken, getTokenPrice } from './priceService'; import { updateProjectStatistics } from './projectService'; import { updateOrCreateProjectRoundRecord } from '../repositories/projectRoundRecordRepository'; import { updateOrCreateProjectUserRecord } from '../repositories/projectUserRecordRepository'; +import { findActiveEarlyAccessRound } from '../repositories/earlyAccessRoundRepository'; +import { findActiveQfRound } from '../repositories/qfRoundRepository'; export const TRANSAK_COMPLETED_STATUS = 'COMPLETED'; @@ -257,15 +259,27 @@ export const syncDonationStatusWithBlockchainNetwork = async (params: { timestamp: donation.createdAt.getTime() / 1000, }); donation.status = DONATION_STATUS.VERIFIED; + if (transaction.hash !== donation.transactionId) { donation.speedup = true; donation.transactionId = transaction.hash; } + + const transactionDate = new Date(transaction.timestamp * 1000); + + const [earlyAccessRound, qfRound] = await Promise.all([ + findActiveEarlyAccessRound(transactionDate), + findActiveQfRound({ date: transactionDate }), + ]); + + donation.earlyAccessRound = earlyAccessRound; + donation.qfRound = qfRound; + await donation.save(); // ONLY verified donations should be accumulated // After updating, recalculate user and project total donations - await updateProjectStatistics(donation.projectId); + await updateProjectStatistics(donation.projectId, transactionDate); await updateUserTotalDonated(donation.userId); await updateUserTotalReceived(donation.project.adminUserId); await updateOrCreateProjectRoundRecord( diff --git a/src/services/projectService.ts b/src/services/projectService.ts index 476167549..79cbf18be 100644 --- a/src/services/projectService.ts +++ b/src/services/projectService.ts @@ -27,8 +27,13 @@ export const getAppropriateSlug = async ( return slug; }; -export const updateProjectStatistics = async (projectId: number) => { - const activeQfRound = await findActiveQfRound(); +export const updateProjectStatistics = async ( + projectId: number, + date = new Date(), +) => { + const activeQfRound = await findActiveQfRound({ + date, + }); let sumDonationValueUsdForActiveQfRound = 0, countUniqueDonorsForActiveQfRound = 0; if (activeQfRound) { diff --git a/test/testUtils.ts b/test/testUtils.ts index f827ec78c..79a5372a4 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1965,8 +1965,8 @@ export interface CreateDonationData { projectId?: number; status?: string; verified?: string; - qfRoundId?: number; - earlyAccessRoundId?: number; + qfRoundId?: number | null; + earlyAccessRoundId?: number | null; tokenAddress?: string; qfRoundUserScore?: number; useDonationBox?: boolean;