From c42bd245902638523fcae01734fda5d3abe4256e Mon Sep 17 00:00:00 2001 From: mohammadranjbarz Date: Mon, 20 May 2024 17:05:27 +0330 Subject: [PATCH 01/12] =?UTF-8?q?Ignore=20small=20differences=20of=20amoun?= =?UTF-8?q?ts=20when=20matching=20draft=20donation=20for=20=E2=80=A6=20(#1?= =?UTF-8?q?573)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Ignore small differences of amounts when matching draft donation for erc20 * Fix eslint errors --- src/resolvers/draftDonationResolver.ts | 1 + .../chains/evm/draftDonationService.test.ts | 38 ++++++++++++- .../chains/evm/draftDonationService.ts | 55 +++++++++++++++++-- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/src/resolvers/draftDonationResolver.ts b/src/resolvers/draftDonationResolver.ts index b211010a5..5aae6e53a 100644 --- a/src/resolvers/draftDonationResolver.ts +++ b/src/resolvers/draftDonationResolver.ts @@ -41,6 +41,7 @@ export class DraftDonationResolver { @Mutation(_returns => Number) async createDraftDonation( + // TODO we should change it to bigInt in both backend and frontend to not round numbers @Arg('amount') amount: number, @Arg('networkId') networkId: number, @Arg('tokenAddress', { nullable: true }) tokenAddress: string, diff --git a/src/services/chains/evm/draftDonationService.test.ts b/src/services/chains/evm/draftDonationService.test.ts index 7753d5855..a192a9a12 100644 --- a/src/services/chains/evm/draftDonationService.test.ts +++ b/src/services/chains/evm/draftDonationService.test.ts @@ -1,4 +1,4 @@ -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { saveProjectDirectlyToDb, createProjectData, @@ -10,7 +10,10 @@ import { } from '../../../entities/draftDonation'; import { NETWORK_IDS } from '../../../provider'; import { ProjectAddress } from '../../../entities/projectAddress'; -import { matchDraftDonations } from './draftDonationService'; +import { + isAmountWithinTolerance, + matchDraftDonations, +} from './draftDonationService'; import { findUserByWalletAddress } from '../../../repositories/userRepository'; import { DONATION_ORIGINS, @@ -21,6 +24,7 @@ import { Project, ProjectUpdate } from '../../../entities/project'; import { User } from '../../../entities/user'; describe.skip('draftDonationMatching', draftDonationMatchingTests); +describe('isAmountWithinTolerance', isAmountWithinToleranceTests); const RandomAddress1 = '0xf3ddeb5022a6f06b61488b48c90315087ca2beef'; const RandomAddress2 = '0xc42a4791735ae1253c50c6226832e37ede3669f5'; @@ -244,3 +248,33 @@ function draftDonationMatchingTests() { expect(donation2).to.not.be.ok; }); } + +function isAmountWithinToleranceTests() { + it(`should return true for 40.5555 (405555) (0xa9059cbb000000000000000000000000b4964e1eca55db36a94e8aeffbfbab48529a2f6c00000000000000000000000000000000000000000000000000000000026ad3ec) + and 40.555499 (40555499)(0xa9059cbb000000000000000000000000b4964e1eca55db36a94e8aeffbfbab48529a2f6c00000000000000000000000000000000000000000000000000000000026ad3eb) + `, () => { + // https://gnosisscan.io/tx/0xfa65ef0a52e2f3b96c5802dcee4783858511989b7235035e8cab4d527fa15a1a + assert.isTrue( + isAmountWithinTolerance( + '0xa9059cbb000000000000000000000000b4964e1eca55db36a94e8aeffbfbab48529a2f6c00000000000000000000000000000000000000000000000000000000026ad3ec', + '0xa9059cbb000000000000000000000000b4964e1eca55db36a94e8aeffbfbab48529a2f6c00000000000000000000000000000000000000000000000000000000026ad3eb', + // Tether Decimals is 6 + 6, + ), + ); + }); + + it(`should return false for 40.5555 (405555) (0xa9059cbb000000000000000000000000b4964e1eca55db36a94e8aeffbfbab48529a2f6c00000000000000000000000000000000000000000000000000000000026ad3ec) + and 40.550571 (40550571)(0xa9059cbb000000000000000000000000b4964e1eca55db36a94e8aeffbfbab48529a2f6c00000000000000000000000000000000000000000000000000000000026ac0ab) + `, () => { + // https://gnosisscan.io/tx/0xfa65ef0a52e2f3b96c5802dcee4783858511989b7235035e8cab4d527fa15a1a + assert.isFalse( + isAmountWithinTolerance( + '0xa9059cbb000000000000000000000000b4964e1eca55db36a94e8aeffbfbab48529a2f6c00000000000000000000000000000000000000000000000000000000026ad3ec', + '0xa9059cbb000000000000000000000000b4964e1eca55db36a94e8aeffbfbab48529a2f6c00000000000000000000000000000000000000000000000000000000026ac0ab', + // Tether Decimals is 6 + 6, + ), + ); + }); +} diff --git a/src/services/chains/evm/draftDonationService.ts b/src/services/chains/evm/draftDonationService.ts index 8f4b017e2..dfbdeb1e9 100644 --- a/src/services/chains/evm/draftDonationService.ts +++ b/src/services/chains/evm/draftDonationService.ts @@ -15,6 +15,32 @@ import { DonationResolver } from '../../../resolvers/donationResolver'; import { ApolloContext } from '../../../types/ApolloContext'; import { logger } from '../../../utils/logger'; import { DraftDonationWorker } from '../../../workers/draftDonationMatchWorker'; +import { normalizeAmount } from '../../../utils/utils'; + +export const isAmountWithinTolerance = ( + callData1, + callData2, + tokenDecimals, +) => { + // Define the tolerance as 0.001 tokens in terms of the full token amount + const tolerance = 0.001; // For a readable number, directly as floating point + + // Function to extract and convert the amount part of the callData using BigInt for precision + function extractAmount(callData) { + const amountHex = callData.slice(-64); // Last 64 characters are the amount in hexadecimal + return BigInt('0x' + amountHex); + } + + const amount1 = extractAmount(callData1); + const amount2 = extractAmount(callData2); + + // Convert BigInt amounts to string then normalize + const normalizedAmount1 = normalizeAmount(amount1.toString(), tokenDecimals); + const normalizedAmount2 = normalizeAmount(amount2.toString(), tokenDecimals); + + // Compare within tolerance using normalized floating point numbers + return Math.abs(normalizedAmount1 - normalizedAmount2) <= tolerance; +}; const transferErc20CallData = (to: string, amount: number, decimals = 18) => { const iface = new ethers.utils.Interface([ @@ -126,6 +152,10 @@ export async function matchDraftDonations( } await submitMatchedDraftDonation(draftDonation, transaction); } else { + const token = await findTokenByNetworkAndAddress( + networkId, + targetAddress, + ); // ERC20 transfer let transferCallData = draftDonation.expectedCallData; logger.debug('matchDraftDonations() transferCallData', { @@ -133,10 +163,6 @@ export async function matchDraftDonations( transaction, }); if (!transferCallData) { - const token = await findTokenByNetworkAndAddress( - networkId, - targetAddress, - ); transferCallData = transferErc20CallData( draftDonation.toWalletAddress, draftDonation.amount, @@ -148,12 +174,29 @@ export async function matchDraftDonations( draftDonation.expectedCallData = transferCallData; } - if (transaction.input.toLowerCase() !== transferCallData) { + const isToAddressAreTheSame = + transferCallData.slice(0, 64).toLowerCase() === + transaction.input.slice(0, 64).toLocaleLowerCase(); + if ( + // TODO In the future we should compare exact match, but now because we save amount as number not bigInt in our db exact match with return false for some number because of rounding + !isToAddressAreTheSame || + !isAmountWithinTolerance( + transaction.input, + transferCallData, + token.decimals, + ) + ) { logger.debug( - 'matchDraftDonations() transaction.input.toLowerCase() !== transferCallData', + '!isToAddressAreTheSame || !isAmountWithinTolerance(transaction.input, transferCallData, token.decimals)', { transferCallData, transaction, + isToAddressAreTheSame, + isAmountWithinTolerance: isAmountWithinTolerance( + transaction.input, + transferCallData, + token.decimals, + ), }, ); continue; From 39c3e5960b703c89ab998fe4c2ed6f8de293ddb9 Mon Sep 17 00:00:00 2001 From: mohammadranjbarz Date: Tue, 21 May 2024 11:55:29 +0330 Subject: [PATCH 02/12] Release Integrate QF with super fluid streamed donations (#1555) * add activeQfRoundId to sortingBy InstantBoosting * add orderBy totalDonations and totalReactions * feat: add getRecurringDonationStats resolver * fix filtering by QF * remove qfRounds joins for non qf round filters * add some temp logs * remove temp logs * fix: changes test cases of recuring donations stats - create recored with createdAt field in past so test result won't be related to other endpoints test cases * Fix projectActualserviceView * fix stream balance depleted issue (#1496) Co-authored-by: mohammadranjbarz * rebuild * refresh and fetch user address separately (#1499) * Added pg_trgm extension migration (#1502) * fix: change recurring donations stats query to single query * fix recurring donation count * WIP: projectIds textArea * fix actual matching cap (#1507) * fix query error * fix user donations count * fix recurring donation count tests * fix user recurring donation query * fix user recurring donation test * add donations relation to qfround * add findArchivedQfRounds endpoint * add findArchivedQfRounds endpoint * feat: add sponsors & banner images upload * add sortBy to findArchivedQfRounds * 1.23.3 * add new test graphql query * add tests for new QfArchivedRounds * fixes on qfArchivedRounds query * add new tests for qfArchivedRounds query * fix findArchivedQfRounds tests * fix: keep already uploaded sponsors images * fix skip and limit for findArchivedQfRounds * Add logs and refactor the bootstrap code to help investigate latency problem * Add poolSize to orm config * Fix eslint errors * remove changing squareRootSumOfProjects when cap is overflown * Trigger ortto activity when user saves their profile info for the first time (#1520) * add newUser to updateUser query * add createOrttoProfile * add createOrttoProfile to NotificationAdapterInterface * add createOrttoProfile to MockNotificationAdapter * add CREATE_ORTTO_PROFILE event * Allow to set the matching pool token & amount to be something other than usd (#1517) * Allow to set the matching pool token & amount to be something other than USD in adminjs * Allow to set the matching pool token & amount to be something other than USD in QFRound table * add null to allocatedTokenSymbol and allocatedTokenChainId * add nullable true to allocatedTokenSymbol and allocatedTokenChainId * add allocatedFundUSDPreferred and allocatedFundUSD to qfRound * Comment migrations * Hotfix db improvements (#1523) * add extra configurations for postgresql connections * add master and slave replication strategy for typeorm * add qfRound to qfRoundStats * fix qfRoundStatsQuery * Hotfix staging fix latency (#1528) * add project donation summary view entity * convert projectQueries to querybuilder * add cache to projectDonationSummary queries * add configurable cache to slow queries * remove massive recurring donation log * add await for project queries * Add logs to projectVerificationForm * Add logs to projectVerificationForm * fix: add project Ids list textarea for qf round edit * Master to staging (#1543) * Hotfix db improvements (#1523) (#1524) * add extra configurations for postgresql connections * add master and slave replication strategy for typeorm Co-authored-by: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> * Fix/db replica production (#1525) * Hotfix db improvements (#1523) * add extra configurations for postgresql connections * add master and slave replication strategy for typeorm * Define db read only configs --------- Co-authored-by: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> * update comment * Hotfix latency issues for prod (#1529) * Hotfix staging fix latency (#1528) * add project donation summary view entity * convert projectQueries to querybuilder * add cache to projectDonationSummary queries * add configurable cache to slow queries * remove massive recurring donation log * add await for project queries * Add informative logs for draft donation service job (#1537) * Fix eslint errors * Fix/master test (#1541) * Fixed master test issue * Returned test to master pipeline * Comment executing donation summary view --------- Co-authored-by: Mohammad Ranjbar Z --------- Co-authored-by: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> Co-authored-by: Carlos Co-authored-by: mohammadranjbarz * Fix/word similarity - staging (#1546) * Hotfix db improvements (#1523) (#1524) * add extra configurations for postgresql connections * add master and slave replication strategy for typeorm Co-authored-by: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> * Fix/db replica production (#1525) * Hotfix db improvements (#1523) * add extra configurations for postgresql connections * add master and slave replication strategy for typeorm * Define db read only configs --------- Co-authored-by: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> * update comment * Hotfix latency issues for prod (#1529) * Hotfix staging fix latency (#1528) * add project donation summary view entity * convert projectQueries to querybuilder * add cache to projectDonationSummary queries * add configurable cache to slow queries * remove massive recurring donation log * add await for project queries * Add informative logs for draft donation service job (#1537) * Fix eslint errors * Fix/master test (#1541) * Fixed master test issue * Returned test to master pipeline * Comment executing donation summary view --------- Co-authored-by: Mohammad Ranjbar Z * Fixed word similarity issue * Removed unused import --------- Co-authored-by: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> Co-authored-by: Carlos Co-authored-by: mohammadranjbarz * remove refresh_project_summary_from totals and add user cache (#1539) * Remove users field from project * Remove users field from filterProjectsQuery * Remove projects field from user * remove added logs * fix eslint errors * remove users from Project.create * remove users from Project.create in testUtils.ts * remove projectOwnerId * replace admin with adminUserId * Add recurring donation join to donations() endpoint (#1554) related to #1483 * replace admin with adminUserId in SEED data * replace admin with adminUserId * replace admin with adminUserId in projectResolver.ts * replace admin with adminUserId in projectsTab.ts * replace admin with adminUserId in projectResolver.test.ts * replace admin with adminUserId in projectResolver.ts * add allocatedFundUSD and allocatedTokenSymbol to qfArchivedRounds * fix nullable * remove admin from project * replace admin with adminUserId * replace admin with adminUserId * add adminUserId field * drop admin column * fix: add telegram to ProjectSocialMediaType enum to allow adding telegram url * Add some logs * Fix eslint errors * Add maxQueuedJobs for draft donation worker * Fix eslint errors * fix unstable test case * Disable concurrency for draft donation worker * add indexes to project_summary_view (#1568) * improve projectBySlug query * fix: change output types to float * Ignore small differences of amounts when matching draft donation for erc20 * add graphql-fields * add getVerificationFormStatusByProjectId * refactor projectBySlug * add findProjectIdBySlug * fix projectBySlug tests with new changes * fix projectBySlug tests with new changes * fix projectBySlug tests with new changes * remove projectVerificationForm assert * add logs * fix title in should return projects with indicated slug test * Add streamed mini donations to qf round (#1557) * Add streamed mini donations to qf round related to Giveth/giveth-dapps-v2#3284 * Fix eslint error * Fix eslint errors * make verificationFormStatus field public * Reduce test runnning time (#1574) * fix: reduce test runnning time * fix: add permissions test case * fix: add permissions test case * fix: refactor (excluding permissions test cases from global beforeEach) --------- Co-authored-by: Ramin Co-authored-by: Meriem-BM Co-authored-by: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> Co-authored-by: Amin Latifi Co-authored-by: Carlos --- .DS_Store | Bin 6148 -> 8196 bytes ...14018700116-add_archived_QFRound_fields.ts | 12 +- ...6-fillRelatedAddressesFromProjectsTable.ts | 2 +- .../1712853017092-UserNewRoleQfManager.ts | 26 +- ...rent_mini_stream_donations_to_qf_Rounds.ts | 25 ++ migration/1715556030126-dropAdminColumn.ts | 11 + package-lock.json | 6 + package.json | 3 + .../NotificationCenterAdapter.ts | 1 - src/entities/project.ts | 14 +- src/entities/projectDonationSummaryView.ts | 25 ++ src/entities/user.ts | 21 +- src/repositories/powerBoostingRepository.ts | 6 +- .../previousRoundRankRepository.test.ts | 12 +- .../projectAddressRepository.test.ts | 6 +- src/repositories/projectRepository.test.ts | 14 +- src/repositories/projectRepository.ts | 11 +- .../projectVerificationRepository.test.ts | 34 +-- .../projectVerificationRepository.ts | 46 +++- src/repositories/qfRoundRepository.ts | 10 +- .../socialProfileRepository.test.ts | 6 +- src/repositories/userRepository.test.ts | 2 +- src/resolvers/donationResolver.test.ts | 82 +++++- src/resolvers/donationResolver.ts | 1 + .../projectResolver.allProject.test.ts | 4 +- src/resolvers/projectResolver.test.ts | 204 +++++++-------- src/resolvers/projectResolver.ts | 196 ++++++++------ .../projectVerificationFormResolver.test.ts | 58 ++--- .../projectVerificationFormResolver.ts | 4 +- src/resolvers/qfRoundResolver.ts | 4 + src/resolvers/reactionResolver.test.ts | 17 +- .../recurringDonationResolver.test.ts | 239 +++++++++++++++++- src/resolvers/recurringDonationResolver.ts | 62 +++++ src/resolvers/socialProfilesResolver.test.ts | 20 +- src/resolvers/types/project-input.ts | 2 +- src/server/adminJs/adminJs-types.ts | 2 +- src/server/adminJs/adminJsPermissions.test.ts | 2 +- .../tabs/components/ProjectsInQfRound.tsx | 2 +- src/server/adminJs/tabs/projectsTab.test.ts | 6 +- src/server/adminJs/tabs/projectsTab.ts | 6 +- src/server/adminJs/tabs/qfRoundTab.ts | 16 +- .../chains/evm/draftDonationService.ts | 5 +- src/services/donationService.ts | 3 - src/services/googleSheets.ts | 2 +- src/services/onramper/donationService.ts | 2 +- .../poignArt/syncPoignArtDonationCronJob.ts | 2 +- src/services/projectUpdatesService.test.ts | 2 +- src/services/recurringDonationService.ts | 22 +- .../the-giving-blocks/syncProjectsCronJob.ts | 1 - src/services/userService.test.ts | 2 +- src/types/projectSocialMediaType.ts | 1 + src/user/MeResolver.ts | 2 +- .../validators/graphqlQueryValidators.ts | 11 + src/utils/validators/projectValidator.ts | 4 +- test/graphqlQueries.ts | 101 +++----- test/testUtils.ts | 27 +- 56 files changed, 973 insertions(+), 434 deletions(-) create mode 100644 migration/1715521134568-relate_current_mini_stream_donations_to_qf_Rounds.ts create mode 100644 migration/1715556030126-dropAdminColumn.ts create mode 100644 src/entities/projectDonationSummaryView.ts diff --git a/.DS_Store b/.DS_Store index 64174a1f7d87d62a9694b48cff7515a7cd0b7c1a..49156d3d43e3fd9687e8d0b3276f4b9168001d2d 100644 GIT binary patch delta 474 zcmZoMXmOBWU|?W$DortDU;r^WfEYvza8E20o2aKKC=QYb@)>f{4TF>Oa|<>La)_~P zR^XV(yqKMXV`2kKJxB=)gC0XVLncEBOc_v&fobBSW5{y3`7SO=Ir&LIF%DCn`=t@z zjyj^sr@-Y?#Kbevy|HrZH zw&q8$302e2`07GzOkocxF R-~dJvDDoIK$Mei#1^^Fubw>aI delta 129 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{Mvv5sJ6q~50D9R0z2a9Dgq { await queryRunner.query(` ALTER TABLE "qf_round" - ADD COLUMN IF NOT EXISTS "bannerBgImage" character varying - `); - - await queryRunner.query(` - ALTER TABLE "qf_round" + ADD COLUMN IF NOT EXISTS "bannerBgImage" character varying, ADD COLUMN IF NOT EXISTS "sponsorsImgs" character varying[] DEFAULT '{}' NOT NULL `); } @@ -18,11 +14,7 @@ export class AddArchivedQFRoundFields1714018700116 public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE "qf_round" - DROP COLUMN IF EXISTS "bannerBgImage" - `); - - await queryRunner.query(` - ALTER TABLE "qf_round" + DROP COLUMN IF EXISTS "bannerBgImage", DROP COLUMN IF EXISTS "sponsorsImgs" `); } diff --git a/migration/1654415838996-fillRelatedAddressesFromProjectsTable.ts b/migration/1654415838996-fillRelatedAddressesFromProjectsTable.ts index 81798e6fb..6edde95ef 100644 --- a/migration/1654415838996-fillRelatedAddressesFromProjectsTable.ts +++ b/migration/1654415838996-fillRelatedAddressesFromProjectsTable.ts @@ -15,7 +15,7 @@ const insertRelatedAddress = async (params: { "networkId", address, "projectId", "userId", "isRecipient") VALUES (${networkId}, '${project.walletAddress?.toLowerCase()}', ${ project.id - }, ${Number(project.admin)}, true); + }, ${project.adminUserId}, true); `, ); }; diff --git a/migration/1712853017092-UserNewRoleQfManager.ts b/migration/1712853017092-UserNewRoleQfManager.ts index 3da4baa5f..0ff47a775 100644 --- a/migration/1712853017092-UserNewRoleQfManager.ts +++ b/migration/1712853017092-UserNewRoleQfManager.ts @@ -4,22 +4,24 @@ export class UserNewRoleQfManager1712853017092 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { // add enum qfManager to user table column role await queryRunner.query( - ` - ALTER TYPE user_role_enum ADD VALUE 'qfManager'; - ALTER TABLE "user" ALTER COLUMN "role" TYPE user_role_enum USING "role"::text::user_role_enum; - ALTER TABLE "user" ALTER COLUMN "role" SET DEFAULT 'restricted'; + `DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_enum + WHERE pg_enum.enumtypid = 'user_role_enum'::regtype + AND pg_enum.enumlabel = 'qfManager' + ) THEN + BEGIN + EXECUTE 'ALTER TYPE user_role_enum ADD VALUE ''qfManager'''; + END; + END IF; + END $$; `, ); } public async down(queryRunner: QueryRunner): Promise { - // remove enum qfManager from user table column role - await queryRunner.query( - ` - ALTER TYPE user_role_enum DROP VALUE 'qfManager'; - ALTER TABLE "user" ALTER COLUMN "role" TYPE user_role_enum USING "role"::text::user_role_enum; - ALTER TABLE "user" ALTER COLUMN "role" SET DEFAULT 'restricted'; - `, - ); + await queryRunner.query(''); // no need to remove enum value } } diff --git a/migration/1715521134568-relate_current_mini_stream_donations_to_qf_Rounds.ts b/migration/1715521134568-relate_current_mini_stream_donations_to_qf_Rounds.ts new file mode 100644 index 000000000..e25afa95d --- /dev/null +++ b/migration/1715521134568-relate_current_mini_stream_donations_to_qf_Rounds.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RelateCurrentMiniStreamDonationsToQfRounds1715521134568 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE donation + SET "qfRoundId" = qf_round.id + FROM project_qf_rounds_qf_round, qf_round + WHERE donation."recurringDonationId" IS NOT NULL + AND donation."projectId" = project_qf_rounds_qf_round."projectId" + AND project_qf_rounds_qf_round."qfRoundId" = qf_round."id" + AND donation."createdAt" BETWEEN qf_round."beginDate" AND qf_round."endDate" + `); + } + + public async down(_queryRunner: QueryRunner): Promise { + // Since the `up` migration changes data based on existing conditions rather than schema, + // rolling back would ideally require prior knowledge of the previous state, which might + // not be practical or possible to restore. + // Therefore, typically for data migrations, the down method might be left empty or + // could reset changes based on specific requirements. + } +} diff --git a/migration/1715556030126-dropAdminColumn.ts b/migration/1715556030126-dropAdminColumn.ts new file mode 100644 index 000000000..1e1579f15 --- /dev/null +++ b/migration/1715556030126-dropAdminColumn.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class DropAdminColumn1715556030126 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "project" + DROP COLUMN "admin"; + `); + } + public async down(_queryRunner: QueryRunner): Promise {} +} diff --git a/package-lock.json b/package-lock.json index 044ba777c..cabf95684 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "form-data": "^3.0.1", "google-spreadsheet": "^3.2.0", "graphql": "16.8.1", + "graphql-fields": "^2.0.3", "graphql-tag": "^2.12.6", "graphql-upload": "15.0.2", "handlebars": "4.7.7", @@ -11943,6 +11944,11 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/graphql-fields": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/graphql-fields/-/graphql-fields-2.0.3.tgz", + "integrity": "sha512-x3VE5lUcR4XCOxPIqaO4CE+bTK8u6gVouOdpQX9+EKHr+scqtK5Pp/l8nIGqIpN1TUlkKE6jDCCycm/WtLRAwA==" + }, "node_modules/graphql-query-complexity": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/graphql-query-complexity/-/graphql-query-complexity-0.12.0.tgz", diff --git a/package.json b/package.json index c65bb0c29..4174475a4 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "form-data": "^3.0.1", "google-spreadsheet": "^3.2.0", "graphql": "16.8.1", + "graphql-fields": "^2.0.3", "graphql-tag": "^2.12.6", "graphql-upload": "15.0.2", "handlebars": "4.7.7", @@ -175,6 +176,7 @@ "test:fillSnapshotBalance": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/fillSnapshotBalances.test.ts", "test:donationService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/qfRoundHistoryRepository.test.ts ./src/services/donationService.test.ts", "test:draftDonationService": "NODE_ENV=test mocha -t 99999 ./test/pre-test-scripts.ts src/services/chains/evm/draftDonationService.test.ts src/repositories/draftDonationRepository.test.ts src/workers/draftDonationMatchWorker.test.ts src/resolvers/draftDonationResolver.test.ts", + "test:draftDonationWorker": "NODE_ENV=test mocha -t 99999 ./test/pre-test-scripts.ts src/workers/draftDonationMatchWorker.test.ts", "test:draftRecurringDonationService": "NODE_ENV=test mocha -t 99999 ./test/pre-test-scripts.ts src/services/chains/evm/draftRecurringDonationService.test.ts", "test:userService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/userService.test.ts", "test:lostDonations": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/importLostDonationsJob.test.ts", @@ -201,6 +203,7 @@ "test:bootstrap": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/server/bootstrap.test.ts", "test:utils": "NODE_ENV=test mocha ./src/utils/utils.test.ts", "start": "NODE_ENV=development ts-node-dev --project ./tsconfig.json --respawn ./src/index.ts", + "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:revert:test": "NODE_ENV=test npx typeorm-ts-node-commonjs migration:revert -d ./src/ormconfig.ts", diff --git a/src/adapters/notifications/NotificationCenterAdapter.ts b/src/adapters/notifications/NotificationCenterAdapter.ts index d9cb1c6d9..884ef7e92 100644 --- a/src/adapters/notifications/NotificationCenterAdapter.ts +++ b/src/adapters/notifications/NotificationCenterAdapter.ts @@ -980,7 +980,6 @@ const getEmailDataDonationAttributes = async (params: { title: project.title, firstName: user.firstName, userId: user.id, - projectOwnerId: project.admin, slug: project.slug, projectLink: `${process.env.WEBSITE_URL}/project/${project.slug}`, amount, diff --git a/src/entities/project.ts b/src/entities/project.ts index 8e89219f2..109ebbe29 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -151,10 +151,6 @@ export class Project extends BaseEntity { @Column('text', { array: true, default: '{}' }) slugHistory?: string[]; - @Field({ nullable: true }) - @Column({ nullable: true }) - admin?: string; - @Field({ nullable: true }) @Column({ nullable: true }) description?: string; @@ -269,11 +265,6 @@ export class Project extends BaseEntity { @Column('jsonb', { nullable: true }) contacts: ProjectContacts[]; - @ManyToMany(_type => User, user => user.projects) - @Field(_type => [User], { nullable: true }) - @JoinTable() - users: User[]; - @Field(() => [Reaction], { nullable: true }) @OneToMany(_type => Reaction, reaction => reaction.project) reactions?: Reaction[]; @@ -318,6 +309,7 @@ export class Project extends BaseEntity { adminUser: User; @Column({ nullable: true }) + @Field(_type => Int) @RelationId((project: Project) => project.adminUser) adminUserId: number; @@ -582,10 +574,6 @@ export class Project extends BaseEntity { } } - owner() { - return this.users[0]; - } - @BeforeUpdate() async updateProjectDescriptionSummary() { await Project.update( diff --git a/src/entities/projectDonationSummaryView.ts b/src/entities/projectDonationSummaryView.ts new file mode 100644 index 000000000..e55fdcb6f --- /dev/null +++ b/src/entities/projectDonationSummaryView.ts @@ -0,0 +1,25 @@ +import { Field, ObjectType } from 'type-graphql'; +import { + Column, + PrimaryColumn, + BaseEntity, + ViewEntity, + ViewColumn, +} from 'typeorm'; + +@ViewEntity('project_donation_summary_view', { synchronize: false }) +@ObjectType() +export class ProjectDonationSummaryView extends BaseEntity { + @Field() + @ViewColumn() + @PrimaryColumn() + projectId: number; + + @ViewColumn() + @Column('double precision') + sumVerifiedDonations: number; + + @ViewColumn() + @Column('int') + uniqueDonorsCount: number; +} diff --git a/src/entities/user.ts b/src/entities/user.ts index d09ef9528..497eecacd 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -5,14 +5,12 @@ import { CreateDateColumn, Entity, Index, - JoinTable, - ManyToMany, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import { Project, ProjStatus, ReviewStatus } from './project'; +import { ProjStatus, ReviewStatus } from './project'; import { Donation, DONATION_STATUS } from './donation'; import { Reaction } from './reaction'; import { AccountVerification } from './accountVerification'; @@ -161,11 +159,6 @@ export class User extends BaseEntity { }) referredEvent?: ReferredEvent; - @Field(_type => [Project]) - @ManyToMany(_type => Project, project => project.users) - @JoinTable() - projects?: Project[]; - @Column('bool', { default: false }) segmentIdentified: boolean; @@ -216,6 +209,10 @@ export class User extends BaseEntity { status: DONATION_STATUS.VERIFIED, }) .andWhere(`donation."recurringDonationId" IS NULL`) + .cache( + `user-donationsCount-normal-${this.id}`, + Number(process.env.USER_STATS_CACHE_TIME || 60000), + ) .getCount(); // Count for recurring donations @@ -224,6 +221,10 @@ export class User extends BaseEntity { ) .where(`recurring_donation."donorId" = :donorId`, { donorId: this.id }) .andWhere('recurring_donation.totalUsdStreamed > 0') + .cache( + `user-donationsCount-recurring-${this.id}`, + Number(process.env.USER_STATS_CACHE_TIME || 60000), + ) .getCount(); // Sum of both counts @@ -239,6 +240,10 @@ export class User extends BaseEntity { `project.statusId = ${ProjStatus.active} AND project.reviewStatus = :reviewStatus`, { reviewStatus: ReviewStatus.Listed }, ) + .cache( + `user-likedProjectsCount-recurring-${this.id}`, + Number(process.env.USER_STATS_CACHE_TIME || 60000), + ) .getCount(); return likedProjectsCount; diff --git a/src/repositories/powerBoostingRepository.ts b/src/repositories/powerBoostingRepository.ts index d2082eb8e..9b255d488 100644 --- a/src/repositories/powerBoostingRepository.ts +++ b/src/repositories/powerBoostingRepository.ts @@ -165,7 +165,11 @@ export const findPowerBoostingsCountByUserId = async ( .leftJoin('powerBoosting.user', 'user') .addSelect(publicSelectionFields) .where(`percentage > 0`) - .andWhere(`"userId" =${userId}`); + .andWhere(`"userId" =${userId}`) + .cache( + `findPowerBoostingsCountByUserId-recurring-${userId}`, + Number(process.env.USER_STATS_CACHE_TIME || 60000), + ); return query.getCount(); }; diff --git a/src/repositories/previousRoundRankRepository.test.ts b/src/repositories/previousRoundRankRepository.test.ts index ad3c5c00c..cd418446a 100644 --- a/src/repositories/previousRoundRankRepository.test.ts +++ b/src/repositories/previousRoundRankRepository.test.ts @@ -13,6 +13,7 @@ import { saveProjectDirectlyToDb, saveUserDirectlyToDb, SEED_DATA, + dbIndependentTests, } from '../../test/testUtils'; import { insertSinglePowerBoosting, @@ -44,12 +45,19 @@ describe( projectsThatTheirRanksHaveChangedTestCases, ); -beforeEach(async () => { - await AppDataSource.getDataSource().query('truncate power_snapshot cascade'); +beforeEach(async function () { + const { title } = this.currentTest?.parent || {}; + + if (title && dbIndependentTests.includes(title)) { + return; + } + + await AppDataSource.getDataSource().query('TRUNCATE power_snapshot CASCADE'); await PowerBalanceSnapshot.clear(); await PowerBoostingSnapshot.clear(); await PreviousRoundRank.clear(); await PowerRound.clear(); + await createSomeSampleProjectsAndPowerViews(); }); diff --git a/src/repositories/projectAddressRepository.test.ts b/src/repositories/projectAddressRepository.test.ts index 5debdec7f..1df448209 100644 --- a/src/repositories/projectAddressRepository.test.ts +++ b/src/repositories/projectAddressRepository.test.ts @@ -142,7 +142,7 @@ function addNewProjectAddressTestCases() { ...createProjectData(), walletAddress, verified: true, - admin: String(user.id), + adminUserId: user.id, }); const newAddress = generateRandomEtheriumAddress(); const newRelatedAddress = await addNewProjectAddress({ @@ -199,7 +199,7 @@ function addBulkNewProjectAddressTestCases() { ...createProjectData(), walletAddress, verified: true, - admin: String(user.id), + adminUserId: user.id, }); const newAddress = generateRandomEtheriumAddress(); await addBulkNewProjectAddress([ @@ -224,7 +224,7 @@ function addBulkNewProjectAddressTestCases() { ...createProjectData(), walletAddress, verified: true, - admin: String(user.id), + adminUserId: user.id, }); const newAddress1 = generateRandomEtheriumAddress(); const newAddress2 = generateRandomEtheriumAddress(); diff --git a/src/repositories/projectRepository.test.ts b/src/repositories/projectRepository.test.ts index 4bf7b678f..28bb1a761 100644 --- a/src/repositories/projectRepository.test.ts +++ b/src/repositories/projectRepository.test.ts @@ -248,7 +248,7 @@ function updateProjectWithVerificationFormTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, networkId: NETWORK_IDS.GOERLI, }); @@ -298,7 +298,7 @@ function verifyProjectTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); assert.isFalse(project?.verified); @@ -314,7 +314,7 @@ function verifyProjectTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: true, }); assert.isTrue(project?.verified); @@ -333,12 +333,12 @@ function verifyMultipleProjectsTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const project2 = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); assert.isFalse(project?.verified); @@ -357,12 +357,12 @@ function verifyMultipleProjectsTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: true, }); const project2 = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: true, }); assert.isTrue(project?.verified); diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index f5eb813e3..4a669bbe0 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -92,7 +92,6 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { let query = Project.createQueryBuilder('project') .leftJoinAndSelect('project.status', 'status') - .leftJoinAndSelect('project.users', 'users') .leftJoinAndSelect('project.addresses', 'addresses') // We dont need it right now, but I comment it because we may need it later // .leftJoinAndSelect('project.anchorContracts', 'anchor_contract_address') @@ -325,6 +324,16 @@ export const findProjectBySlug = (slug: string): Promise => { ); }; +export const findProjectIdBySlug = (slug: string): Promise => { + // check current slug and previous slugs + return Project.createQueryBuilder('project') + .select('project.id') + .where(`:slug = ANY(project."slugHistory") or project.slug = :slug`, { + slug, + }) + .getOne(); +}; + export const findProjectBySlugWithoutAnyJoin = ( slug: string, ): Promise => { diff --git a/src/repositories/projectVerificationRepository.test.ts b/src/repositories/projectVerificationRepository.test.ts index cd2a9e719..b1c60e628 100644 --- a/src/repositories/projectVerificationRepository.test.ts +++ b/src/repositories/projectVerificationRepository.test.ts @@ -73,7 +73,7 @@ function createProjectVerificationFormTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -92,7 +92,7 @@ function updateProjectPersonalInfoOfProjectVerificationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -123,7 +123,7 @@ function updateProjectRegistryOfProjectVerificationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -153,7 +153,7 @@ function updateProjectContactsOfProjectVerificationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -187,7 +187,7 @@ function updateProjectVerificationLastStepTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -212,7 +212,7 @@ function updateMilestonesOfProjectVerificationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -253,7 +253,7 @@ function updateManagingFundsOfProjectVerificationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -295,7 +295,7 @@ function getInProgressProjectVerificationRequestTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -318,7 +318,7 @@ function getInProgressProjectVerificationRequestTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -344,7 +344,7 @@ function verifyFormTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -374,7 +374,7 @@ function verifyFormTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -407,7 +407,7 @@ function makeFormDraftTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -435,7 +435,7 @@ function makeFormDraftTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -472,7 +472,7 @@ function verifyMultipleFormTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -483,7 +483,7 @@ function verifyMultipleFormTestCases() { await projectVerificationForm.save(); const project2 = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm2 = await createProjectVerificationForm({ @@ -527,7 +527,7 @@ function verifyMultipleFormTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -538,7 +538,7 @@ function verifyMultipleFormTestCases() { await projectVerificationForm.save(); const project2 = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm2 = await createProjectVerificationForm({ diff --git a/src/repositories/projectVerificationRepository.ts b/src/repositories/projectVerificationRepository.ts index d101ff977..c3299900f 100644 --- a/src/repositories/projectVerificationRepository.ts +++ b/src/repositories/projectVerificationRepository.ts @@ -2,18 +2,19 @@ import { UpdateResult } from 'typeorm'; import { ManagingFunds, Milestones, - PROJECT_VERIFICATION_STATUSES, PersonalInfo, + PROJECT_VERIFICATION_STATUSES, + PROJECT_VERIFICATION_STEPS, ProjectContacts, ProjectRegistry, ProjectVerificationForm, - PROJECT_VERIFICATION_STEPS, } from '../entities/projectVerificationForm'; import { findProjectById } from './projectRepository'; import { findUserById } from './userRepository'; import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { User } from '../entities/user'; import { getAppropriateNetworkId } from '../services/chains'; +import { logger } from '../utils/logger'; export const createProjectVerificationForm = async (params: { userId: number; @@ -168,18 +169,28 @@ export const updateProjectPersonalInfoOfProjectVerification = async (params: { projectVerificationId: number; personalInfo: PersonalInfo; }): Promise => { - const { personalInfo, projectVerificationId } = params; - const projectVerificationForm = await findProjectVerificationFormById( - projectVerificationId, - ); - if (!projectVerificationForm) { - throw new Error( - i18n.__(translationErrorMessagesKeys.PROJECT_VERIFICATION_FORM_NOT_FOUND), + try { + const { personalInfo, projectVerificationId } = params; + const projectVerificationForm = await findProjectVerificationFormById( + projectVerificationId, + ); + if (!projectVerificationForm) { + throw new Error( + i18n.__( + translationErrorMessagesKeys.PROJECT_VERIFICATION_FORM_NOT_FOUND, + ), + ); + } + + projectVerificationForm.personalInfo = personalInfo; + return projectVerificationForm?.save(); + } catch (error) { + logger.debug( + 'updateProjectPersonalInfoOfProjectVerification error: ', + error, ); + throw new Error(error); } - - projectVerificationForm.personalInfo = personalInfo; - return projectVerificationForm?.save(); }; export const updateProjectRegistryOfProjectVerification = async (params: { @@ -317,6 +328,17 @@ export const updateManagingFundsOfProjectVerification = async (params: { return projectVerificationForm?.save(); }; +export const getVerificationFormStatusByProjectId = async ( + projectId: number, +): Promise => { + return ProjectVerificationForm.createQueryBuilder('project_verification_form') + .select(['project_verification_form.status']) + .where(`project_verification_form.projectId=:projectId`, { + projectId, + }) + .getOne(); +}; + export const getVerificationFormByProjectId = async ( projectId: number, ): Promise => { diff --git a/src/repositories/qfRoundRepository.ts b/src/repositories/qfRoundRepository.ts index cd1b08426..bc8e6f3ac 100644 --- a/src/repositories/qfRoundRepository.ts +++ b/src/repositories/qfRoundRepository.ts @@ -42,6 +42,12 @@ export class QFArchivedRounds { @Field(_type => Int) allocatedFund: number; + @Field(_type => Int, { nullable: true }) + allocatedFundUSD?: number | null; + + @Field(_type => String, { nullable: true }) + allocatedTokenSymbol?: string | null; + @Field(_type => [Int]) eligibleNetworks: number; @@ -83,6 +89,8 @@ export const findArchivedQfRounds = async ( .addSelect('SUM(donation.amount)', 'totalDonations') .addSelect('COUNT(DISTINCT donation.fromWalletAddress)', 'uniqueDonors') .addSelect('qfRound.allocatedFund', 'allocatedFund') + .addSelect('qfRound.allocatedFundUSD', 'allocatedFundUSD') + .addSelect('qfRound.allocatedTokenSymbol', 'allocatedTokenSymbol') .addSelect('qfRound.beginDate', 'beginDate') .groupBy('qfRound.id') .orderBy(fieldMap[field], direction, 'NULLS LAST') @@ -191,7 +199,7 @@ export const deactivateExpiredQfRounds = async (): Promise => { export const getRelatedProjectsOfQfRound = async ( qfRoundId: number, -): Promise<{ slug: string; name: string }[]> => { +): Promise<{ slug: string; name: string; id: number }[]> => { const query = ` SELECT "p"."slug", "p"."title" , p.id FROM "project" "p" diff --git a/src/repositories/socialProfileRepository.test.ts b/src/repositories/socialProfileRepository.test.ts index f5628c96f..c4a7116b4 100644 --- a/src/repositories/socialProfileRepository.test.ts +++ b/src/repositories/socialProfileRepository.test.ts @@ -28,7 +28,7 @@ function removeSocialProfileByIdTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -54,7 +54,7 @@ function findSocialProfilesByProjectIdTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -78,7 +78,7 @@ function findSocialProfilesByProjectIdTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const socialProfiles = await findSocialProfilesByProjectId({ diff --git a/src/repositories/userRepository.test.ts b/src/repositories/userRepository.test.ts index 508099a95..585e4b4a5 100644 --- a/src/repositories/userRepository.test.ts +++ b/src/repositories/userRepository.test.ts @@ -488,7 +488,7 @@ function findUsersWhoSupportProjectTestCases() { ); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(projectOwner.id), + adminUserId: projectOwner.id, }); const donor1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 39eed727b..10edd7f07 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -657,6 +657,74 @@ function donationsTestCases() { allDonationsCount, ); }); + it('should get result with recurring donations joined (for streamed mini donations)', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const anchorAddress = generateRandomEtheriumAddress(); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: user, + creator: user, + address: anchorAddress, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const currency = 'USD'; + + const recurringDonation = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: user, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + totalUsdStreamed: 1, + }); + recurringDonation.status = RECURRING_DONATION_STATUS.ACTIVE; + await recurringDonation.save(); + const donation = await saveDonationDirectlyToDb( + { + ...createDonationData(), + }, + user.id, + project.id, + ); + donation.recurringDonation = recurringDonation; + await donation.save(); + + // Use moment to parse the createdAt string + const momentDate = moment(donation.createdAt, 'YYYYMMDD HH:mm:ss'); + + // Create fromDate as one second before + const fromDate = momentDate + .clone() + .subtract(1, 'seconds') + .format('YYYYMMDD HH:mm:ss'); + + // Create toDate as one second after + const toDate = momentDate + .clone() + .add(1, 'seconds') + .format('YYYYMMDD HH:mm:ss'); + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchAllDonationsQuery, + variables: { + fromDate, + toDate, + }, + }); + assert.isOk(donationsResponse.data.data.donations); + assert.equal(donationsResponse.data.data.donations.length, 1); + assert.equal( + Number(donationsResponse.data.data.donations[0].recurringDonation.id), + recurringDonation.id, + ); + }); it('should get result when sending fromDate', async () => { const oldDonation = await saveDonationDirectlyToDb( createDonationData(), @@ -817,6 +885,16 @@ function donationsTestCases() { assert.isArray(donation.project.categories); }); }); + it('should project include categories', async () => { + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchAllDonationsQuery, + variables: {}, + }); + assert.isOk(donationsResponse.data.data.donations); + donationsResponse.data.data.donations.forEach(donation => { + assert.isArray(donation.project.categories); + }); + }); } function createDonationTestCases() { @@ -3643,7 +3721,7 @@ function donationsByUserIdTestCases() { updatedAt: new Date(), slug: title, // firstUser's id - admin: String(user.id), + adminUserId: user.id, qualityScore: 30, // just need the initial value to be different than 0 totalDonations: 10, @@ -3723,7 +3801,7 @@ function donationsByUserIdTestCases() { updatedAt: new Date(), slug: title, // firstUser's id - admin: String(user.id), + adminUserId: user.id, qualityScore: 30, // just need the initial value to be different than 0 totalDonations: 10, diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 4a00c1ebc..0540d7f22 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -238,6 +238,7 @@ export class DonationResolver { .leftJoin('donation.user', 'user') .addSelect(publicSelectionFields) .leftJoinAndSelect('donation.project', 'project') + .leftJoinAndSelect('donation.recurringDonation', 'recurringDonation') .leftJoinAndSelect('project.categories', 'categories') .leftJoin('project.projectPower', 'projectPower') .addSelect([ diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts index 5d1ae0678..025dbb9d6 100644 --- a/src/resolvers/projectResolver.allProject.test.ts +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -62,12 +62,12 @@ function allProjectsTestCases() { const projects = result.data.data.allProjects.projects; const secondUserProjects = await Project.find({ where: { - admin: String(SEED_DATA.SECOND_USER.id), + adminUserId: SEED_DATA.SECOND_USER.id, }, }); assert.equal(projects.length, secondUserProjects.length); - assert.equal(Number(projects[0]?.admin), SEED_DATA.SECOND_USER.id); + assert.equal(projects[0]?.adminUserId, SEED_DATA.SECOND_USER.id); assert.isNotEmpty(projects[0].addresses); projects.forEach(project => { assert.isNotOk(project.adminUser.email); diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index 5138d9554..5d82a7df0 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -359,7 +359,7 @@ function projectsByUserIdTestCases() { ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), - admin: String(user.id), + adminUserId: user.id, }); const verificationForm = await ProjectVerificationForm.create({ @@ -387,7 +387,7 @@ function projectsByUserIdTestCases() { const projects = result.data.data.projectsByUserId.projects; const projectWithAnotherOwner = projects.find( - project => Number(project.admin) !== user!.id, + project => project.adminUserId !== user!.id, ); assert.isNotOk(projectWithAnotherOwner); projects.forEach(project => { @@ -410,7 +410,7 @@ function projectsByUserIdTestCases() { }); const projects = result.data.data.projectsByUserId.projects; const projectWithAnotherOwner = projects.find( - project => Number(project.admin) !== userId, + project => project.adminUserId !== userId, ); assert.isNotOk(projectWithAnotherOwner); projects.forEach(project => { @@ -481,7 +481,7 @@ function projectsByUserIdTestCases() { const userId = SEED_DATA.FIRST_USER.id; const draftProject = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, statusId: ProjStatus.drafted, }); const result = await axios.post(graphqlUrl, { @@ -505,7 +505,7 @@ function projectsByUserIdTestCases() { const userId = SEED_DATA.FIRST_USER.id; const notListedProject = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, listed: false, reviewStatus: ReviewStatus.NotListed, }); @@ -553,7 +553,7 @@ function createProjectTestCases() { const accessToken = await generateTestAccessToken(SEED_DATA.FIRST_USER.id); const sampleProject1 = { title: 'title1', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -563,7 +563,7 @@ function createProjectTestCases() { }; const sampleProject2 = { title: 'title1', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -571,7 +571,7 @@ function createProjectTestCases() { }, ], }; - const promise1 = axios.post( + const res1 = await axios.post( graphqlUrl, { query: createProjectQuery, @@ -585,7 +585,7 @@ function createProjectTestCases() { }, }, ); - const promise2 = axios.post( + const res2 = await axios.post( graphqlUrl, { query: createProjectQuery, @@ -599,20 +599,15 @@ function createProjectTestCases() { }, }, ); - const [result1, result2] = await Promise.all([promise1, promise2]); - const isResult1Ok = !!result1.data.data?.createProject; - const isResult2Ok = !!result2.data.data?.createProject; - - // Exactly one should be ok - const exactlyOneOk = - (isResult1Ok && !isResult2Ok) || (!isResult1Ok && isResult2Ok); - - assert.isTrue(exactlyOneOk, 'Exactly one operation should be successful'); + const isRes1Ok = !!res1.data.data?.createProject; + const isRes2Ok = !!res2.data.data?.createProject; + assert.isTrue(isRes1Ok); + assert.isFalse(isRes2Ok); }); it('Create Project should return <>, calling without token IN ENGLISH when no-lang header is sent', async () => { const sampleProject = { title: 'title1', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -641,7 +636,7 @@ function createProjectTestCases() { it('Create Project should return <>, calling without token IN ENGLISH when non supported language is sent', async () => { const sampleProject = { title: 'title1', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -674,7 +669,7 @@ function createProjectTestCases() { it('Create Project should return <>, calling without token IN SPANISH', async () => { const sampleProject = { title: 'title1', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -706,7 +701,7 @@ function createProjectTestCases() { it('Create Project should return <>, calling without token', async () => { const sampleProject = { title: 'title1', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -740,7 +735,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: ['invalid category'], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -782,7 +777,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: [nonActiveCategory.name], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -815,7 +810,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: SEED_DATA.FOOD_SUB_CATEGORIES, description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -858,7 +853,7 @@ function createProjectTestCases() { SEED_DATA.FOOD_SUB_CATEGORIES[1], ], description: '
Sample Project Creation
', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -895,7 +890,7 @@ function createProjectTestCases() { SEED_DATA.FOOD_SUB_CATEGORIES[1], ], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -939,7 +934,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: SEED_DATA.MALFORMED_ETHEREUM_ADDRESS, @@ -972,7 +967,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: SEED_DATA.MALFORMED_SOLANA_ADDRESS, @@ -1006,7 +1001,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: SEED_DATA.FIRST_PROJECT.walletAddress, @@ -1043,7 +1038,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: SEED_DATA.DAI_SMART_CONTRACT_ADDRESS, @@ -1081,7 +1076,7 @@ function createProjectTestCases() { title: SEED_DATA.FIRST_PROJECT.title, categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -1134,7 +1129,7 @@ function createProjectTestCases() { description: 'a'.repeat(PROJECT_DESCRIPTION_MAX_LENGTH + 1), image: 'https://gateway.pinata.cloud/ipfs/QmauSzWacQJ9rPkPJgr3J3pdgfNRGAaDCr1yAToVWev2QS', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -1196,7 +1191,7 @@ function createProjectTestCases() { description: 'description', image: 'https://gateway.pinata.cloud/ipfs/QmauSzWacQJ9rPkPJgr3J3pdgfNRGAaDCr1yAToVWev2QS', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -1280,7 +1275,7 @@ function createProjectTestCases() { description: 'description', image: 'https://gateway.pinata.cloud/ipfs/QmauSzWacQJ9rPkPJgr3J3pdgfNRGAaDCr1yAToVWev2QS', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -1323,7 +1318,7 @@ function createProjectTestCases() { categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', isDraft: true, - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -1367,8 +1362,8 @@ function createProjectTestCases() { ); assert.equal( - result.data.data.createProject.admin, - String(SEED_DATA.FIRST_USER.id), + result.data.data.createProject.adminUserId, + SEED_DATA.FIRST_USER.id, ); assert.equal(result.data.data.createProject.verified, false); assert.equal( @@ -1580,7 +1575,7 @@ function updateProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); @@ -1652,7 +1647,7 @@ function updateProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); const editProjectResult = await axios.post( @@ -1697,7 +1692,7 @@ function updateProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const ethAddress = generateRandomEtheriumAddress(); const solanaAddress = generateRandomSolanaAddress(); @@ -1754,7 +1749,7 @@ function updateProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); const newWalletAddress2 = generateRandomEtheriumAddress(); @@ -1820,7 +1815,7 @@ function updateProjectTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, walletAddress, }); const newWalletAddress = project.walletAddress; @@ -1882,7 +1877,7 @@ function updateProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); const editProjectResult = await axios.post( @@ -1915,7 +1910,7 @@ function updateProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); const editProjectResult = await axios.post( @@ -1980,7 +1975,7 @@ function updateProjectTestCases() { title: 'test ' + String(new Date().getTime()), categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -2038,7 +2033,7 @@ function updateProjectTestCases() { title, categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -2096,7 +2091,7 @@ function updateProjectTestCases() { const walletAddress = generateRandomEtheriumAddress(); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, walletAddress, }); @@ -2566,7 +2561,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); @@ -2602,7 +2597,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomSolanaAddress(); @@ -2639,7 +2634,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); @@ -2673,7 +2668,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); @@ -2707,7 +2702,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, listed: true, reviewStatus: ReviewStatus.Listed, }); @@ -2748,7 +2743,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, listed: false, reviewStatus: ReviewStatus.NotListed, }); @@ -2790,7 +2785,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); @@ -3877,7 +3872,7 @@ function walletAddressIsPurpleListedTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); await addNewProjectAddress({ @@ -3973,7 +3968,7 @@ function getPurpleListTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); await addNewProjectAddress({ @@ -4292,7 +4287,7 @@ function getProjectUpdatesTestCases() { } function projectBySlugTestCases() { - it('should return projects with indicated slug and verification form if owner', async () => { + it('should return projects with indicated slug and verification form status if owner', async () => { const project1 = await saveProjectDirectlyToDb({ ...createProjectData(), title: String(new Date().getTime()), @@ -4301,7 +4296,7 @@ function projectBySlugTestCases() { const user = (await User.findOne({ where: { - id: Number(project1.admin), + id: project1.adminUserId, }, })) as User; @@ -4331,68 +4326,53 @@ function projectBySlugTestCases() { const project = result.data.data.projectBySlug; assert.equal(Number(project.id), project1.id); - assert.isOk(project.projectVerificationForm); - assert.equal(project.projectVerificationForm.id, verificationForm.id); + assert.isOk(project.verificationFormStatus); + assert.equal(project.verificationFormStatus, verificationForm.status); assert.isOk(project.adminUser.walletAddress); assert.isOk(project.adminUser.firstName); assert.isNotOk(project.adminUser.email); assert.isOk(project.categories[0].mainCategory.title); }); - it('should return verificationFormStatus if its not owner', async () => { - const project1 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - - const user = - (await User.findOne({ - where: { - id: Number(project1.admin), - }, - })) || undefined; - - const verificationForm = await ProjectVerificationForm.create({ - project: project1, - user, - status: PROJECT_VERIFICATION_STATUSES.DRAFT, - }).save(); - - const result = await axios.post(graphqlUrl, { - query: fetchProjectBySlugQuery, - variables: { - slug: project1.slug, - connectedWalletUserId: user!.id, - }, - }); - - const project = result.data.data.projectBySlug; - assert.equal(Number(project.id), project1.id); - assert.isNotOk(project.projectVerificationForm); - assert.equal(project.verificationFormStatus, verificationForm.status); - }); it('should return projects with indicated slug', async () => { const walletAddress = generateRandomEtheriumAddress(); - const project1 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - walletAddress, - }); - + const accessToken = await generateTestAccessToken(SEED_DATA.FIRST_USER.id); + const sampleProject1 = { + title: walletAddress, + adminUserId: SEED_DATA.FIRST_USER.id, + addresses: [ + { + address: walletAddress, + networkId: NETWORK_IDS.XDAI, + chainType: ChainType.EVM, + }, + ], + }; + const res1 = await axios.post( + graphqlUrl, + { + query: createProjectQuery, + variables: { + project: sampleProject1, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + const _project = res1.data.data.createProject; const result = await axios.post(graphqlUrl, { query: fetchProjectBySlugQuery, variables: { - slug: project1.slug, + slug: _project.slug, }, }); - const project = result.data.data.projectBySlug; - assert.equal(Number(project.id), project1.id); + assert.equal(Number(project.id), Number(_project.id)); assert.isOk(project.adminUser.walletAddress); assert.isOk(project.givbackFactor); - assert.isNull(project.projectVerificationForm); assert.isOk(project.adminUser.firstName); assert.isNotOk(project.adminUser.email); assert.isNotEmpty(project.addresses); @@ -5192,8 +5172,8 @@ function similarProjectsBySlugTestCases() { const [, relatedCount] = await Project.createQueryBuilder('project') .innerJoinAndSelect('project.categories', 'categories') .where('project.id != :id', { id: viewedProject?.id }) - .andWhere('project.admin = :ownerId', { - ownerId: String(SEED_DATA.FIRST_USER.id), + .andWhere('project.adminUserId = :ownerId', { + ownerId: SEED_DATA.FIRST_USER.id, }) .andWhere( `project.statusId = ${ProjStatus.active} AND project.reviewStatus = :reviewStatus`, @@ -5237,7 +5217,7 @@ function addProjectUpdateTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const result = await axios.post( @@ -5323,7 +5303,7 @@ function addProjectUpdateTestCases() { const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const accessTokenUser1 = await generateTestAccessToken(user1.id); @@ -5422,7 +5402,7 @@ function editProjectUpdateTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const updateProject = await ProjectUpdate.create({ userId: user.id, @@ -5549,7 +5529,7 @@ function deleteProjectUpdateTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const updateProject = await ProjectUpdate.create({ @@ -5582,7 +5562,7 @@ function deleteProjectUpdateTestCases() { const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const accessTokenUser1 = await generateTestAccessToken(user1.id); diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index fb38730ed..e68fc17e8 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1,5 +1,5 @@ import { Max, Min } from 'class-validator'; -import { Brackets, Repository } from 'typeorm'; +import { Brackets, getMetadataArgsStorage, Repository } from 'typeorm'; import { Service } from 'typedi'; import { Arg, @@ -7,6 +7,7 @@ import { ArgsType, Ctx, Field, + Info, InputType, Int, Mutation, @@ -15,6 +16,7 @@ import { registerEnumType, Resolver, } from 'type-graphql'; +import graphqlFields from 'graphql-fields'; import { SelectQueryBuilder } from 'typeorm/query-builder/SelectQueryBuilder'; import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; import { Reaction } from '../entities/reaction'; @@ -77,15 +79,14 @@ import { FilterProjectQueryInputParams, filterProjectsQuery, findProjectById, - findProjectBySlugWithoutAnyJoin, + findProjectIdBySlug, totalProjectsPerDate, totalProjectsPerDateByMonthAndYear, - userIsOwnerOfProject, } from '../repositories/projectRepository'; import { sortTokensByOrderAndAlphabets } from '../utils/tokenUtils'; import { getNotificationAdapter } from '../adapters/adaptersFactory'; import { NETWORK_IDS } from '../provider'; -import { getVerificationFormByProjectId } from '../repositories/projectVerificationRepository'; +import { getVerificationFormStatusByProjectId } from '../repositories/projectVerificationRepository'; import { resourcePerDateReportValidator, validateWithJoiSchema, @@ -464,9 +465,9 @@ export class ProjectResolver { query: SelectQueryBuilder, take: number, skip: number, - ownerId?: string | null, + ownerId?: number | null, ): Promise { - query.andWhere('project.admin = :ownerId', { ownerId }); + query.andWhere('project.adminUserId = :ownerId', { ownerId }); const [projects, totalCount] = await query .orderBy('project.creationDate', 'DESC') .take(take) @@ -844,56 +845,103 @@ export class ProjectResolver { query = ProjectResolver.addUserReaction(query, connectedWalletUserId, user); const project = await query.getOne(); - canUserVisitProject(project, String(user?.userId)); + canUserVisitProject(project, user?.userId); return project; } + // Helper method to get the fields of the Project entity + private getEntityFields(entity: typeof Project): string[] { + const metadata = getMetadataArgsStorage(); + const columns = metadata.columns.filter(col => col.target === entity); + return columns.map(col => col.propertyName); + } + @Query(_returns => ProjectBySlugResponse) async projectBySlug( @Arg('slug') slug: string, @Arg('connectedWalletUserId', _type => Int, { nullable: true }) connectedWalletUserId: number, @Ctx() { req: { user } }: ApolloContext, + @Info() info: any, ) { - const viewerUserId = connectedWalletUserId || user?.userId; - let isOwnerOfProject = false; - - // ensure it's the owner - if (viewerUserId) { - isOwnerOfProject = await userIsOwnerOfProject(viewerUserId, slug); - } - - const minimalProject = await findProjectBySlugWithoutAnyJoin(slug); + const minimalProject = await findProjectIdBySlug(slug); if (!minimalProject) { throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); } - const campaignSlugs = (await getAllProjectsRelatedToActiveCampaigns())[ - minimalProject.id - ]; + + // Extract requested fields + const fields = graphqlFields(info); + const projectFields = this.getEntityFields(Project); + + // Filter requested fields to only include those in the Project entity + const selectedFields = Object.keys(fields).filter(field => + projectFields.includes(field), + ); + + // Dynamically build the select fields + const selectFields = selectedFields.map(field => `project.${field}`); let query = this.projectRepository .createQueryBuilder('project') + .select(selectFields) .where(`project.id = :id`, { id: minimalProject.id, }) - .leftJoinAndSelect('project.status', 'status') - .leftJoinAndSelect( - 'project.categories', - 'categories', - 'categories.isActive = :isActive', - { isActive: true }, - ) - .leftJoinAndSelect('categories.mainCategory', 'mainCategory') - .leftJoinAndSelect('project.organization', 'organization') - .leftJoinAndSelect('project.addresses', 'addresses') - .leftJoinAndSelect('project.socialMedia', 'socialMedia') - .leftJoinAndSelect('project.anchorContracts', 'anchor_contract_address') - .leftJoinAndSelect('project.projectPower', 'projectPower') - .leftJoinAndSelect('project.projectInstantPower', 'projectInstantPower') - .leftJoinAndSelect('project.qfRounds', 'qfRounds') - .leftJoinAndSelect('project.projectFuturePower', 'projectFuturePower') - .leftJoinAndMapMany( + .leftJoinAndSelect('project.status', 'status'); + + if (fields.categories) { + query = query + .leftJoinAndSelect( + 'project.categories', + 'categories', + 'categories.isActive = :isActive', + { isActive: true }, + ) + .leftJoinAndSelect('categories.mainCategory', 'mainCategory') + .orderBy({ + 'mainCategory.title': 'ASC', + 'categories.name': 'ASC', + }); + } + if (fields.organization) { + query = query.leftJoinAndSelect('project.organization', 'organization'); + } + if (fields.addresses) { + query = query.leftJoinAndSelect('project.addresses', 'addresses'); + } + if (fields.socialMedia) { + query = query.leftJoinAndSelect('project.socialMedia', 'socialMedia'); + } + if (fields.anchorContracts) { + query = query.leftJoinAndSelect( + 'project.anchorContracts', + 'anchor_contract_address', + ); + } + if (fields.projectPower) { + query = query.leftJoinAndSelect('project.projectPower', 'projectPower'); + } + if (fields.projectInstantPower) { + query = query.leftJoinAndSelect( + 'project.projectInstantPower', + 'projectInstantPower', + ); + } + if (fields.qfRounds) { + query = query.leftJoinAndSelect('project.qfRounds', 'qfRounds'); + } + if (fields.projectFuturePower) { + query = query.leftJoinAndSelect( + 'project.projectFuturePower', + 'projectFuturePower', + ); + } + if (fields.campaigns) { + const campaignSlugs = (await getAllProjectsRelatedToActiveCampaigns())[ + minimalProject.id + ]; + query = query.leftJoinAndMapMany( 'project.campaigns', Campaign, 'campaigns', @@ -902,38 +950,44 @@ export class ProjectResolver { slug, campaignSlugs, }, - ) - .leftJoin('project.adminUser', 'user') - .addSelect(publicSelectionFields); // aliased selection - - query = ProjectResolver.addUserReaction(query, connectedWalletUserId, user); - - if (isOwnerOfProject) { - query = ProjectResolver.addProjectVerificationForm( + ); + } + if (fields.adminUser) { + const adminUserFields = Object.keys(fields.adminUser).map( + field => `user.${field}`, + ); + const filterByPublicFields = publicSelectionFields.filter(field => + adminUserFields.includes(field), + ); + query = query + .leftJoin('project.adminUser', 'user') + .addSelect(filterByPublicFields); // aliased selection + } + if (fields.reaction) { + query = ProjectResolver.addUserReaction( query, connectedWalletUserId, user, ); } - query = query.orderBy({ - 'mainCategory.title': 'ASC', - 'categories.name': 'ASC', - }); - const project = await query.getOne(); - canUserVisitProject(project, String(user?.userId)); - const verificationForm = - project?.projectVerificationForm || - (await getVerificationFormByProjectId(project?.id as number)); - if (verificationForm) { - (project as Project).verificationFormStatus = verificationForm?.status; - } + canUserVisitProject(project, user?.userId); + if (fields.verificationFormStatus) { + const verificationForm = await getVerificationFormStatusByProjectId( + project?.id as number, + ); + if (verificationForm) { + (project as Project).verificationFormStatus = verificationForm?.status; + } + } + if (fields.givbackFactor) { + const { givbackFactor } = await calculateGivbackFactor(project!.id); + return { ...project, givbackFactor }; + } // We know that we have the project because if we reach this line means minimalProject is not null - const { givbackFactor } = await calculateGivbackFactor(project!.id); - - return { ...project, givbackFactor }; + return project; } @Mutation(_returns => Project) @@ -954,10 +1008,10 @@ export class ProjectResolver { if (!project) throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); - logger.debug(`project.admin ---> : ${project.admin}`); + logger.debug(`project.adminUserId ---> : ${project.adminUserId}`); logger.debug(`user.userId ---> : ${user.userId}`); logger.debug(`updateProject, inputData :`, newProjectData); - if (project.admin !== String(user.userId)) + if (project.adminUserId !== user.userId) throw new Error( i18n.__(translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT), ); @@ -1063,7 +1117,7 @@ export class ProjectResolver { } } - const adminUser = (await findUserById(Number(project.admin))) as User; + const adminUser = (await findUserById(project.adminUserId)) as User; if (newProjectData.addresses) { await removeRecipientAddressOfProject({ project }); await addBulkNewProjectAddress( @@ -1116,7 +1170,7 @@ export class ProjectResolver { if (!project) throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); - if (project.admin !== String(user.userId)) { + if (project.adminUserId !== user.userId) { throw new Error( i18n.__(translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT), ); @@ -1124,7 +1178,7 @@ export class ProjectResolver { await validateProjectWalletAddress(address, projectId, chainType); - const adminUser = (await findUserById(Number(project.admin))) as User; + const adminUser = (await findUserById(project.adminUserId)) as User; await addNewProjectAddress({ project, user: adminUser, @@ -1312,7 +1366,6 @@ export class ProjectResolver { const project = Project.create({ ...projectInput, - categories: categories as Category[], organization: organization as Organization, image, @@ -1320,8 +1373,7 @@ export class ProjectResolver { updatedAt: now, slug: slug.toLowerCase(), slugHistory: [], - admin: String(ctx.req.user.userId), - users: [user], + adminUserId: ctx.req.user.userId, status: status as ProjectStatus, qualityScore, totalDonations: 0, @@ -1426,7 +1478,7 @@ export class ProjectResolver { if (!project) throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); - if (project.admin !== String(user.userId)) + if (project.adminUserId !== user.userId) throw new Error( i18n.__(translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT), ); @@ -1486,7 +1538,7 @@ export class ProjectResolver { const project = await Project.findOne({ where: { id: update.projectId } }); if (!project) throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); - if (project.admin !== String(user.userId)) + if (project.adminUserId !== user.userId) throw new Error( i18n.__(translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT), ); @@ -1518,7 +1570,7 @@ export class ProjectResolver { const project = await Project.findOne({ where: { id: update.projectId } }); if (!project) throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); - if (project.admin !== String(user.userId)) + if (project.adminUserId !== user.userId) throw new Error( i18n.__(translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT), ); @@ -1711,7 +1763,7 @@ export class ProjectResolver { ); } - query = query.where('project.admin = :userId', { userId: String(userId) }); + query = query.where('project.adminUserId = :userId', { userId }); if (userId !== user?.userId) { query = query.andWhere( @@ -1852,7 +1904,7 @@ export class ProjectResolver { query, take, skip, - viewedProject?.admin, + viewedProject?.adminUserId, ); } } diff --git a/src/resolvers/projectVerificationFormResolver.test.ts b/src/resolvers/projectVerificationFormResolver.test.ts index d2a1acb6b..695e86d59 100644 --- a/src/resolvers/projectVerificationFormResolver.test.ts +++ b/src/resolvers/projectVerificationFormResolver.test.ts @@ -72,7 +72,7 @@ function createProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -113,7 +113,7 @@ function createProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user1.id), + adminUserId: user1.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -144,7 +144,7 @@ function createProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: true, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -190,7 +190,7 @@ function createProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -208,7 +208,7 @@ function createProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -352,7 +352,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -409,7 +409,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -462,7 +462,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -519,7 +519,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -575,7 +575,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -635,7 +635,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -715,7 +715,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -784,7 +784,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -839,7 +839,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -893,7 +893,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -948,7 +948,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1002,7 +1002,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1054,7 +1054,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: true, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1090,7 +1090,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1146,7 +1146,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1202,7 +1202,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1258,7 +1258,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1298,7 +1298,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: true, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1323,7 +1323,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user1.id), + adminUserId: user1.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1359,7 +1359,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1431,7 +1431,7 @@ function projectVerificationSendEmailConfirmationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1488,7 +1488,7 @@ function projectVerificationSendEmailConfirmationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1537,7 +1537,7 @@ function projectVerificationConfirmEmailTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1604,7 +1604,7 @@ function projectVerificationConfirmEmailTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, diff --git a/src/resolvers/projectVerificationFormResolver.ts b/src/resolvers/projectVerificationFormResolver.ts index eee286f20..100ca8d78 100644 --- a/src/resolvers/projectVerificationFormResolver.ts +++ b/src/resolvers/projectVerificationFormResolver.ts @@ -213,7 +213,7 @@ export class ProjectVerificationFormResolver { i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND), ); } - if (Number(project.admin) !== userId) { + if (project.adminUserId !== userId) { throw new Error( i18n.__( translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT, @@ -315,7 +315,7 @@ export class ProjectVerificationFormResolver { i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND), ); } - if (Number(project.admin) !== userId) { + if (project.adminUserId !== userId) { throw new Error( i18n.__( translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT, diff --git a/src/resolvers/qfRoundResolver.ts b/src/resolvers/qfRoundResolver.ts index 14ad60ae5..3201278c4 100644 --- a/src/resolvers/qfRoundResolver.ts +++ b/src/resolvers/qfRoundResolver.ts @@ -35,6 +35,9 @@ export class QfRoundStatsResponse { @Field() matchingPool: number; + + @Field() + qfRound: QfRound; } @ObjectType() @@ -136,6 +139,7 @@ export class QfRoundResolver { uniqueDonors: contributorsCount, allDonationsUsdValue: totalDonationsSum, matchingPool: qfRound.allocatedFund, + qfRound, }; } } diff --git a/src/resolvers/reactionResolver.test.ts b/src/resolvers/reactionResolver.test.ts index 32a0195b4..624a5e08c 100644 --- a/src/resolvers/reactionResolver.test.ts +++ b/src/resolvers/reactionResolver.test.ts @@ -5,6 +5,7 @@ import { graphqlUrl, PROJECT_UPDATE_SEED_DATA, SEED_DATA, + dbIndependentTests, } from '../../test/testUtils'; import { likeProjectQuery, @@ -65,7 +66,13 @@ function likeUnlikeProjectTestCases() { }, ); - beforeEach(async () => { + beforeEach(async function () { + const { title } = this.currentTest?.parent || {}; + + if (title && dbIndependentTests.includes(title)) { + return; + } + firstUserAccessToken = await generateTestAccessToken(USER_DATA.id); projectBefore = await Project.findOne({ where: { id: PROJECT_DATA.id } }); }); @@ -196,7 +203,13 @@ function likeUnlikeProjectUpdateTestCases() { }, ); - beforeEach(async () => { + beforeEach(async function () { + const { title } = this.currentTest?.parent || {}; + + if (title && dbIndependentTests.includes(title)) { + return; + } + firstUserAccessToken = await generateTestAccessToken(USER_DATA.id); projectUpdateBefore = await ProjectUpdate.findOne({ where: { diff --git a/src/resolvers/recurringDonationResolver.test.ts b/src/resolvers/recurringDonationResolver.test.ts index fb263b010..a5c60996a 100644 --- a/src/resolvers/recurringDonationResolver.test.ts +++ b/src/resolvers/recurringDonationResolver.test.ts @@ -19,6 +19,7 @@ import { updateRecurringDonationQuery, updateRecurringDonationQueryById, updateRecurringDonationStatusMutation, + fetchRecurringDonationStatsQuery, } from '../../test/graphqlQueries'; describe( 'createRecurringDonation test cases', @@ -54,6 +55,11 @@ describe( updateRecurringDonationByIdTestCases, ); +describe( + 'getRecurringDonationStatsTestCases test cases', + getRecurringDonationStatsTestCases, +); + function createRecurringDonationTestCases() { it('should create recurringDonation successfully', async () => { const projectOwner = await saveUserDirectlyToDb( @@ -1745,7 +1751,6 @@ function updateRecurringDonationByIdTestCases() { ); }); } - function recurringDonationsByProjectIdTestCases() { it('should sort by the createdAt DESC', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); @@ -2893,3 +2898,235 @@ function updateRecurringDonationStatusTestCases() { ); }); } + +function getRecurringDonationStatsTestCases() { + const lastYear = new Date().getFullYear() - 1; + const beginDate = `${lastYear}-01-01`; + const endDate = `${lastYear}-03-01`; + + it(`should return the correct stats for a given date range (${beginDate} to ${endDate})`, async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + createdAt: new Date(`${lastYear}-01-02`), + }, + }); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + createdAt: new Date(`${lastYear}-01-24`), + }, + }); + + // we are querying from January 1st of last year to the 1st of March of last year + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate, + endDate, + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 0); + assert.equal(stats.totalStreamedUsdValue, 500); + }); + + it(`should return the correct stats for a given date range (${beginDate} -> ${endDate}) and currency`, async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + currency: 'DAI', + createdAt: new Date(`${lastYear}-01-01`), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + currency: 'USDT', + createdAt: new Date(`${lastYear}-02-01`), + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate, + endDate, + currency: 'USDT', + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 0); + assert.equal(stats.totalStreamedUsdValue, 600); + }); + + it(`should return the correct stats for a given date range (${beginDate} -> ${endDate}) with correct active count`, async () => { + const donor1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const donor2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor1.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + currency: 'DAI', + createdAt: new Date(`${lastYear}-01-01`), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor2.id, + totalUsdStreamed: 100, + status: RECURRING_DONATION_STATUS.ACTIVE, + currency: 'DAI', + createdAt: new Date(`${lastYear}-02-01`), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor1.id, + totalUsdStreamed: 200, + currency: 'DAI', + createdAt: new Date(`${lastYear}-02-01`), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor2.id, + totalUsdStreamed: 100, + status: RECURRING_DONATION_STATUS.ACTIVE, + currency: 'USDT', + createdAt: new Date(`${lastYear}-02-01`), + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate, + endDate, + status: RECURRING_DONATION_STATUS.ACTIVE, + currency: 'DAI', + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 2); + assert.equal(stats.totalStreamedUsdValue, 1100); + }); + + it('should return the correct stats for the given date range where beginDate', async () => { + const lastYear15thOfJanuary = new Date(`${lastYear}-01-15T09:00:00`); + + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + createdAt: lastYear15thOfJanuary, + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + createdAt: lastYear15thOfJanuary, + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: `${lastYear}-01-15T09:00:00`, + endDate: `${lastYear}-01-15T09:00:00`, + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + + assert.equal(stats.activeRecurringDonationsCount, 1); + assert.equal(stats.totalStreamedUsdValue, 500); + }); + + it(`should return empty stats for the given date range where beginDate is same as endDate`, async () => { + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: `${lastYear}-04-01`, + endDate: `${lastYear}-05-01`, + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 0); + assert.equal(stats.totalStreamedUsdValue, 0); + }); + + it('should return an error for the given an empty date range', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + createdAt: new Date(), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + createdAt: new Date(), + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: '', + endDate: '', + }, + }); + + assert.isNotNull(result.data.errors); + }); + + it('should return an error for the given an invalid date range', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + createdAt: new Date(), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + createdAt: new Date(), + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: 'invalid date', + endDate: 'invalid date', + }, + }); + + assert.isNotNull(result.data.errors); + }); +} diff --git a/src/resolvers/recurringDonationResolver.ts b/src/resolvers/recurringDonationResolver.ts index a58f4609e..56c63b5fa 100644 --- a/src/resolvers/recurringDonationResolver.ts +++ b/src/resolvers/recurringDonationResolver.ts @@ -4,6 +4,7 @@ import { ArgsType, Ctx, Field, + Float, InputType, Int, Mutation, @@ -39,6 +40,7 @@ import { import { detectAddressChainType } from '../utils/networks'; import { logger } from '../utils/logger'; import { + getRecurringDonationStatsArgsValidator, updateDonationQueryValidator, validateWithJoiSchema, } from '../utils/validators/graphqlQueryValidators'; @@ -154,6 +156,28 @@ class UserRecurringDonations { totalCount: number; } +@Service() +@ArgsType() +class GetRecurringDonationStatsArgs { + @Field(_type => String) + beginDate: string; + + @Field(_type => String) + endDate: string; + + @Field(_type => String, { nullable: true }) + currency?: string; +} + +@ObjectType() +class RecurringDonationStats { + @Field(_type => Float) + activeRecurringDonationsCount: number; + + @Field(_type => Float) + totalStreamedUsdValue: number; +} + @Resolver(_of => AnchorContractAddress) export class RecurringDonationResolver { @Mutation(_returns => RecurringDonation, { nullable: true }) @@ -630,4 +654,42 @@ export class RecurringDonationResolver { throw e; } } + + @Query(_returns => RecurringDonationStats) + async getRecurringDonationStats( + @Args() { beginDate, endDate, currency }: GetRecurringDonationStatsArgs, + ): Promise { + try { + validateWithJoiSchema( + { beginDate, endDate }, + getRecurringDonationStatsArgsValidator, + ); + + const query = RecurringDonation.createQueryBuilder('recurring_donation') + .select([ + 'COUNT(CASE WHEN recurring_donation.status = :active THEN 1 END)', + 'SUM(recurring_donation.totalUsdStreamed)', + ]) + .setParameter('active', 'active') + .where( + `recurring_donation.createdAt >= :beginDate AND recurring_donation.createdAt <= :endDate`, + { beginDate, endDate }, + ); + + if (currency) { + query.andWhere(`recurring_donation.currency = :currency`, { currency }); + } + + const [result] = await query.getRawMany(); + + return { + activeRecurringDonationsCount: parseInt(result.count), + totalStreamedUsdValue: parseFloat(result.sum) || 0, + }; + } catch (e) { + SentryLogger.captureException(e); + logger.error('getRecurringDonationStats() error ', e); + throw e; + } + } } diff --git a/src/resolvers/socialProfilesResolver.test.ts b/src/resolvers/socialProfilesResolver.test.ts index 02466bb18..0afe05808 100644 --- a/src/resolvers/socialProfilesResolver.test.ts +++ b/src/resolvers/socialProfilesResolver.test.ts @@ -33,7 +33,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -63,7 +63,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -99,7 +99,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -147,7 +147,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -181,7 +181,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -216,7 +216,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -251,7 +251,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -371,7 +371,7 @@ function removeSocialProfileMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -406,7 +406,7 @@ function removeSocialProfileMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -458,7 +458,7 @@ function removeSocialProfileMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = diff --git a/src/resolvers/types/project-input.ts b/src/resolvers/types/project-input.ts index 444df5016..251405c37 100644 --- a/src/resolvers/types/project-input.ts +++ b/src/resolvers/types/project-input.ts @@ -32,7 +32,7 @@ class ProjectInput { title: string; @Field({ nullable: true }) - admin?: string; + adminUserId?: number; @Field({ nullable: true }) @MaxLength(PROJECT_DESCRIPTION_MAX_LENGTH, { diff --git a/src/server/adminJs/adminJs-types.ts b/src/server/adminJs/adminJs-types.ts index 254890507..4a13ece68 100644 --- a/src/server/adminJs/adminJs-types.ts +++ b/src/server/adminJs/adminJs-types.ts @@ -52,7 +52,7 @@ export const projectHeaders = [ 'id', 'title', 'slug', - 'admin', + 'adminUserId', 'creationDate', 'updatedAt', 'impactLocation', diff --git a/src/server/adminJs/adminJsPermissions.test.ts b/src/server/adminJs/adminJsPermissions.test.ts index aff23d379..6e30c8d3c 100644 --- a/src/server/adminJs/adminJsPermissions.test.ts +++ b/src/server/adminJs/adminJsPermissions.test.ts @@ -271,7 +271,7 @@ const canAccessAction = ( }; // TODO Should uncomment it after https://github.com/Giveth/impact-graph/issues/1481 ( I commented this to reduce the test execution time) -describe.skip('canAccessUserAction test cases', () => { +describe('AdminJsPermissions', () => { roles.forEach(role => { Object.keys(actionsPerRole[role]).forEach(page => { actions.forEach(action => { diff --git a/src/server/adminJs/tabs/components/ProjectsInQfRound.tsx b/src/server/adminJs/tabs/components/ProjectsInQfRound.tsx index dfec3613f..c5347e8ae 100644 --- a/src/server/adminJs/tabs/components/ProjectsInQfRound.tsx +++ b/src/server/adminJs/tabs/components/ProjectsInQfRound.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { withTheme } from 'styled-components'; import { Section, Label, Link } from '@adminjs/design-system'; diff --git a/src/server/adminJs/tabs/projectsTab.test.ts b/src/server/adminJs/tabs/projectsTab.test.ts index 508f5eeb1..832c5d930 100644 --- a/src/server/adminJs/tabs/projectsTab.test.ts +++ b/src/server/adminJs/tabs/projectsTab.test.ts @@ -433,7 +433,7 @@ function verifyProjectsTestCases() { const projectVerificationForm = await createProjectVerificationForm({ projectId: project.id, - userId: Number(project.admin), + userId: project.adminUserId, }); projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.VERIFIED; @@ -496,7 +496,7 @@ function verifyProjectsTestCases() { const projectVerificationForm = await createProjectVerificationForm({ projectId: project.id, - userId: Number(project.admin), + userId: project.adminUserId, }); projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.DRAFT; @@ -583,7 +583,7 @@ function verifyProjectsTestCases() { }); const projectVerificationForm = await createProjectVerificationForm({ projectId: project.id, - userId: Number(project.admin), + userId: project.adminUserId, }); projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.VERIFIED; diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index fecb8aed3..176ed5ff2 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -479,7 +479,7 @@ const sendProjectsToGoogleSheet = async ( id: project.id, title: project.title, slug: project.slug, - admin: project.admin, + admin: project.adminUserId, creationDate: project.creationDate, updatedAt: project.updatedAt, impactLocation: project.impactLocation || '', @@ -951,7 +951,7 @@ export const projectsTab = { NOTIFICATIONS_EVENT_NAMES.PROJECT_NOT_REVIEWED, ); } - if (request?.payload?.admin !== project?.admin) { + if (Number(request?.payload?.admin) !== project?.adminUserId) { request.payload.adminChanged = true; } @@ -972,7 +972,7 @@ export const projectsTab = { if (project) { if (request?.record?.params?.adminChanged) { const adminUser = await User.findOne({ - where: { id: Number(project.admin) }, + where: { id: project.adminUserId }, }); project.adminUser = adminUser!; await project.save(); diff --git a/src/server/adminJs/tabs/qfRoundTab.ts b/src/server/adminJs/tabs/qfRoundTab.ts index b719a1135..7ac744ff8 100644 --- a/src/server/adminJs/tabs/qfRoundTab.ts +++ b/src/server/adminJs/tabs/qfRoundTab.ts @@ -33,9 +33,19 @@ import { pinFile } from '../../../middleware/pinataUtils'; export const refreshMaterializedViews = async ( response, ): Promise> => { + const projectIds = await getRelatedProjectsOfQfRound( + response.record.params.id, + ); await refreshProjectEstimatedMatchingView(); await refreshProjectDonationSummaryView(); await refreshProjectActualMatchingView(); + response.record = { + ...response.record, + params: { + ...response.record.params, + projectIdsList: projectIds.map(project => project.id).join(','), + }, + }; return response; }; @@ -119,7 +129,7 @@ export const qfRoundTab = { resource: QfRound, options: { properties: { - addProjectIdsList: { + projectIdsList: { type: 'textarea', // projectIds separated By comma isVisible: { @@ -308,10 +318,10 @@ export const qfRoundTab = { request.payload.isActive = qfRound.isActive; } else if ( qfRound.isActive && - request?.payload?.addProjectIdsList?.split(',')?.length > 0 + request?.payload?.projectIdsList?.split(',')?.length > 0 ) { await relateManyProjectsToQfRound({ - projectIds: request.payload.addProjectIdsList.split(','), + projectIds: request.payload.projectIdsList.split(','), qfRound, add: true, }); diff --git a/src/services/chains/evm/draftDonationService.ts b/src/services/chains/evm/draftDonationService.ts index dfbdeb1e9..4007e369a 100644 --- a/src/services/chains/evm/draftDonationService.ts +++ b/src/services/chains/evm/draftDonationService.ts @@ -319,8 +319,9 @@ export async function runDraftDonationMatchWorker() { () => spawn(new Worker('./../../../workers/draftDonationMatchWorker')), { name: 'draftDonationMatchWorker', - concurrency: 4, - size: 2, + concurrency: 1, + maxQueuedJobs: 1, + size: 1, }, ); } diff --git a/src/services/donationService.ts b/src/services/donationService.ts index f88aed761..d9d4a7c66 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -198,9 +198,6 @@ export const updateTotalDonationsOfProject = async ( `, [projectId], ); - - // we want to update the project donation summary view after updating the total donations - refreshProjectDonationSummaryView(); } catch (e) { logger.error('updateTotalDonationsOfAProject error', e); } diff --git a/src/services/googleSheets.ts b/src/services/googleSheets.ts index 64ef44457..abb340cd7 100644 --- a/src/services/googleSheets.ts +++ b/src/services/googleSheets.ts @@ -10,7 +10,7 @@ interface ProjectExport { id: number; title: string; slug?: string | null; - admin?: string | null; + adminUserId?: number | null; creationDate: Date; updatedAt: Date; impactLocation?: string | null; diff --git a/src/services/onramper/donationService.ts b/src/services/onramper/donationService.ts index 443011b72..be187b74b 100644 --- a/src/services/onramper/donationService.ts +++ b/src/services/onramper/donationService.ts @@ -147,7 +147,7 @@ export const createFiatDonationFromOnramper = async ( // After updating price we update totalDonations await updateTotalDonationsOfProject(project.id); - await updateUserTotalReceived(Number(project.admin)); + await updateUserTotalReceived(project.adminUserId); } catch (e) { SentryLogger.captureException(e); logger.error('createFiatDonationFromOnramper() error', e); diff --git a/src/services/poignArt/syncPoignArtDonationCronJob.ts b/src/services/poignArt/syncPoignArtDonationCronJob.ts index 63557813f..ae3df3893 100644 --- a/src/services/poignArt/syncPoignArtDonationCronJob.ts +++ b/src/services/poignArt/syncPoignArtDonationCronJob.ts @@ -63,7 +63,7 @@ const importPoignArtDonations = async () => { for (const poignArtWithdrawal of poignArtWithdrawals) { await createPoignArtDonationInDb(poignArtWithdrawal, unchainProject); } - await updateUserTotalReceived(Number(unchainProject.admin)); + await updateUserTotalReceived(unchainProject.adminUserId); await updateTotalDonationsOfProject(unchainProject.id); } catch (e) { logger.error('importPoignArtDonations() error', e); diff --git a/src/services/projectUpdatesService.test.ts b/src/services/projectUpdatesService.test.ts index 4a1ff8ab4..e7e24c353 100644 --- a/src/services/projectUpdatesService.test.ts +++ b/src/services/projectUpdatesService.test.ts @@ -31,7 +31,7 @@ function updateTotalProjectUpdatesOfAProjectTestCases() { INSERT INTO public.project_update ( "userId","projectId",content,title,"createdAt","isMain" ) VALUES ( - ${Number(project.admin)}, ${project.id}, '', '', '${ + ${project.adminUserId}, ${project.id}, '', '', '${ new Date().toISOString().split('T')[0] }', false )`); diff --git a/src/services/recurringDonationService.ts b/src/services/recurringDonationService.ts index 537fc55e8..284174b6c 100644 --- a/src/services/recurringDonationService.ts +++ b/src/services/recurringDonationService.ts @@ -37,6 +37,7 @@ import { updateUserTotalDonated, updateUserTotalReceived } from './userService'; import config from '../config'; import { User } from '../entities/user'; import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; +import { relatedActiveQfRoundForProject } from './qfRoundService'; // Initially it will only be monthly data export const priceDisplay = 'month'; @@ -212,17 +213,16 @@ export const createRelatedDonationsToStream = async ( amount: donation.amount, }); - // TODO - uncomment this when QF is enabled - // const activeQfRoundForProject = await relatedActiveQfRoundForProject( - // project.id, - // ); - // - // if ( - // activeQfRoundForProject && - // activeQfRoundForProject.isEligibleNetwork(networkId) - // ) { - // donation.qfRound = activeQfRoundForProject; - // } + const activeQfRoundForProject = await relatedActiveQfRoundForProject( + project.id, + ); + + if ( + activeQfRoundForProject && + activeQfRoundForProject.isEligibleNetwork(networkId) + ) { + donation.qfRound = activeQfRoundForProject; + } const { givbackFactor, projectRank, bottomRankInRound, powerRound } = await calculateGivbackFactor(project.id); diff --git a/src/services/the-giving-blocks/syncProjectsCronJob.ts b/src/services/the-giving-blocks/syncProjectsCronJob.ts index e591ba81f..3ae30789b 100644 --- a/src/services/the-giving-blocks/syncProjectsCronJob.ts +++ b/src/services/the-giving-blocks/syncProjectsCronJob.ts @@ -134,7 +134,6 @@ const createGivingProject = async (data: { image: givingBlockProject.logo, slugHistory: [], givingBlocksId: String(givingBlockProject.id), - admin: adminId, statusId: activeStatus?.id, qualityScore, totalDonations: 0, diff --git a/src/services/userService.test.ts b/src/services/userService.test.ts index 63ee3d723..85e386344 100644 --- a/src/services/userService.test.ts +++ b/src/services/userService.test.ts @@ -82,7 +82,7 @@ function updateUserTotalReceivedTestCases() { }).save(); await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, organizationLabel: ORGANIZATION_LABELS.GIVING_BLOCK, totalDonations: 180, }); diff --git a/src/types/projectSocialMediaType.ts b/src/types/projectSocialMediaType.ts index 38ede741c..af901a2bf 100644 --- a/src/types/projectSocialMediaType.ts +++ b/src/types/projectSocialMediaType.ts @@ -11,6 +11,7 @@ export enum ProjectSocialMediaType { FARCASTER = 'FARCASTER', LENS = 'LENS', WEBSITE = 'WEBSITE', + TELEGRAM = 'TELEGRAM', } registerEnumType(ProjectSocialMediaType, { diff --git a/src/user/MeResolver.ts b/src/user/MeResolver.ts index ab214002a..c31d2c16f 100644 --- a/src/user/MeResolver.ts +++ b/src/user/MeResolver.ts @@ -50,7 +50,7 @@ export class MeResolver { const user = await getLoggedInUser(ctx); const projects = this.projectRepository.find({ - where: { admin: user.id?.toString() }, + where: { adminUserId: user.id }, // relations: ['status', 'donations', 'reactions'], relations: ['status', 'reactions'], order: { diff --git a/src/utils/validators/graphqlQueryValidators.ts b/src/utils/validators/graphqlQueryValidators.ts index 95da5af03..ea232be2b 100644 --- a/src/utils/validators/graphqlQueryValidators.ts +++ b/src/utils/validators/graphqlQueryValidators.ts @@ -166,6 +166,17 @@ export const updateDonationQueryValidator = Joi.object({ status: Joi.string().valid(DONATION_STATUS.VERIFIED, DONATION_STATUS.FAILED), }); +export const getRecurringDonationStatsArgsValidator = Joi.object({ + beginDate: Joi.string().pattern(resourcePerDateRegex).messages({ + 'string.base': errorMessages.INVALID_FROM_DATE, + 'string.pattern.base': errorMessages.INVALID_DATE_FORMAT, + }), + endDate: Joi.string().pattern(resourcePerDateRegex).messages({ + 'string.base': errorMessages.INVALID_FROM_DATE, + 'string.pattern.base': errorMessages.INVALID_DATE_FORMAT, + }), +}); + export const createProjectVerificationRequestValidator = Joi.object({ slug: Joi.string().required(), }); diff --git a/src/utils/validators/projectValidator.ts b/src/utils/validators/projectValidator.ts index 9352b3027..e1027a2b4 100644 --- a/src/utils/validators/projectValidator.ts +++ b/src/utils/validators/projectValidator.ts @@ -160,7 +160,7 @@ async function isSmartContract(provider, projectWalletAddress) { export const canUserVisitProject = ( project?: Project | null, - userId?: string, + userId?: number, ) => { if (!project) { throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); @@ -169,7 +169,7 @@ export const canUserVisitProject = ( (project.status.id === ProjStatus.drafted || project.status.id === ProjStatus.cancelled) && // If project is draft or cancelled, just owner can view it - project.admin !== userId + project.adminUserId !== userId ) { throw new Error( i18n.__( diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 3e9146d6a..094a908d3 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -115,7 +115,7 @@ export const createProjectQuery = ` title description descriptionSummary - admin + adminUserId image impactLocation slug @@ -166,7 +166,7 @@ export const updateProjectQuery = ` verified slugHistory creationDate - admin + adminUserId walletAddress impactLocation categories { @@ -203,7 +203,7 @@ export const addRecipientAddressToProjectQuery = ` slugHistory creationDate updatedAt - admin + adminUserId walletAddress impactLocation categories { @@ -672,6 +672,10 @@ export const fetchAllDonationsQuery = ` anonymous valueUsd amount + recurringDonation{ + id + txHash + } user { id walletAddress @@ -683,7 +687,7 @@ export const fetchAllDonationsQuery = ` reviewStatus verified slug - admin + adminUserId title categories { name @@ -777,7 +781,7 @@ export const fetchFeaturedProjects = ` slug creationDate updatedAt - admin + adminUserId description walletAddress impactLocation @@ -873,7 +877,7 @@ export const fetchMultiFilterAllProjectsQuery = ` descriptionSummary creationDate updatedAt - admin + adminUserId description walletAddress impactLocation @@ -978,6 +982,12 @@ export const qfRoundStatsQuery = ` uniqueDonors allDonationsUsdValue matchingPool + qfRound{ + allocatedFund + allocatedFundUSD + allocatedFundUSDPreferred + allocatedTokenSymbol + } } } `; @@ -1020,7 +1030,7 @@ export const fetchProjectBySlugQuery = ` slug creationDate updatedAt - admin + adminUserId description walletAddress impactLocation @@ -1077,53 +1087,6 @@ export const fetchProjectBySlugQuery = ` name isActive } - projectVerificationForm { - status - id - isTermAndConditionsAccepted - emailConfirmationTokenExpiredAt - email - emailConfirmationToken - emailConfirmationSent - emailConfirmationSentAt - emailConfirmedAt - emailConfirmed - projectRegistry { - organizationDescription - isNonProfitOrganization - organizationCountry - organizationWebsite - attachments - organizationName - } - personalInfo { - email - walletAddress - fullName - } - projectContacts { - name - url - } - milestones { - mission - foundationDate - achievedMilestones - achievedMilestonesProofs - problem - plans - impact - } - managingFunds { - description - relatedAddresses { - address - networkId - chainType - title - } - } - } status { id symbol @@ -1157,6 +1120,7 @@ export const fetchProjectBySlugQuery = ` email firstName walletAddress + email } totalReactions totalDonations @@ -1194,7 +1158,7 @@ export const fetchSimilarProjectsBySlugQuery = ` slug creationDate updatedAt - admin + adminUserId description walletAddress impactLocation @@ -1255,7 +1219,7 @@ export const fetchLikedProjectsQuery = ` slug creationDate updatedAt - admin + adminUserId description walletAddress impactLocation @@ -1502,7 +1466,7 @@ export const projectsBySlugsQuery = ` image slug creationDate - admin + adminUserId walletAddress impactLocation listed @@ -1550,7 +1514,7 @@ export const projectsByUserIdQuery = ` image slug creationDate - admin + adminUserId walletAddress impactLocation listed @@ -1654,7 +1618,7 @@ export const projectByIdQuery = ` reviewStatus description, walletAddress - admin + adminUserId categories{ name } @@ -2300,6 +2264,8 @@ export const fetchQFArchivedRounds = ` slug isActive allocatedFund + allocatedFundUSD + allocatedTokenSymbol eligibleNetworks beginDate endDate @@ -2422,3 +2388,20 @@ export const updateRecurringDonationQuery = ` } } `; + +export const fetchRecurringDonationStatsQuery = ` + query ( + $beginDate: String! + $endDate: String! + $currency: String + ) { + getRecurringDonationStats( + beginDate: $beginDate + endDate: $endDate + currency: $currency + ) { + totalStreamedUsdValue, + activeRecurringDonationsCount, + } + } +`; diff --git a/test/testUtils.ts b/test/testUtils.ts index 58ba6b75e..bff29abb5 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -123,7 +123,7 @@ export interface CreateProjectData { title: string; slug: string; description: string; - admin: string; + adminUserId: number; // relatedAddresses: RelatedAddressInputType[]; walletAddress: string; categories: string[]; @@ -233,7 +233,7 @@ export const saveProjectDirectlyToDb = async ( owner || ((await User.findOne({ where: { - id: Number(projectData.admin), + id: projectData.adminUserId, }, })) as User); const categoriesPromise = Promise.all( @@ -253,9 +253,8 @@ export const saveProjectDirectlyToDb = async ( status, organization, categories, - users: [user], adminUser: user, - admin: String(user.id), + adminUserId: user.id, }).save(); if (projectData.networkId) { @@ -315,7 +314,7 @@ export const createProjectData = (): CreateProjectData => { updatedAt: new Date(), slug: title, // firstUser's id - admin: '1', + adminUserId: 1, qualityScore: 30, // just need the initial value to be different than 0 totalDonations: 10, @@ -398,7 +397,7 @@ export const SEED_DATA = { slug: 'first-project', description: 'first description', id: 1, - admin: '1', + adminUserId: 1, }, SECOND_PROJECT: { ...createProjectData(), @@ -406,7 +405,7 @@ export const SEED_DATA = { slug: 'second-project', description: 'second description', id: 2, - admin: '2', + adminUserId: 2, }, TRANSAK_PROJECT: { ...createProjectData(), @@ -414,7 +413,7 @@ export const SEED_DATA = { slug: 'transak-project', description: 'transak description', id: 3, - admin: '3', + adminUserId: 3, }, FOURTH_PROJECT: { ...createProjectData(), @@ -422,7 +421,7 @@ export const SEED_DATA = { slug: 'forth-project', description: 'forth description', id: 4, - admin: '1', + adminUserId: 1, }, FIFTH_PROJECT: { ...createProjectData(), @@ -430,7 +429,7 @@ export const SEED_DATA = { slug: 'fifth-project', description: 'forth description', id: 5, - admin: '1', + adminUserId: 1, }, SIXTH_PROJECT: { ...createProjectData(), @@ -438,7 +437,7 @@ export const SEED_DATA = { slug: 'sixth-project', description: 'forth description', id: 6, - admin: '1', + adminUserId: 1, }, NON_VERIFIED_PROJECT: { ...createProjectData(), @@ -446,8 +445,8 @@ export const SEED_DATA = { slug: 'non-verified-project', description: 'non verified description', id: 7, - admin: '1', verified: false, + adminUserId: 1, }, MAIN_CATEGORIES: ['drink', 'food', 'nonProfit'], NON_PROFIT_SUB_CATEGORIES: [CATEGORY_NAMES.registeredNonProfits], @@ -1958,6 +1957,7 @@ export const saveRecurringDonationDirectlyToDb = async (params?: { donorId, projectId, anchorContractAddressId, + createdAt: params?.donationData?.createdAt || moment(), }).save(); }; @@ -2011,3 +2011,6 @@ export function generateRandomSolanaTxHash() { const length = Math.floor(Math.random() * 3) + 86; return generateRandomAlphanumeric(length); } + +// list of test cases titles that doesn't require DB interaction +export const dbIndependentTests = ['AdminJsPermissions']; From 47995519cfba3511814170840b498c71a3d005f3 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Wed, 22 May 2024 15:23:04 +0330 Subject: [PATCH 03/12] WIP add base network --- .github/workflows/develop-pipeline.yml | 2 + .github/workflows/master-pipeline.yml | 2 + .github/workflows/staging-pipeline.yml | 2 + config/example.env | 3 + config/test.env | 3 + .../1716367359560-add_base_chain_tokens.ts | 57 +++++++++++++++++++ migration/data/seedTokens.ts | 13 +++++ src/entities/project.ts | 1 + src/provider.ts | 3 + src/resolvers/donationResolver.test.ts | 2 +- src/resolvers/projectResolver.ts | 5 ++ 11 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 migration/1716367359560-add_base_chain_tokens.ts diff --git a/.github/workflows/develop-pipeline.yml b/.github/workflows/develop-pipeline.yml index bf6209ae8..58a3f5af2 100644 --- a/.github/workflows/develop-pipeline.yml +++ b/.github/workflows/develop-pipeline.yml @@ -65,6 +65,8 @@ jobs: CELO_ALFAJORES_SCAN_API_KEY: ${{ secrets.CELO_ALFAJORES_SCAN_API_KEY }} ARBITRUM_SCAN_API_KEY: ${{ secrets.ARBITRUM_SCAN_API_KEY }} ARBITRUM_SEPOLIA_SCAN_API_KEY: ${{ secrets.ARBITRUM_SEPOLIA_SCAN_API_KEY }} + BASE_SCAN_API_KEY: ${{ secrets.BASE_SCAN_API_KEY }} + BASE_SEPOLIA_SCAN_API_KEY: ${{ secrets.BASE_SEPOLIA_SCAN_API_KEY }} MORDOR_ETC_TESTNET: ${{ secrets.MORDOR_ETC_TESTNET }} ETC_NODE_HTTP_URL: ${{ secrets.ETC_NODE_HTTP_URL }} SOLANA_TEST_NODE_RPC_URL: ${{ secrets.SOLANA_TEST_NODE_RPC_URL }} diff --git a/.github/workflows/master-pipeline.yml b/.github/workflows/master-pipeline.yml index 6bcebcb83..433ab725d 100644 --- a/.github/workflows/master-pipeline.yml +++ b/.github/workflows/master-pipeline.yml @@ -102,6 +102,8 @@ jobs: CELO_ALFAJORES_SCAN_API_KEY: ${{ secrets.CELO_ALFAJORES_SCAN_API_KEY }} ARBITRUM_SCAN_API_KEY: ${{ secrets.ARBITRUM_SCAN_API_KEY }} ARBITRUM_SEPOLIA_SCAN_API_KEY: ${{ secrets.ARBITRUM_SEPOLIA_SCAN_API_KEY }} + BASE_SCAN_API_KEY: ${{ secrets.BASE_SCAN_API_KEY }} + BASE_SEPOLIA_SCAN_API_KEY: ${{ secrets.BASE_SEPOLIA_SCAN_API_KEY }} MORDOR_ETC_TESTNET: ${{ secrets.MORDOR_ETC_TESTNET }} ETC_NODE_HTTP_URL: ${{ secrets.ETC_NODE_HTTP_URL }} DROP_DATABASE: ${{ secrets.DROP_DATABASE_DURING_TEST_PROD }} diff --git a/.github/workflows/staging-pipeline.yml b/.github/workflows/staging-pipeline.yml index 388f4660c..b49337e84 100644 --- a/.github/workflows/staging-pipeline.yml +++ b/.github/workflows/staging-pipeline.yml @@ -102,6 +102,8 @@ jobs: CELO_ALFAJORES_SCAN_API_KEY: ${{ secrets.CELO_ALFAJORES_SCAN_API_KEY }} ARBITRUM_SCAN_API_KEY: ${{ secrets.ARBITRUM_SCAN_API_KEY }} ARBITRUM_SEPOLIA_SCAN_API_KEY: ${{ secrets.ARBITRUM_SEPOLIA_SCAN_API_KEY }} + BASE_SCAN_API_KEY: ${{ secrets.BASE_SCAN_API_KEY }} + BASE_SEPOLIA_SCAN_API_KEY: ${{ secrets.BASE_SEPOLIA_SCAN_API_KEY }} MORDOR_ETC_TESTNET: ${{ secrets.MORDOR_ETC_TESTNET }} ETC_NODE_HTTP_URL: ${{ secrets.ETC_NODE_HTTP_URL }} DROP_DATABASE: ${{ secrets.DROP_DATABASE_DURING_TEST_STAGING }} diff --git a/config/example.env b/config/example.env index 8c3393325..2c7ee4613 100644 --- a/config/example.env +++ b/config/example.env @@ -307,3 +307,6 @@ ENABLE_DRAFT_RECURRING_DONATION=true DRAFT_RECURRING_DONATION_MATCH_EXPIRATION_HOURS=24 OPTIMISTIC_SEPOLIA_SCAN_API_KEY= + +BASE_SCAN_API_KEY= +BASE_SEPOLIA_SCAN_API_KEY= diff --git a/config/test.env b/config/test.env index ef55172b7..cd1bf147e 100644 --- a/config/test.env +++ b/config/test.env @@ -236,3 +236,6 @@ DRAFT_RECURRING_DONATION_MATCH_EXPIRATION_HOURS=24 OPTIMISTIC_SEPOLIA_SCAN_API_KEY= SUPER_FLUID_ADAPTER=superfluid + +BASE_SCAN_API_KEY= +BASE_SEPOLIA_SCAN_API_KEY= diff --git a/migration/1716367359560-add_base_chain_tokens.ts b/migration/1716367359560-add_base_chain_tokens.ts new file mode 100644 index 000000000..b12b38986 --- /dev/null +++ b/migration/1716367359560-add_base_chain_tokens.ts @@ -0,0 +1,57 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { Token } from '../src/entities/token'; +import seedTokens from './data/seedTokens'; +import config from '../src/config'; +import { NETWORK_IDS } from '../src/provider'; + +export class AddBaseChainTokens1716367359560 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + const environment = config.get('ENVIRONMENT') as string; + + const networkId = + environment === 'production' + ? NETWORK_IDS.ARBITRUM_MAINNET + : NETWORK_IDS.ARBITRUM_SEPOLIA; + + await queryRunner.manager.save( + Token, + seedTokens + .filter(token => token.networkId === networkId) + .map(token => { + const t = { + ...token, + }; + t.address = t.address?.toLowerCase(); + delete t.chainType; + return t; + }), + ); + const tokens = await queryRunner.query(` + SELECT * FROM token + WHERE "networkId" = ${networkId} + `); + const givethOrganization = ( + await queryRunner.query(`SELECT * FROM organization + WHERE label='giveth'`) + )[0]; + + const traceOrganization = ( + await queryRunner.query(`SELECT * FROM organization + WHERE label='trace'`) + )[0]; + + for (const token of tokens) { + // Add all Base tokens to Giveth organization + await queryRunner.query(`INSERT INTO organization_tokens_token ("tokenId","organizationId") VALUES + (${token.id}, ${givethOrganization.id}), + (${token.id}, ${traceOrganization.id}) + ;`); + } + } + + public async down(_queryRunner: QueryRunner): Promise { + // + } + +} diff --git a/migration/data/seedTokens.ts b/migration/data/seedTokens.ts index be4fe6dec..5b403f4e5 100644 --- a/migration/data/seedTokens.ts +++ b/migration/data/seedTokens.ts @@ -1593,6 +1593,19 @@ const seedTokens: ITokenData[] = [ networkId: NETWORK_IDS.ARBITRUM_MAINNET, coingeckoId: 'cartesi', }, + + + + + // BASE Sepolia + { + name: 'BASE', + symbol: 'ETH', + address: '0x319f865b287fCC10b30d8cE6144e8b6D1b476999', + decimals: 18, + networkId: NETWORK_IDS.BASE_SEPOLIA, + coingeckoId: 'cartesi', + }, ]; export default seedTokens; diff --git a/src/entities/project.ts b/src/entities/project.ts index 109ebbe29..6935c0bcc 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -92,6 +92,7 @@ export enum FilterField { AcceptFundOnETC = 'acceptFundOnETC', AcceptFundOnCelo = 'acceptFundOnCelo', AcceptFundOnArbitrum = 'acceptFundOnArbitrum', + AcceptFundOnBase = 'acceptFundOnBase', AcceptFundOnOptimism = 'acceptFundOnOptimism', AcceptFundOnSolana = 'acceptFundOnSolana', GivingBlock = 'fromGivingBlock', diff --git a/src/provider.ts b/src/provider.ts index a3b972971..56a0c8e4c 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -21,6 +21,9 @@ export const NETWORK_IDS = { ARBITRUM_MAINNET: 42161, ARBITRUM_SEPOLIA: 421614, + BASE_MAINNET: 8453, + BASE_SEPOLIA: 84532, + // https://docs.particle.network/developers/other-services/node-service/solana-api SOLANA_MAINNET: 101, SOLANA_TESTNET: 102, diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 10edd7f07..8f7f184bb 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -2611,7 +2611,7 @@ function createDonationTestCases() { ); assert.equal( saveDonationResponse.data.errors[0].message, - '"transactionNetworkId" must be one of [1, 3, 5, 100, 137, 10, 11155420, 56, 42220, 44787, 61, 63, 42161, 421614, 101, 102, 103]', + '"transactionNetworkId" must be one of [1, 3, 5, 100, 137, 10, 11155420, 56, 42220, 44787, 61, 63, 42161, 421614, 8453, 84532, 101, 102, 103]', ); }); it('should not throw exception when currency is not valid when currency is USDC.e', async () => { diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index e68fc17e8..33fa248a5 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -527,6 +527,11 @@ export class ProjectResolver { networkIds.push(NETWORK_IDS.ARBITRUM_SEPOLIA); return; + case FilterField.AcceptFundOnBase: + networkIds.push(NETWORK_IDS.BASE_MAINNET); + networkIds.push(NETWORK_IDS.BASE_SEPOLIA); + return; + case FilterField.AcceptFundOnPolygon: networkIds.push(NETWORK_IDS.POLYGON); return; From f03ab1fb474fa2f7003b486e55bbc7d75b5afb46 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Thu, 23 May 2024 10:41:58 +0330 Subject: [PATCH 04/12] Continue on integrating with base chain --- config/example.env | 6 +- config/test.env | 9 ++- .../1716367359560-add_base_chain_tokens.ts | 72 +++++++++---------- migration/data/seedTokens.ts | 3 - src/provider.ts | 8 +++ .../projectVerificationFormResolver.test.ts | 10 +++ src/server/adminJs/tabs/donationTab.ts | 2 + src/server/adminJs/tabs/qfRoundTab.ts | 2 + src/server/adminJs/tabs/tokenTab.ts | 2 + src/services/chains/index.test.ts | 38 ++++++++++ src/utils/networksConfig.ts | 2 + .../validators/graphqlQueryValidators.ts | 2 + src/utils/validators/projectValidator.ts | 2 + test/pre-test-scripts.ts | 28 ++++++++ test/testUtils.ts | 18 +++++ 15 files changed, 159 insertions(+), 45 deletions(-) diff --git a/config/example.env b/config/example.env index 2c7ee4613..834fb7fb3 100644 --- a/config/example.env +++ b/config/example.env @@ -308,5 +308,7 @@ DRAFT_RECURRING_DONATION_MATCH_EXPIRATION_HOURS=24 OPTIMISTIC_SEPOLIA_SCAN_API_KEY= -BASE_SCAN_API_KEY= -BASE_SEPOLIA_SCAN_API_KEY= +BASE_SCAN_API_URL=https://api.basescan.org/api +BASE_SCAN_API_KEY=0000000000000000000000000000000000 +BASE_SEPOLIA_SCAN_API_URL=https://api-sepolia.basescan.org/api +BASE_SEPOLIA_SCAN_API_KEY=0000000000000000000000000000000000 diff --git a/config/test.env b/config/test.env index cd1bf147e..6092dca7c 100644 --- a/config/test.env +++ b/config/test.env @@ -47,6 +47,12 @@ ARBITRUM_SCAN_API_URL=https://api.arbiscan.io/api ARBITRUM_SCAN_API_KEY=0000000000000000000000000000000000 ARBITRUM_SEPOLIA_SCAN_API_URL=https://api-sepolia.arbiscan.io/api ARBITRUM_SEPOLIA_SCAN_API_KEY=0000000000000000000000000000000000 + +BASE_SCAN_API_URL=https://api.basescan.org/api +BASE_SCAN_API_KEY=0000000000000000000000000000000000 +BASE_SEPOLIA_SCAN_API_URL=https://api-sepolia.basescan.org/api +BASE_SEPOLIA_SCAN_API_KEY=0000000000000000000000000000000000 + GNOSISSCAN_API_URL=https://api.gnosisscan.io/api ETHERSCAN_API_KEY=00000000000000000000000000000000 GNOSISSCAN_API_KEY=0000000000000000000000000000000000 @@ -236,6 +242,3 @@ DRAFT_RECURRING_DONATION_MATCH_EXPIRATION_HOURS=24 OPTIMISTIC_SEPOLIA_SCAN_API_KEY= SUPER_FLUID_ADAPTER=superfluid - -BASE_SCAN_API_KEY= -BASE_SEPOLIA_SCAN_API_KEY= diff --git a/migration/1716367359560-add_base_chain_tokens.ts b/migration/1716367359560-add_base_chain_tokens.ts index b12b38986..dbc369134 100644 --- a/migration/1716367359560-add_base_chain_tokens.ts +++ b/migration/1716367359560-add_base_chain_tokens.ts @@ -5,53 +5,51 @@ import config from '../src/config'; import { NETWORK_IDS } from '../src/provider'; export class AddBaseChainTokens1716367359560 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { - const environment = config.get('ENVIRONMENT') as string; - - const networkId = - environment === 'production' - ? NETWORK_IDS.ARBITRUM_MAINNET - : NETWORK_IDS.ARBITRUM_SEPOLIA; - - await queryRunner.manager.save( - Token, - seedTokens - .filter(token => token.networkId === networkId) - .map(token => { - const t = { - ...token, - }; - t.address = t.address?.toLowerCase(); - delete t.chainType; - return t; - }), - ); - const tokens = await queryRunner.query(` + public async up(queryRunner: QueryRunner): Promise { + const environment = config.get('ENVIRONMENT') as string; + + const networkId = + environment === 'production' + ? NETWORK_IDS.ARBITRUM_MAINNET + : NETWORK_IDS.ARBITRUM_SEPOLIA; + + await queryRunner.manager.save( + Token, + seedTokens + .filter(token => token.networkId === networkId) + .map(token => { + const t = { + ...token, + }; + t.address = t.address?.toLowerCase(); + delete t.chainType; + return t; + }), + ); + const tokens = await queryRunner.query(` SELECT * FROM token WHERE "networkId" = ${networkId} `); - const givethOrganization = ( - await queryRunner.query(`SELECT * FROM organization + const givethOrganization = ( + await queryRunner.query(`SELECT * FROM organization WHERE label='giveth'`) - )[0]; + )[0]; - const traceOrganization = ( - await queryRunner.query(`SELECT * FROM organization + const traceOrganization = ( + await queryRunner.query(`SELECT * FROM organization WHERE label='trace'`) - )[0]; + )[0]; - for (const token of tokens) { - // Add all Base tokens to Giveth organization - await queryRunner.query(`INSERT INTO organization_tokens_token ("tokenId","organizationId") VALUES + for (const token of tokens) { + // Add all Base tokens to Giveth organization + await queryRunner.query(`INSERT INTO organization_tokens_token ("tokenId","organizationId") VALUES (${token.id}, ${givethOrganization.id}), (${token.id}, ${traceOrganization.id}) ;`); - } - } - - public async down(_queryRunner: QueryRunner): Promise { - // } + } + public async down(_queryRunner: QueryRunner): Promise { + // + } } diff --git a/migration/data/seedTokens.ts b/migration/data/seedTokens.ts index 5b403f4e5..4e42698f5 100644 --- a/migration/data/seedTokens.ts +++ b/migration/data/seedTokens.ts @@ -1594,9 +1594,6 @@ const seedTokens: ITokenData[] = [ coingeckoId: 'cartesi', }, - - - // BASE Sepolia { name: 'BASE', diff --git a/src/provider.ts b/src/provider.ts index 56a0c8e4c..d097c7e11 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -401,6 +401,14 @@ export function getBlockExplorerApiUrl(networkId: number): string { apiUrl = config.get('ARBITRUM_SEPOLIA_SCAN_API_URL'); apiKey = config.get('ARBITRUM_SEPOLIA_SCAN_API_KEY'); break; + case NETWORK_IDS.BASE_MAINNET: + apiUrl = config.get('BASE_SCAN_API_URL'); + apiKey = config.get('BASE_SCAN_API_KEY'); + break; + case NETWORK_IDS.BASE_SEPOLIA: + apiUrl = config.get('BASE_SEPOLIA_SCAN_API_URL'); + apiKey = config.get('BASE_SEPOLIA_SCAN_API_KEY'); + break; default: throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_NETWORK_ID)); } diff --git a/src/resolvers/projectVerificationFormResolver.test.ts b/src/resolvers/projectVerificationFormResolver.test.ts index 695e86d59..b14dc0b59 100644 --- a/src/resolvers/projectVerificationFormResolver.test.ts +++ b/src/resolvers/projectVerificationFormResolver.test.ts @@ -321,6 +321,16 @@ function updateProjectVerificationFormMutationTestCases() { networkId: NETWORK_IDS.ARBITRUM_SEPOLIA, title: 'test title', }, + { + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.BASE_MAINNET, + title: 'test title', + }, + { + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.BASE_SEPOLIA, + title: 'test title', + }, { address: generateRandomEtheriumAddress(), networkId: NETWORK_IDS.ETC, diff --git a/src/server/adminJs/tabs/donationTab.ts b/src/server/adminJs/tabs/donationTab.ts index 5e4574756..ffc2b4f20 100644 --- a/src/server/adminJs/tabs/donationTab.ts +++ b/src/server/adminJs/tabs/donationTab.ts @@ -603,6 +603,8 @@ export const donationTab = { { value: NETWORK_IDS.CELO_ALFAJORES, label: 'Alfajores' }, { value: NETWORK_IDS.ARBITRUM_MAINNET, label: 'Arbitrum' }, { value: NETWORK_IDS.ARBITRUM_SEPOLIA, label: 'Arbitrum Sepolia' }, + { value: NETWORK_IDS.BASE_MAINNET, label: 'Base' }, + { value: NETWORK_IDS.BASE_SEPOLIA, label: 'Base Sepolia' }, ], isVisible: { list: true, diff --git a/src/server/adminJs/tabs/qfRoundTab.ts b/src/server/adminJs/tabs/qfRoundTab.ts index 7ac744ff8..3a6ed6eb6 100644 --- a/src/server/adminJs/tabs/qfRoundTab.ts +++ b/src/server/adminJs/tabs/qfRoundTab.ts @@ -121,6 +121,8 @@ const availableNetworkValues = [ }, { value: NETWORK_IDS.ARBITRUM_MAINNET, label: 'ARBITRUM MAINNET' }, { value: NETWORK_IDS.ARBITRUM_SEPOLIA, label: 'ARBITRUM SEPOLIA' }, + { value: NETWORK_IDS.BASE_MAINNET, label: 'BASE MAINNET' }, + { value: NETWORK_IDS.BASE_SEPOLIA, label: 'BASE SEPOLIA' }, { value: NETWORK_IDS.XDAI, label: 'XDAI' }, { value: NETWORK_IDS.BSC, label: 'BSC' }, ]; diff --git a/src/server/adminJs/tabs/tokenTab.ts b/src/server/adminJs/tabs/tokenTab.ts index b6a83f959..a486b660c 100644 --- a/src/server/adminJs/tabs/tokenTab.ts +++ b/src/server/adminJs/tabs/tokenTab.ts @@ -197,6 +197,8 @@ export const generateTokenTab = async () => { }, { value: NETWORK_IDS.ARBITRUM_MAINNET, label: 'ARBITRUM MAINNET' }, { value: NETWORK_IDS.ARBITRUM_SEPOLIA, label: 'ARBITRUM SEPOLIA' }, + { value: NETWORK_IDS.BASE_MAINNET, label: 'BASE MAINNET' }, + { value: NETWORK_IDS.BASE_SEPOLIA, label: 'BASE SEPOLIA' }, { value: NETWORK_IDS.XDAI, label: 'XDAI' }, { value: NETWORK_IDS.BSC, label: 'BSC' }, { value: NETWORK_IDS.ETC, label: 'Ethereum Classic' }, diff --git a/src/services/chains/index.test.ts b/src/services/chains/index.test.ts index dd7958ac4..fc03fa910 100644 --- a/src/services/chains/index.test.ts +++ b/src/services/chains/index.test.ts @@ -540,6 +540,44 @@ function getTransactionDetailTestCases() { assert.equal(transactionInfo.amount, amount); }); + it('should return transaction detail for normal transfer on Base Mainnet', async () => { + // https://basescan.org/tx/0x1cbf53e5a9a0874b9ad97316e4f2e1782e24bec318bacd183d3f48052bfe1523 + + const amount = 0.0032; + const transactionInfo = await getTransactionInfoFromNetwork({ + txHash: + '0x1cbf53e5a9a0874b9ad97316e4f2e1782e24bec318bacd183d3f48052bfe1523', + symbol: 'ETH', + networkId: NETWORK_IDS.BASE_MAINNET, + fromAddress: '0xbaed383ede0e5d9d72430661f3285daa77e9439f', + toAddress: '0xa5401000d255dbb154deb756b82dd5105486d8c9', + amount, + timestamp: 1716445331, + }); + assert.isOk(transactionInfo); + assert.equal(transactionInfo.currency, 'ETH'); + assert.equal(transactionInfo.amount, amount); + }); + + it('should return transaction detail for normal transfer on Base Sepolia', async () => { + // https://sepolia.basescan.org/tx/0x66fdfe46de46fa1fbb77de642cc778cafc85943204039f69694aee6121f764f4 + + const amount = 0.001; + const transactionInfo = await getTransactionInfoFromNetwork({ + txHash: + '0x66fdfe46de46fa1fbb77de642cc778cafc85943204039f69694aee6121f764f4', + symbol: 'ETH', + networkId: NETWORK_IDS.BASE_SEPOLIA, + fromAddress: '0x9cab0c7ff1c6250e641f4dcd4d9cd9db83bffb71', + toAddress: '0xd7eedf8422ababfbcafc0797e809ceae742fc142', + amount, + timestamp: 1716445488, + }); + assert.isOk(transactionInfo); + assert.equal(transactionInfo.currency, 'ETH'); + assert.equal(transactionInfo.amount, amount); + }); + it('should return transaction detail for OP token transfer on optimistic', async () => { // https://optimistic.etherscan.io/tx/0xf11be189d967831bb8a76656882eeeac944a799bd222acbd556f2156fdc02db4 const amount = 0.453549908802477308; diff --git a/src/utils/networksConfig.ts b/src/utils/networksConfig.ts index e1cf9b752..80d27e3f2 100644 --- a/src/utils/networksConfig.ts +++ b/src/utils/networksConfig.ts @@ -37,6 +37,8 @@ const networksConfig = { }, '42161': { blockExplorer: 'https://arbiscan.io/' }, '421614': { blockExplorer: 'https://sepolia.arbiscan.io/' }, + '8453': { blockExplorer: 'https://basescan.org/' }, + '84532': { blockExplorer: 'https://sepolia.basescan.org/' }, }; export default networksConfig; diff --git a/src/utils/validators/graphqlQueryValidators.ts b/src/utils/validators/graphqlQueryValidators.ts index ea232be2b..d859eb73e 100644 --- a/src/utils/validators/graphqlQueryValidators.ts +++ b/src/utils/validators/graphqlQueryValidators.ts @@ -243,6 +243,8 @@ const managingFundsValidator = Joi.object({ NETWORK_IDS.CELO_ALFAJORES, NETWORK_IDS.ARBITRUM_MAINNET, NETWORK_IDS.ARBITRUM_SEPOLIA, + NETWORK_IDS.BASE_MAINNET, + NETWORK_IDS.BASE_SEPOLIA, NETWORK_IDS.OPTIMISTIC, NETWORK_IDS.OPTIMISM_SEPOLIA, NETWORK_IDS.XDAI, diff --git a/src/utils/validators/projectValidator.ts b/src/utils/validators/projectValidator.ts index e1027a2b4..bb3997f91 100644 --- a/src/utils/validators/projectValidator.ts +++ b/src/utils/validators/projectValidator.ts @@ -141,6 +141,8 @@ export const isWalletAddressSmartContract = async ( NETWORK_IDS.CELO_ALFAJORES, NETWORK_IDS.ARBITRUM_MAINNET, NETWORK_IDS.ARBITRUM_SEPOLIA, + NETWORK_IDS.BASE_MAINNET, + NETWORK_IDS.BASE_SEPOLIA, ]; const _isSmartContracts = await Promise.all( diff --git a/test/pre-test-scripts.ts b/test/pre-test-scripts.ts index b33f1c2f8..a5c9e6dc5 100644 --- a/test/pre-test-scripts.ts +++ b/test/pre-test-scripts.ts @@ -185,6 +185,34 @@ async function seedTokens() { } await Token.create(tokenData as Token).save(); } + for (const token of SEED_DATA.TOKENS.base_mainnet) { + const tokenData = { + ...token, + networkId: NETWORK_IDS.BASE_MAINNET, + isGivbackEligible: true, + }; + if (token.symbol === 'GIV') { + // TODO I'm not sure whether we support GIV or not + (tokenData as any).order = 1; + } else if (token.symbol === 'ETH') { + (tokenData as any).order = 2; + } + await Token.create(tokenData as Token).save(); + } + for (const token of SEED_DATA.TOKENS.base_sepolia) { + const tokenData = { + ...token, + networkId: NETWORK_IDS.BASE_SEPOLIA, + isGivbackEligible: true, + }; + if (token.symbol === 'GIV') { + // TODO I'm not sure whether we support GIV or not + (tokenData as any).order = 1; + } else if (token.symbol === 'ETH') { + (tokenData as any).order = 2; + } + await Token.create(tokenData as Token).save(); + } for (const token of SEED_DATA.TOKENS.optimistic) { const tokenData = { ...token, diff --git a/test/testUtils.ts b/test/testUtils.ts index bff29abb5..8a6e5171d 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1732,6 +1732,24 @@ export const SEED_DATA = { coingeckoId: 'weth', }, ], + base_mainnet: [ + { + name: 'Base ETH', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + coingeckoId: 'ethereum', + }, + ], + base_sepolia: [ + { + name: 'Base Sepolia native token', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + coingeckoId: 'ethereum', + }, + ], }, }; From bfcff6530e081e6b324b1cc59cd21fd5ddb798e8 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Thu, 23 May 2024 11:21:58 +0330 Subject: [PATCH 05/12] Fix migrations --- migration/data/seedTokens.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/migration/data/seedTokens.ts b/migration/data/seedTokens.ts index 4e42698f5..1c5d41894 100644 --- a/migration/data/seedTokens.ts +++ b/migration/data/seedTokens.ts @@ -1598,10 +1598,18 @@ const seedTokens: ITokenData[] = [ { name: 'BASE', symbol: 'ETH', - address: '0x319f865b287fCC10b30d8cE6144e8b6D1b476999', + address: '0x0000000000000000000000000000000000000000', decimals: 18, networkId: NETWORK_IDS.BASE_SEPOLIA, - coingeckoId: 'cartesi', + }, + + // BASE Mainnet + { + name: 'BASE', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, }, ]; From 933111e6f442071cb9dec3004636e8009235738b Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Thu, 23 May 2024 11:30:08 +0330 Subject: [PATCH 06/12] Fix migrations --- migration/1716367359560-add_base_chain_tokens.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migration/1716367359560-add_base_chain_tokens.ts b/migration/1716367359560-add_base_chain_tokens.ts index dbc369134..4e5f0eb00 100644 --- a/migration/1716367359560-add_base_chain_tokens.ts +++ b/migration/1716367359560-add_base_chain_tokens.ts @@ -10,8 +10,8 @@ export class AddBaseChainTokens1716367359560 implements MigrationInterface { const networkId = environment === 'production' - ? NETWORK_IDS.ARBITRUM_MAINNET - : NETWORK_IDS.ARBITRUM_SEPOLIA; + ? NETWORK_IDS.BASE_MAINNET + : NETWORK_IDS.BASE_SEPOLIA; await queryRunner.manager.save( Token, From b48c030db08bdb9f16dee0b886f04be37891eb24 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Fri, 24 May 2024 11:00:40 +0330 Subject: [PATCH 07/12] Add test cases about filteriing base network --- package.json | 4 +- src/resolvers/donationResolver.test.ts | 2 +- .../projectResolver.allProject.test.ts | 108 +++++++++++++++++- 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 4174475a4..76f307419 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "test:reactionResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/reactionResolver.test.ts", "test:donationResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/donationResolver.test.ts", "test:draftDonationResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/draftDonationResolver.test.ts", - "test:projectResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/projectResolver.test.ts", + "test:projectResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/projectResolver.test.ts ./src/resolvers/projectResolver.allProject.test.ts", "test:chainvineResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/chainvineResolver.test.ts", "test:qfRoundResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/qfRoundResolver.test.ts", "test:qfRoundHistoryResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/qfRoundHistoryResolver.test.ts", @@ -192,7 +192,7 @@ "test:instantPowerBoostingService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/instantBoostingServices.test.ts", "test:actualMatchingFundView": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/actualMatchingFundView.test.ts", "test:categoryResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/categoryResolver.test.ts", - "test:givpower": "NODE_ENV=test mocha -b -t 30000 ./test/pre-test-scripts.ts ./src/repositories/powerBoostingRepository.test.ts ./src/repositories/userPowerRepository.test.ts ./src/repositories/powerRoundRepository.test.ts ./src/repositories/userProjectPowerViewRepository.test.ts ./src/repositories/projectPowerViewRepository.test.ts ./src/resolvers/powerBoostingResolver.test.ts ./src/resolvers/userProjectPowerResolver.test.ts ./src/resolvers/projectPowerResolver.test.ts ./src/adapters/givpowerSubgraph/givPowerSubgraphAdapter.test.ts ./src/repositories/projectRepository.test.ts ./src/resolvers/projectResolver.test.ts ./src/repositories/dbCronRepository.test.ts", + "test:givpower": "NODE_ENV=test mocha -b -t 30000 ./test/pre-test-scripts.ts ./src/repositories/powerBoostingRepository.test.ts ./src/repositories/userPowerRepository.test.ts ./src/repositories/powerRoundRepository.test.ts ./src/repositories/userProjectPowerViewRepository.test.ts ./src/repositories/projectPowerViewRepository.test.ts ./src/resolvers/powerBoostingResolver.test.ts ./src/resolvers/userProjectPowerResolver.test.ts ./src/resolvers/projectPowerResolver.test.ts ./src/adapters/givpowerSubgraph/givPowerSubgraphAdapter.test.ts ./src/repositories/projectRepository.test.ts ./src/resolvers/projectResolver.test.ts ./src/resolvers/projectResolver.allProject.test.ts ./src/repositories/dbCronRepository.test.ts", "test:apiGive": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/routers/apiGivRoutes.test.ts", "test:adminJs": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/server/adminJs/**/*.test.ts ", "test:adminJsRolePermissions": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/server/adminJs/adminJsPermissions.test.ts", diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 8f7f184bb..74916e03c 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -2611,7 +2611,7 @@ function createDonationTestCases() { ); assert.equal( saveDonationResponse.data.errors[0].message, - '"transactionNetworkId" must be one of [1, 3, 5, 100, 137, 10, 11155420, 56, 42220, 44787, 61, 63, 42161, 421614, 8453, 84532, 101, 102, 103]', + '"transactionNetworkId" must be one of [1, 3, 5, 100, 137, 10, 11155420, 56, 42220, 44787, 61, 63, 42161, 421614, 8453, 84532, 101, 102, 103]', ); }); it('should not throw exception when currency is not valid when currency is USDC.e', async () => { diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts index 854400e34..a7aa99ba6 100644 --- a/src/resolvers/projectResolver.allProject.test.ts +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -918,7 +918,6 @@ function allProjectsTestCases() { ), ); }); - it('should return projects, filter by accept donation on arbitrum, not return when it doesnt have arbitrum address', async () => { const arbitrumProject = await saveProjectDirectlyToDb({ ...createProjectData(), @@ -964,6 +963,113 @@ function allProjectsTestCases() { ); }); + + it('should return projects, filter by accept donation on base', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.BASE_MAINNET, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnBase'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + (address.networkId === NETWORK_IDS.BASE_MAINNET || + address.networkId === NETWORK_IDS.BASE_SEPOLIA), + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + it('should return projects, filter by accept donation on base', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.BASE_MAINNET, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnBase'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + (address.networkId === NETWORK_IDS.BASE_MAINNET || + address.networkId === NETWORK_IDS.BASE_SEPOLIA) && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + it('should return projects, filter by accept donation on base, not return when it doesnt have base address', async () => { + const baseProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.BASE_MAINNET, + }); + const polygonProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.POLYGON, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnBase'], + sortingBy: SortingField.Newest, + }, + }); + + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + (address.networkId === NETWORK_IDS.BASE_MAINNET || + address.networkId === NETWORK_IDS.BASE_SEPOLIA) && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isNotOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(polygonProject.id), + ), + ); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(baseProject.id), + ), + ); + }); + it('should return projects, filter by accept donation on mainnet', async () => { const savedProject = await saveProjectDirectlyToDb({ ...createProjectData(), From e00842adefc45aefdca00503daeb8efcaa08c7fe Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Fri, 24 May 2024 13:30:32 +0330 Subject: [PATCH 08/12] Add BASE node http urls for rpc --- config/example.env | 6 ++++++ src/provider.ts | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/config/example.env b/config/example.env index 834fb7fb3..18fb2d657 100644 --- a/config/example.env +++ b/config/example.env @@ -312,3 +312,9 @@ BASE_SCAN_API_URL=https://api.basescan.org/api BASE_SCAN_API_KEY=0000000000000000000000000000000000 BASE_SEPOLIA_SCAN_API_URL=https://api-sepolia.basescan.org/api BASE_SEPOLIA_SCAN_API_KEY=0000000000000000000000000000000000 + +# BASE MAINNET +BASE_MAINNET_NODE_HTTP_URL= + +# BASE SEPOLIA +BASE_SEPOLIA_NODE_HTTP_URL= diff --git a/src/provider.ts b/src/provider.ts index d097c7e11..3caf4be82 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -322,6 +322,18 @@ export function getProvider(networkId: number) { `https://arbitrum-sepolia.infura.io/v3/${INFURA_ID}`; break; + case NETWORK_IDS.BASE_MAINNET: + url = + (process.env.BASE_MAINNET_NODE_HTTP_URL as string) || + `https://base-mainnet.infura.io/v3/${INFURA_ID}`; + break; + + case NETWORK_IDS.BASE_SEPOLIA: + url = + (process.env.BASE_SEPOLIA_NODE_HTTP_URL as string) || + `https://base-sepolia.infura.io/v3/${INFURA_ID}`; + break; + default: { // Use infura const connectionInfo = ethers.providers.InfuraProvider.getUrl( From 889552906952c5858080463511adbea10a1485f2 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Fri, 24 May 2024 13:31:43 +0330 Subject: [PATCH 09/12] Add BASE node http urls for rpc --- src/resolvers/projectResolver.allProject.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts index a7aa99ba6..5dfea13bc 100644 --- a/src/resolvers/projectResolver.allProject.test.ts +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -963,7 +963,6 @@ function allProjectsTestCases() { ); }); - it('should return projects, filter by accept donation on base', async () => { const savedProject = await saveProjectDirectlyToDb({ ...createProjectData(), From 28cf3eb19131be38da8a38285eb3d45ee8cd741a Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Fri, 24 May 2024 14:05:09 +0330 Subject: [PATCH 10/12] Add missing things for integrating with base --- src/provider.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/provider.ts b/src/provider.ts index 3caf4be82..26fa243f8 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -1,6 +1,7 @@ import { ethers } from 'ethers'; import config from './config'; import { i18n, translationErrorMessagesKeys } from './utils/errorMessages'; +import { logger } from './utils/logger'; const INFURA_ID = config.get('INFURA_ID'); @@ -157,6 +158,8 @@ const NETWORK_NAMES = { MORDOR_ETC_TESTNET: 'Ethereum Classic Testnet', ARBITRUM_MAINNET: 'Arbitrum Mainnet', ARBITRUM_SEPOLIA: 'Arbitrum Sepolia', + BASE_MAINNET: 'Base Mainnet', + BASE_SEPOLIA: 'Base Sepolia', }; const NETWORK_NATIVE_TOKENS = { @@ -174,6 +177,8 @@ const NETWORK_NATIVE_TOKENS = { MORDOR_ETC_TESTNET: 'mETC', ARBITRUM_MAINNET: 'ETH', ARBITRUM_SEPOLIA: 'ETH', + BASE_MAINNET: 'ETH', + BASE_SEPOLIA: 'ETH', }; const networkNativeTokensList = [ @@ -247,6 +252,16 @@ const networkNativeTokensList = [ networkId: NETWORK_IDS.ARBITRUM_SEPOLIA, nativeToken: NETWORK_NATIVE_TOKENS.ARBITRUM_SEPOLIA, }, + { + networkName: NETWORK_NAMES.BASE_MAINNET, + networkId: NETWORK_IDS.BASE_MAINNET, + nativeToken: NETWORK_NATIVE_TOKENS.BASE_MAINNET, + }, + { + networkName: NETWORK_NAMES.BASE_SEPOLIA, + networkId: NETWORK_IDS.BASE_SEPOLIA, + nativeToken: NETWORK_NATIVE_TOKENS.BASE_SEPOLIA, + }, ]; export function getNetworkNameById(networkId: number): string { @@ -254,6 +269,10 @@ export function getNetworkNameById(networkId: number): string { item => item.networkId === networkId, ); if (!networkInfo) { + logger.error( + 'getNetworkNameById() error networkNativeTokensList doesnt have info for networkId', + networkId, + ); throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_NETWORK_ID)); } return networkInfo.networkName; @@ -264,6 +283,10 @@ export function getNetworkNativeToken(networkId: number): string { return item.networkId === networkId; }); if (!networkInfo) { + logger.error( + 'getNetworkNativeToken() error networkNativeTokensList doesnt have info for networkId', + networkId, + ); throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_NETWORK_ID)); } return networkInfo.nativeToken; @@ -422,6 +445,10 @@ export function getBlockExplorerApiUrl(networkId: number): string { apiKey = config.get('BASE_SEPOLIA_SCAN_API_KEY'); break; default: + logger.error( + 'getBlockExplorerApiUrl() no url found for networkId', + networkId, + ); throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_NETWORK_ID)); } From 38ac33fd15a199611da30d1e271a005aceb5fdf7 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Sun, 26 May 2024 11:06:53 +0330 Subject: [PATCH 11/12] Fix creating new token with coingeckoId related to https://github.com/Giveth/impact-graph/issues/1564 --- src/server/adminJs/tabs/tokenTab.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/adminJs/tabs/tokenTab.ts b/src/server/adminJs/tabs/tokenTab.ts index a486b660c..65360652f 100644 --- a/src/server/adminJs/tabs/tokenTab.ts +++ b/src/server/adminJs/tabs/tokenTab.ts @@ -122,6 +122,7 @@ export const createToken = async ( isGivbackEligible, mainnetAddress, name, + coingeckoId, networkId, symbol, organizations, @@ -133,6 +134,7 @@ export const createToken = async ( address: address?.toLowerCase(), mainnetAddress: mainnetAddress?.toLowerCase(), isGivbackEligible, + coingeckoId, decimals: Number(decimals), networkId: Number(networkId), }); From dc7e7fbb115a9a362ff2e4e715d8c0c8b1c93a10 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Sun, 26 May 2024 16:45:47 +0330 Subject: [PATCH 12/12] Fill base mainnet tokens related to https://github.com/Giveth/impact-graph/issues/1561 --- migration/data/seedTokens.ts | 271 ++++++++++++++++++++++++++++++++++- 1 file changed, 270 insertions(+), 1 deletion(-) diff --git a/migration/data/seedTokens.ts b/migration/data/seedTokens.ts index 1c5d41894..9a0633d87 100644 --- a/migration/data/seedTokens.ts +++ b/migration/data/seedTokens.ts @@ -15,6 +15,7 @@ interface ITokenData { coingeckoId?: string; isStableCoin?: boolean; } + const seedTokens: ITokenData[] = [ // Mainnet tokens { @@ -1603,13 +1604,281 @@ const seedTokens: ITokenData[] = [ networkId: NETWORK_IDS.BASE_SEPOLIA, }, - // BASE Mainnet + // BASE Mainnet - https://basescan.org/token/0x0000000000000000000000000000000000000000 { name: 'BASE', symbol: 'ETH', address: '0x0000000000000000000000000000000000000000', decimals: 18, networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'ethereum', + isGivbackEligible: true, + }, + + // USDC - https://basescan.org/token/0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 + { + name: 'USDC', + symbol: 'USDC', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + decimals: 6, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'usd-coin', + isGivbackEligible: true, + isStableCoin: true, + }, + // Dai Stablecoin - https://basescan.org/token/0x50c5725949a6f0c72e6c4a641f24049a917db0cb + { + name: 'Dai Stablecoin', + symbol: 'DAI', + address: '0x50c5725949a6f0c72e6c4a641f24049a917db0cb', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'dai', + isGivbackEligible: true, + isStableCoin: true, + }, + // Rocket Pool ETH - https://basescan.org/token/0xb6fe221fe9eef5aba221c348ba20a1bf5e73624c + { + name: 'Rocket Pool ETH', + symbol: 'rETH', + address: '0xb6fe221fe9eef5aba221c348ba20a1bf5e73624c', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'rocket-pool-eth', + isGivbackEligible: true, + }, + // Synthetix Network Token - https://basescan.org/token/0x22e6966b799c4d5b13be962e1d117b56327fda66 + { + name: 'Synthetix Network Token', + symbol: 'SNX', + address: '0x22e6966b799c4d5b13be962e1d117b56327fda66', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'havven', + isGivbackEligible: true, + }, + // Coinbase Wrapped Staked ETH - https://basescan.org/token/0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22 + { + name: 'Coinbase Wrapped Staked ETH', + symbol: 'cbETH', + address: '0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'coinbase-wrapped-staked-eth', + isGivbackEligible: true, + }, + // Prime - https://basescan.org/token/0xfA980cEd6895AC314E7dE34Ef1bFAE90a5AdD21b + { + name: 'Prime', + symbol: 'PRIME', + address: '0xfA980cEd6895AC314E7dE34Ef1bFAE90a5AdD21b', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'echelon-prime', + isGivbackEligible: true, + }, + // Aerodrome - https://basescan.org/token/0x940181a94a35a4569e4529a3cdfb74e38fd98631 + { + name: 'Aerodrome', + symbol: 'AERO', + address: '0x940181a94a35a4569e4529a3cdfb74e38fd98631', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'aerodrome-finance', + isGivbackEligible: true, + }, + // Degen - https://basescan.org/token/0x4ed4e862860bed51a9570b96d89af5e1b0efefed + { + name: 'Degen', + symbol: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'degen-base', + isGivbackEligible: true, + }, + // Osaka Protocol - https://basescan.org/token/0xbFd5206962267c7b4b4A8B3D76AC2E1b2A5c4d5e + { + name: 'Osaka Protocol', + symbol: 'OSAK', + address: '0xbFd5206962267c7b4b4A8B3D76AC2E1b2A5c4d5e', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'osaka-protocol', + isGivbackEligible: true, + }, + // BTRST - https://basescan.org/token/0xa7d68d155d17cb30e311367c2ef1e82ab6022b67 + { + name: 'BTRST', + symbol: 'BTRST', + address: '0xa7d68d155d17cb30e311367c2ef1e82ab6022b67', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'braintrust', + isGivbackEligible: true, + }, + // SmarDex Token - https://basescan.org/token/0xfd4330b0312fdeec6d4225075b82e00493ff2e3f + { + name: 'SmarDex Token', + symbol: 'SDEX', + address: '0xfd4330b0312fdeec6d4225075b82e00493ff2e3f', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'smardex', + isGivbackEligible: true, + }, + // Spectral Token - https://basescan.org/token/0x96419929d7949d6a801a6909c145c8eef6a40431 + { + name: 'Spectral Token', + symbol: 'SPEC', + address: '0x96419929d7949d6a801a6909c145c8eef6a40431', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'spectral', + isGivbackEligible: true, + }, + // Heroes of Mavia - https://basescan.org/token/0x24fcFC492C1393274B6bcd568ac9e225BEc93584 + { + name: 'Heroes of Mavia', + symbol: 'MAVIA', + address: '0x24fcFC492C1393274B6bcd568ac9e225BEc93584', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'heroes-of-mavia', + isGivbackEligible: true, + }, + // Maverick Token - https://basescan.org/token/0x64b88c73a5dfa78d1713fe1b4c69a22d7e0faaa7 + { + name: 'Maverick Token', + symbol: 'MAV', + address: '0x64b88c73a5dfa78d1713fe1b4c69a22d7e0faaa7', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'maverick-protocol', + isGivbackEligible: true, + }, + // Dola USD Stablecoin - https://basescan.org/token/0x4621b7a9c75199271f773ebd9a499dbd165c3191 + { + name: 'Dola USD Stablecoin', + symbol: 'DOLA', + address: '0x4621b7a9c75199271f773ebd9a499dbd165c3191', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'dola-usd', + isGivbackEligible: true, + isStableCoin: true, + }, + // USD+ - https://basescan.org/token/0xb79dd08ea68a908a97220c76d19a6aa9cbde4376 + { + name: 'USD+', + symbol: 'USD+', + address: '0xb79dd08ea68a908a97220c76d19a6aa9cbde4376', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'usd', + isGivbackEligible: true, + isStableCoin: true, + }, + // Magic Internet Money - https://basescan.org/token/0x4A3A6Dd60A34bB2Aba60D73B4C88315E9CeB6A3D + { + name: 'Magic Internet Money', + symbol: 'MIM', + address: '0x4A3A6Dd60A34bB2Aba60D73B4C88315E9CeB6A3D', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'magic-internet-money', + isGivbackEligible: true, + }, + // Seamless - https://basescan.org/token/0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85 + { + name: 'Seamless', + symbol: 'SEAM', + address: '0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'seamless-protocol', + isGivbackEligible: true, + }, + // Extra Finance - https://basescan.org/token/0x2dad3a13ef0c6366220f989157009e501e7938f8 + { + name: 'Extra Finance', + symbol: 'EXTRA', + address: '0x2dad3a13ef0c6366220f989157009e501e7938f8', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'extra-finance', + isGivbackEligible: true, + }, + // agEUR - https://basescan.org/token/0xa61beb4a3d02decb01039e378237032b351125b4 + { + name: 'agEUR', + symbol: 'agEUR', + address: '0xa61beb4a3d02decb01039e378237032b351125b4', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'ageur-plenty-bridge', + isGivbackEligible: true, + isStableCoin: true, + }, + // SubQueryToken - https://basescan.org/token/0x858c50C3AF1913b0E849aFDB74617388a1a5340d + { + name: 'SubQueryToken', + symbol: 'SQT', + address: '0x858c50C3AF1913b0E849aFDB74617388a1a5340d', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'subquery-network', + isGivbackEligible: true, + }, + // Rai.Finance - https://basescan.org/token/0x703d57164ca270b0b330a87fd159cfef1490c0a5 + { + name: 'Rai.Finance', + symbol: 'SOFI', + address: '0x703d57164ca270b0b330a87fd159cfef1490c0a5', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'rai-finance', + isGivbackEligible: true, + }, + // UNJD (MBS) - https://basescan.org/token/0x8fbd0648971d56f1f2c35fa075ff5bc75fb0e39d + { + name: 'UNJD', + symbol: 'MBS', + address: '0x8fbd0648971d56f1f2c35fa075ff5bc75fb0e39d', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'monkeyball', + isGivbackEligible: true, + }, + // Rigo Token - https://basescan.org/token/0x09188484e1ab980daef53a9755241d759c5b7d60 + { + name: 'Rigo Token', + symbol: 'GRG', + address: '0x09188484e1ab980daef53a9755241d759c5b7d60', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'rigoblock', + isGivbackEligible: true, + }, + // DEUS - https://basescan.org/token/0xde5ed76e7c05ec5e4572cfc88d1acea165109e44 + { + name: 'DEUS', + symbol: 'DEUS', + address: '0xde5ed76e7c05ec5e4572cfc88d1acea165109e44', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'deus-finance-2', + isGivbackEligible: true, + }, + // Alongside Crypto Market Index - https://basescan.org/token/0x13f4196cc779275888440b3000ae533bbbbc3166 + { + name: 'Alongside Crypto Market Index', + symbol: 'AMKT', + address: '0x13f4196cc779275888440b3000ae533bbbbc3166', + decimals: 18, + networkId: NETWORK_IDS.BASE_MAINNET, + coingeckoId: 'alongside-crypto-market-index', + isGivbackEligible: true, }, ];