diff --git a/.DS_Store b/.DS_Store index cb3f0e0ba..64174a1f7 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..7e6661d18 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,42 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["import", "unused-imports"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "no-console": "error", + "unused-imports/no-unused-imports": "error", + "@typescript-eslint/no-unused-vars": [ + "error", { "argsIgnorePattern": "^_" }], + "prettier/prettier": [ + "error", + { + "arrowParens": "avoid", + "singleQuote": true, + "semi": true, + "tabWidth": 2, + "useTabs": false, + "trailingComma": "all", + "jsdoc-format": false, + "endOfLine": "auto" + } + ], + "import/order": [ + "error", + { + "groups": [ + "builtin", + "external", + ["internal", "parent", "sibling"], + "index", + "object", + "type" + ] + } + ] + } +} diff --git a/.github/workflows/develop-pipeline.yml b/.github/workflows/develop-pipeline.yml index 417acd9aa..bf6209ae8 100644 --- a/.github/workflows/develop-pipeline.yml +++ b/.github/workflows/develop-pipeline.yml @@ -46,8 +46,8 @@ jobs: node-version: 20.11.0 - name: Install dependencies run: npm ci - - name: Run tslint - run: npm run tslint + - name: Run eslint + run: npm run eslint - name: Run build run: npm run build - name: Run migrations diff --git a/.github/workflows/master-pipeline.yml b/.github/workflows/master-pipeline.yml index 08f6fda9a..6bcebcb83 100644 --- a/.github/workflows/master-pipeline.yml +++ b/.github/workflows/master-pipeline.yml @@ -80,8 +80,8 @@ jobs: - name: Install dependencies run: npm ci - - name: Run tslint - run: npm run tslint + - name: Run eslint + run: npm run eslint - name: Run build run: npm run build diff --git a/.github/workflows/run-tests-on-pr.yml.bck b/.github/workflows/run-tests-on-pr.yml.bck index d3072bd8e..c5fdd8d5a 100644 --- a/.github/workflows/run-tests-on-pr.yml.bck +++ b/.github/workflows/run-tests-on-pr.yml.bck @@ -49,8 +49,8 @@ jobs: node-version: 16.14.2 - name: Install dependencies run: npm ci - - name: Run tslint - run: npm run tslint + - name: Run eslint + run: npm run eslint - name: Run build run: npm run build - name: Run migrations diff --git a/.github/workflows/staging-pipeline.yml b/.github/workflows/staging-pipeline.yml index 08ec827e4..388f4660c 100644 --- a/.github/workflows/staging-pipeline.yml +++ b/.github/workflows/staging-pipeline.yml @@ -80,8 +80,8 @@ jobs: - name: Install dependencies run: npm ci - - name: Run tslint - run: npm run tslint + - name: Run eslint + run: npm run eslint - name: Run build run: npm run build diff --git a/.gitignore b/.gitignore index 3cedcd092..6799a37fa 100644 --- a/.gitignore +++ b/.gitignore @@ -26,10 +26,11 @@ src/scripts/*.json *.log *.log.* -*.DS_Store .adminbro .adminjs ./src/adminjs ./src/server/adminjs ./src/server/adminJs/adminjs + +.DS_Store diff --git a/.prettierrc.json b/.prettierrc.json index 5fd6619c6..a3e5f049a 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -4,5 +4,6 @@ "semi": true, "tabWidth": 2, "useTabs": false, - "trailingComma": "all" + "trailingComma": "all", + "endOfLine": "auto" } diff --git a/config/example.env b/config/example.env index bc5c24bcd..05d30d5df 100644 --- a/config/example.env +++ b/config/example.env @@ -9,7 +9,6 @@ TYPEORM_DATABASE_PORT= TYPEORM_LOGGING= DROP_DATABASE= APOLLO_KEY= -REGISTER_USERNAME_PASSWORD= STRIPE_KEY= STRIPE_SECRET= STRIPE_WEBHOOK_SECRET= @@ -188,7 +187,7 @@ DONATION_VERIFICAITON_EXPIRATION_HOURS=24 # Default: unnamed SERVICE_NAME=example OPTIMISM_NODE_HTTP_URL=https://optimism-mainnet.public.blastapi.io/ -OPTIMISM_GOERLI_NODE_HTTP_URL= +OPTIMISM_SEPOLIA_NODE_HTTP_URL= ####################################### INSTANT BOOSTING ################################# # OPTIONAL - default: false @@ -278,6 +277,9 @@ NUMBER_OF_UPDATE_RECURRING_DONATION_CONCURRENT_JOB=1 # Default value is 0 0 * * * that means one day at 00:00 UPDATE_RECURRING_DONATIONS_STREAM=0 0 * * * +# Default value is 0.4 +PROJECT_SEARCH_SIMILARITY_THRESHOLD=0.4 + MPETH_GRAPHQL_PRICES_URL= # Draft donation match expiration hours, they will be deleted after to lessen dabase size @@ -294,4 +296,13 @@ INSERT_USER_PASSPORT_SCORE_FOR_QF_ROUND_CRONJOB_TIME=0 0 * * * QfRound_PASSPORT_SCORE_CHECK_START_TIMESTAMP_IN_SECONDS= ORTTO_API_KEY=FAKE_API_KEY -ORTTO_PERSON_API=https://api.ap3api.com/v1/person/merge \ No newline at end of file +ORTTO_PERSON_API=https://api.ap3api.com/v1/person/merge + + +RECURRING_DONATION_VERIFICATION_EXPIRATION_HOURS=24 +VERIFY_RECURRING_DONATION_CRONJOB_EXPRESSION=0 * * * * * +NUMBER_OF_VERIFY_RECURRING_DONATION_CONCURRENT_JOB=1 +ENABLE_DRAFT_RECURRING_DONATION=true +DRAFT_RECURRING_DONATION_MATCH_EXPIRATION_HOURS=24 + +OPTIMISTIC_SEPOLIA_SCAN_API_KEY= diff --git a/config/test.env b/config/test.env index 19eb4a611..ef55172b7 100644 --- a/config/test.env +++ b/config/test.env @@ -12,7 +12,6 @@ TYPEORM_DATABASE_PORT=5443 TYPEORM_LOGGING=all DROP_DATABASE=true APOLLO_KEY=service:0000000000000000000000000000000000 -REGISTER_USERNAME_PASSWORD=false STRIPE_KEY=pk_test_000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 STRIPE_SECRET=sk_test_000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 STRIPE_WEBHOOK_SECRET=whsec_00000000000000000000000000000000 @@ -41,6 +40,7 @@ ETHERSCAN_ROPSTEN_API_URL=https://api-ropsten.etherscan.io/api ETHERSCAN_GOERLI_API_URL=https://api-goerli.etherscan.io/api POLYGON_SCAN_API_URL=https://api.polygonscan.com/api OPTIMISTIC_SCAN_API_URL=https://api-optimistic.etherscan.io/api +OPTIMISTIC_SEPOLIA_SCAN_API_URL=https://api-sepolia-optimistic.etherscan.io/api CELO_SCAN_API_URL=https://api.celoscan.io/api CELO_ALFAJORES_SCAN_API_URL=https://api-alfajores.celoscan.io/api ARBITRUM_SCAN_API_URL=https://api.arbiscan.io/api @@ -139,11 +139,11 @@ NOTIFICATION_CENTER_USERNAME= NOTIFICATION_CENTER_PASSWORD= PROJECT_UPDATES_VERIFIED_REMINDER_DAYS=30 -PROJECT_UPDATES_VERIFIED_WARNING_DAYS=60 +PROJECT_UPDATES_VERIFIED_WARNING_DAYS=45 PROJECT_UPDATES_VERIFIED_LAST_WARNING_DAYS=90 PROJECT_UPDATES_VERIFIED_REVOKED_DAYS=104 PROJECT_UPDATES_FIRST_REVOKE_BATCH_DATE=2021-01-22 -PROJECT_REVOKE_SERVICE_ACTIVE=false +PROJECT_REVOKE_SERVICE_ACTIVE=true UPDATE_POWER_ROUND_CRONJOB_EXPRESSION=0 0 * * * UPDATE_POWER_SNAPSHOT_SERVICE_ACTIVE=false @@ -167,6 +167,7 @@ CHAINVINE_ADAPTER=mock CHAINVINE_API_ENABLE_TEST_MODE=true # We should not try to verify donaitons after some hours, because checking old donations would make lots of requests to web3 providers DONATION_VERIFICAITON_EXPIRATION_HOURS=24 +RECURRING_DONATION_VERIFICAITON_EXPIRATION_HOURS=24 # We need it for monoswap POLYGON_MAINNET_NODE_HTTP_URL=https://polygon-rpc.com @@ -227,3 +228,11 @@ ENABLE_INSERT_USER_PASSPORT_SCORES=true INSERT_USER_PASSPORT_SCORE_FOR_QF_ROUND_CRONJOB_TIME=0 0 * * * # Optional QfRound_PASSPORT_SCORE_CHECK_START_TIMESTAMP_IN_SECONDS= + +ENABLE_DRAFT_RECURRING_DONATION=true +DRAFT_RECURRING_DONATION_MATCH_EXPIRATION_HOURS=24 + + +OPTIMISTIC_SEPOLIA_SCAN_API_KEY= + +SUPER_FLUID_ADAPTER=superfluid diff --git a/docs/adminPermissions.md b/docs/adminPermissions.md new file mode 100644 index 000000000..9f605d262 --- /dev/null +++ b/docs/adminPermissions.md @@ -0,0 +1,34 @@ +## Different roles +* admin +* campaignManager +* reviewer +* operator +* qfManager + +## Resources and permissions +Below table has been generated by https://www.tablesgenerator.com/markdown_tables + + +| Page | admin | campaignManager | reviewer | operator | qfManager | +|:--------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:---------------------:|:-------------------------------------------------------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------:| +| Users | list, new, show, edit | list, show | list, show | list, show | - | +| Organization | list, show | list, show | list, show | list, show | - | +| Project status history | list, show | list, show | list, show | list, show | - | +| Campaign | list, new, show, edit, delete | list, new, show, edit | list, show | list, show | - | +| QF Round | list, new, show, edit, returnAllDonationData | list, show | list, show | list, show | list, new, show, edit, returnAllDonationData (export to spreadsheet) | +| QF Round History | list, show, edit, delete, bulkDelete, updateQfRoundHistories, relateDonationsWithDistributedFunds | list, show | list, show | list, show | list, show, updateQfRoundHistories | +| Project Status Reason | list, new, show, edit | list, show | list, show | list, show | - | +| Project Address | list, new, show, edit, delete, bulkDelete | list, show | list, show | list, show | - | +| Project Status | list, show, edit | list, show | list, show | list, show | - | +| Project | list, show, edit, exportFilterToCsv, listProject, unlistProject, verifyProject, rejectProject, revokeBadge, activateProject, deactivateProject, cancelProject, addToQfRound, removeFromQfRound | list, show | list, show, exportFilterToCsv, listProject, unlistProject, verifyProject, rejectProject, revokeBadge, activateProject, deactivateProject, cancelProject | list, show, exportFilterToCsv, listProject, unlistProject, verifyProject, rejectProject, revokeBadge, activateProject, deactivateProject, cancelProject | list, show, listProject, addToQfRound, removeFromQfRound | +| Third Party Project Import | list, new, show, edit, delete, bulkDelete | list, show | list, show | list, show | - | +| Featured Update | list, new, show, edit, delete, bulkDelete | list, show | list, show | list, show | - | +| Donation | list, new, show, edit, delete, exportFilterToCsv | list, show | list, show, exportFilterToCsv | list, show, exportFilterToCsv | - | +| Project Verification Form | list, show, edit, delete, verifyProject, makeEditableByUser, rejectProject, verifyProjects, rejectProjects | list, show | list, show, edit, delete, verifyProject, makeEditableByUser, rejectProject, verifyProjects, rejectProjects | list, show | - | +| Main Category | list, new, show, edit | list, show | list, show | list, show | - | +| Category | list, new, show, edit | list, show | list, show | list, show | - | +| Broadcast Notification | list, new, show | list, show | list, show | list, show | - | +| Project Update | list, show, addFeaturedProjectUpdate | list, show | list, show | list, show, addFeaturedProjectUpdate | - | +| Sybil | list, show, new, edit, delete, bulkDelete | list, show | list, show | list, show | list, show, new, edit, delete, bulkDelete | +| Project Fraud | list, show, new, edit, delete, bulkDelete | list, show | list, show | list, show | list, show, new, edit, delete, bulkDelete | +| Recurring donation | list, show, new, edit, delete, bulkDelete | list, show | list, show | list, show | - | | \ No newline at end of file diff --git a/docs/img/admin-panel-project-fraud-page.png b/docs/img/admin-panel-project-fraud-page.png new file mode 100644 index 000000000..38ea6fa71 Binary files /dev/null and b/docs/img/admin-panel-project-fraud-page.png differ diff --git a/docs/img/admin-panel-sybil-page.png b/docs/img/admin-panel-sybil-page.png new file mode 100644 index 000000000..06740cf69 Binary files /dev/null and b/docs/img/admin-panel-sybil-page.png differ diff --git a/docs/qfRoundInstruction.md b/docs/qfRoundInstruction.md index 15c35b2c3..73998af94 100644 --- a/docs/qfRoundInstruction.md +++ b/docs/qfRoundInstruction.md @@ -36,4 +36,46 @@ You can add/remove a project to existing active QF Round with these two buttons And also you can see related qfRound of a project in this page +## Mark users as sybil for qfRound + +### Add single item +If you want to just add a single item , you just choose a user from drop down menu and use a qfRound as well, leave the +**Csv Data** blank then click on Save button, this user will be marked as sybil for that round + +### Add bulk +If you want to add multiple item at once, leave the **User Id** and **Qf Round Id** blank and just put a +csv content in the **Csv Data** text box, the csv data format should be like this + +``` +qfRoundId, walletAddress +1, 0x... +2, 0... +``` +![Screen shot](./img/admin-panel-sybil-page.png) + +## Mark projects as fraud for qfRound + +### Add single item +If you want to just add a single item , you just choose a project from drop down menu and use a qfRound as well, leave the +**Csv Data** blank then click on Save button, this project will be marked as fraud for that round + +### Add bulk +If you want to add multiple item at once, leave the **Project Id** and **Qf Round Id** blank and just put a +csv content in the **Csv Data** text box, the csv data format should be like this + +![Screen shot](./img/admin-panel-project-fraud-page.png) + +``` +qfRoundId, slug +1, test +1, giveth +2, common-stack +``` + + + + + + + diff --git a/docs/qfRoundMatchingFundCalculation.md b/docs/qfRoundMatchingFundCalculation.md new file mode 100644 index 000000000..376b6793b --- /dev/null +++ b/docs/qfRoundMatchingFundCalculation.md @@ -0,0 +1,40 @@ +# how our Actual matching fund is calculated + +## Formula +We use Gitcoin formula you can see the detail https://qf.gitcoin.co/?grant=1,2,3&grant=4,5,6&grant=12,1&grant=8&match=1000 +and play it with different amounts to see what happens + +## Edge Cases +If you want to see edge cases and how it works, you can see the test cases in +https://github.com/Giveth/impact-graph/blob/staging/src/services/actualMatchingFundView.test.ts + +But I will list it down here (although the test cases are more detailed and updated) + +* Ensures the view is not null for projects with no donations +In the sheet that we export, we will include all projects of the qf round +whether they have donations or not. If they have no donations, the amount will be null + +* Confirms donations from recipients of verified projects are **excluded** +* Confirms donations from recipients of unverified projects are **included** +* Confirms donations from donors of verified projects are **included** +* Confirms donations from donors of unverified projects are **included** +* Validates correct aggregation of multiple donations to a project +* Ensures accurate calculation when a single user makes multiple donations to a project +* Confirms donations under `qfRound.minimumValidUsdValue` are correctly **ignored** in calculations +* Verifies aggregated donations from a user exceeding `qfRound.minimumValidUsdValue` are **included** in calculations +* Asserts that donations to non-verified projects are properly **included** +* Asserts that donations to unlisted projects are properly **included** +* Ensures donations from identified Sybil users are **excluded** +* Validates that donations from users with passport scores lower than `qfRound.minimumPassportScore` are **excluded** +* Validates that donations in non-eligible networks are **excluded** from both pre-analysis and post-analysis +* Confirms that donations to flagged fraud projects are **ignored** in calculations +* Ensures pending and failed donation statuses are **excluded** from both pre-analysis and post-analysis totals + +## Estimated Matching +Estimated Matching is like actual matching except we **dont ignore** donations in these cases : +* donations from recipients of verified projects +* donations from users with low passport scores +* donations from sybil users +* donations lower than `qfRound.minimumValidUsdValue` +* donations to flagged fraud projects + diff --git a/migration/1614079067364-AddProjectStatus.ts b/migration/1614079067364-AddProjectStatus.ts index db15f4d23..8c4d02c38 100644 --- a/migration/1614079067364-AddProjectStatus.ts +++ b/migration/1614079067364-AddProjectStatus.ts @@ -1,4 +1,3 @@ -/* tslint:disable:no-console */ import { MigrationInterface, QueryRunner } from 'typeorm'; export class AddProjectStatus1614079067364 implements MigrationInterface { @@ -14,6 +13,7 @@ export class AddProjectStatus1614079067364 implements MigrationInterface { try { await queryRunner.query(`DROP TABLE "project_status"`); } catch (e) { + // eslint-disable-next-line no-console console.log('AddProjectStatus1614079067364 error', e); } } diff --git a/migration/1614082100757-SeedProjectStatus.ts b/migration/1614082100757-SeedProjectStatus.ts index b3f168dfb..7609465bb 100644 --- a/migration/1614082100757-SeedProjectStatus.ts +++ b/migration/1614082100757-SeedProjectStatus.ts @@ -1,5 +1,4 @@ -import { MigrationInterface, QueryRunner, getRepository } from 'typeorm'; -import { ProjectStatus } from '../src/entities/projectStatus'; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class SeedProjectStatus1614082100757 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { diff --git a/migration/1640767594947-addCategoryTable.ts b/migration/1640767594947-addCategoryTable.ts index 80e382293..cc8528d37 100644 --- a/migration/1640767594947-addCategoryTable.ts +++ b/migration/1640767594947-addCategoryTable.ts @@ -1,7 +1,5 @@ -/* tslint:disable:no-console */ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class addCategoryTable1640767594947 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { await queryRunner.query( @@ -13,6 +11,7 @@ export class addCategoryTable1640767594947 implements MigrationInterface { try { await queryRunner.query(`DROP TABLE "category"`); } catch (e) { + // eslint-disable-next-line no-console console.log('addCategoryTable1640767594947 error', e); } } diff --git a/migration/1640767827635-seedCategories.ts b/migration/1640767827635-seedCategories.ts index 52fb0845d..0e63becb8 100644 --- a/migration/1640767827635-seedCategories.ts +++ b/migration/1640767827635-seedCategories.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class seedCategories1640767827635 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const categories = await queryRunner.query(`SELECT * FROM category`); diff --git a/migration/1643716017830-ProjectTotalReactions.ts b/migration/1643716017830-ProjectTotalReactions.ts index cb0217cae..7bd23a5be 100644 --- a/migration/1643716017830-ProjectTotalReactions.ts +++ b/migration/1643716017830-ProjectTotalReactions.ts @@ -28,6 +28,5 @@ export class ProjectTotalReactions1643716017830 implements MigrationInterface { } } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1643891610952-setSegmentNotifiedForOlderDonations.ts b/migration/1643891610952-setSegmentNotifiedForOlderDonations.ts index af72c9f34..091c70270 100644 --- a/migration/1643891610952-setSegmentNotifiedForOlderDonations.ts +++ b/migration/1643891610952-setSegmentNotifiedForOlderDonations.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class setSegmentNotifiedForOlderDonations1643891610952 implements MigrationInterface { diff --git a/migration/1643957749780-addProjectStatusReasonTable.ts b/migration/1643957749780-addProjectStatusReasonTable.ts index f0c6b726b..66287de1b 100644 --- a/migration/1643957749780-addProjectStatusReasonTable.ts +++ b/migration/1643957749780-addProjectStatusReasonTable.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class addProjectStatusReasonTable1643957749780 implements MigrationInterface { @@ -19,6 +18,5 @@ export class addProjectStatusReasonTable1643957749780 ); } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1643962364050-seedProjectStatusreasons.ts b/migration/1643962364050-seedProjectStatusreasons.ts index 8355cc1cc..2241dc1e6 100644 --- a/migration/1643962364050-seedProjectStatusreasons.ts +++ b/migration/1643962364050-seedProjectStatusreasons.ts @@ -1,7 +1,6 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; import { ProjStatus } from '../src/entities/project'; -// tslint:disable-next-line:class-name export class seedProjectStatusreasons1643962364050 implements MigrationInterface { @@ -16,6 +15,5 @@ export class seedProjectStatusreasons1643962364050 ;`); } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1644326560894-changeStatusesInfos.ts b/migration/1644326560894-changeStatusesInfos.ts index dcb4bfc15..e438e9682 100644 --- a/migration/1644326560894-changeStatusesInfos.ts +++ b/migration/1644326560894-changeStatusesInfos.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class changeStatusesInfos1644326560894 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { await queryRunner.query( @@ -26,6 +25,5 @@ export class changeStatusesInfos1644326560894 implements MigrationInterface { ); } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1644467038020-setUserTotalDonated.ts b/migration/1644467038020-setUserTotalDonated.ts index 7f5501759..76bb09acd 100644 --- a/migration/1644467038020-setUserTotalDonated.ts +++ b/migration/1644467038020-setUserTotalDonated.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class setUserTotalDonated1644467038020 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { // To run the update query SUM I should check table existance @@ -25,7 +24,7 @@ export class setUserTotalDonated1644467038020 implements MigrationInterface { } } - async down(queryRunner: QueryRunner): Promise { + async down(_queryRunner: QueryRunner): Promise { // deleting the column from project entity removes it completely. } } diff --git a/migration/1644467298829-setUserTotalReceived.ts b/migration/1644467298829-setUserTotalReceived.ts index 56534a823..4bc4f4511 100644 --- a/migration/1644467298829-setUserTotalReceived.ts +++ b/migration/1644467298829-setUserTotalReceived.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class setUserTotalReceived1644467298829 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { // To run the update query SUM I should check table existance @@ -26,7 +25,7 @@ export class setUserTotalReceived1644467298829 implements MigrationInterface { } } - async down(queryRunner: QueryRunner): Promise { + async down(_queryRunner: QueryRunner): Promise { // deleting the column from project entity removes it completely. } } diff --git a/migration/1645240822841-AddDraftStatusDbValue.ts b/migration/1645240822841-AddDraftStatusDbValue.ts index 0c2ad32f9..9b99df9aa 100644 --- a/migration/1645240822841-AddDraftStatusDbValue.ts +++ b/migration/1645240822841-AddDraftStatusDbValue.ts @@ -15,6 +15,5 @@ export class AddDraftStatusDbValue1645240822841 implements MigrationInterface { `); } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1646131845613-removeOlderProjectStatusHistories.ts b/migration/1646131845613-removeOlderProjectStatusHistories.ts index 156ad6077..6874f3545 100644 --- a/migration/1646131845613-removeOlderProjectStatusHistories.ts +++ b/migration/1646131845613-removeOlderProjectStatusHistories.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class removeOlderProjectStatusHistories1646131845613 implements MigrationInterface { @@ -10,6 +9,5 @@ export class removeOlderProjectStatusHistories1646131845613 await queryRunner.query(`DROP TABLE IF EXISTS project_status_history`); } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1646295724658-createTokensTable.ts b/migration/1646295724658-createTokensTable.ts index ce394110b..e107727f4 100644 --- a/migration/1646295724658-createTokensTable.ts +++ b/migration/1646295724658-createTokensTable.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class createTokensTable1646295724658 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { await queryRunner.query( diff --git a/migration/1646301273835-createOrganisationTable.ts b/migration/1646301273835-createOrganisationTable.ts index d27ba3529..18b475d2a 100644 --- a/migration/1646301273835-createOrganisationTable.ts +++ b/migration/1646301273835-createOrganisationTable.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class createOrganisationTable1646301273835 implements MigrationInterface { diff --git a/migration/1646302349926-createOrganisatioTokenTable.ts b/migration/1646302349926-createOrganisatioTokenTable.ts index e54e900ff..54edcd511 100644 --- a/migration/1646302349926-createOrganisatioTokenTable.ts +++ b/migration/1646302349926-createOrganisatioTokenTable.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class createOrganisatioTokenTable1646302349926 implements MigrationInterface { diff --git a/migration/1646303882607-seedTokes.ts b/migration/1646303882607-seedTokes.ts index 6598ddfae..96dba53d5 100644 --- a/migration/1646303882607-seedTokes.ts +++ b/migration/1646303882607-seedTokes.ts @@ -3,7 +3,6 @@ import { Token } from '../src/entities/token'; import seedTokens from './data/seedTokens'; import { ChainType } from '../src/types/network'; -// tslint:disable-next-line:class-name export class seedTokes1646303882607 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { await queryRunner.query( diff --git a/migration/1646305490859-seedOrganizations.ts b/migration/1646305490859-seedOrganizations.ts index 626d6597f..98c76e0bc 100644 --- a/migration/1646305490859-seedOrganizations.ts +++ b/migration/1646305490859-seedOrganizations.ts @@ -3,7 +3,6 @@ import { ORGANIZATION_LABELS } from '../src/entities/organization'; const { GIVETH, GIVING_BLOCK, TRACE, CHANGE } = ORGANIZATION_LABELS; -// tslint:disable-next-line:class-name export class seedOrganizations1646305490859 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`INSERT INTO organization (label,name,website) VALUES diff --git a/migration/1646306281286-relateExistingProjectsToOrganizations.ts b/migration/1646306281286-relateExistingProjectsToOrganizations.ts index c34238034..fdd782678 100644 --- a/migration/1646306281286-relateExistingProjectsToOrganizations.ts +++ b/migration/1646306281286-relateExistingProjectsToOrganizations.ts @@ -1,17 +1,12 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -import { Organization } from '../src/entities/organization'; -import { Project } from '../src/entities/project'; -import { Donation } from '../src/entities/donation'; -import createSchema from '../src/server/createSchema'; -// tslint:disable-next-line:class-name export class relateExistingProjectsToOrganizations1646306281286 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const projectTableExists = await queryRunner.hasTable('project'); if (!projectTableExists) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log( 'The project table doesnt exist, so there is no need to relate it to organizations', ); diff --git a/migration/1646307744677-relateExistingTokensToOrganizations.ts b/migration/1646307744677-relateExistingTokensToOrganizations.ts index eea9f1856..f24a7434a 100644 --- a/migration/1646307744677-relateExistingTokensToOrganizations.ts +++ b/migration/1646307744677-relateExistingTokensToOrganizations.ts @@ -1,8 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -import { Token } from '../src/entities/token'; -import { Organization } from '../src/entities/organization'; -// tslint:disable-next-line:class-name export class relateExistinTokensToOrganizations1646307744677 implements MigrationInterface { diff --git a/migration/1646573865245-changePinataGatewayOfImages.ts b/migration/1646573865245-changePinataGatewayOfImages.ts index 12af9ba95..08e744887 100644 --- a/migration/1646573865245-changePinataGatewayOfImages.ts +++ b/migration/1646573865245-changePinataGatewayOfImages.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class changePinataGatewayOfImages1646573865245 implements MigrationInterface { diff --git a/migration/1646744494458-changeImagePathProjectDescriptions.ts b/migration/1646744494458-changeImagePathProjectDescriptions.ts index ce8e0779a..a74e757de 100644 --- a/migration/1646744494458-changeImagePathProjectDescriptions.ts +++ b/migration/1646744494458-changeImagePathProjectDescriptions.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class changeImagePathProjectDescriptions1646744494458 implements MigrationInterface { diff --git a/migration/1647270507396-filIIsProjectVerifiedForProjects.ts b/migration/1647270507396-filIIsProjectVerifiedForProjects.ts index fa8acb4bb..810d1e323 100644 --- a/migration/1647270507396-filIIsProjectVerifiedForProjects.ts +++ b/migration/1647270507396-filIIsProjectVerifiedForProjects.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class fillIsProjectVerifiedForProjects1647270507396 implements MigrationInterface { diff --git a/migration/1647455975316-modifyUserTotalDonated.ts b/migration/1647455975316-modifyUserTotalDonated.ts index a5052402e..253bd623c 100644 --- a/migration/1647455975316-modifyUserTotalDonated.ts +++ b/migration/1647455975316-modifyUserTotalDonated.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class modifyUserTotalDonated1647455975316 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { // To run the update query SUM I should check table existance @@ -25,6 +24,5 @@ export class modifyUserTotalDonated1647455975316 implements MigrationInterface { } } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1647455993813-modifyUserTotalReceived.ts b/migration/1647455993813-modifyUserTotalReceived.ts index bfcbcb245..a7b7b09f0 100644 --- a/migration/1647455993813-modifyUserTotalReceived.ts +++ b/migration/1647455993813-modifyUserTotalReceived.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class modifyUserTotalReceived1647455993813 implements MigrationInterface { @@ -27,6 +26,5 @@ export class modifyUserTotalReceived1647455993813 } } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1647514950889-fixUpdatetAtOfProjects.ts b/migration/1647514950889-fixUpdatetAtOfProjects.ts index b29a1473e..2cbc3a482 100644 --- a/migration/1647514950889-fixUpdatetAtOfProjects.ts +++ b/migration/1647514950889-fixUpdatetAtOfProjects.ts @@ -1,9 +1,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -import { Project, ProjectUpdate } from '../src/entities/project'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const moment = require('moment'); -// tslint:disable-next-line:class-name export class fixUpdatetAtOfProjects1647514950889 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const projectTableExists = await queryRunner.hasTable('project'); @@ -34,5 +32,5 @@ export class fixUpdatetAtOfProjects1647514950889 implements MigrationInterface { } } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1647913785673-addIsImportedToProjects.ts b/migration/1647913785673-addIsImportedToProjects.ts index 89d8118d4..987736f49 100644 --- a/migration/1647913785673-addIsImportedToProjects.ts +++ b/migration/1647913785673-addIsImportedToProjects.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class addIsImportedToProjects1647913785673 implements MigrationInterface { @@ -22,5 +21,5 @@ export class addIsImportedToProjects1647913785673 } } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1647927483869-fillMainnetAddressForSomeTokens.ts b/migration/1647927483869-fillMainnetAddressForSomeTokens.ts index a1e4d27e8..5fb48873c 100644 --- a/migration/1647927483869-fillMainnetAddressForSomeTokens.ts +++ b/migration/1647927483869-fillMainnetAddressForSomeTokens.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class fillMainnetAddressForSomeTokens1647927483869 implements MigrationInterface { diff --git a/migration/1648066794387-addChangeAcceptedtokens.ts b/migration/1648066794387-addChangeAcceptedtokens.ts index a39fd3245..0b2319d3e 100644 --- a/migration/1648066794387-addChangeAcceptedtokens.ts +++ b/migration/1648066794387-addChangeAcceptedtokens.ts @@ -1,7 +1,6 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; import config from '../src/config'; -// tslint:disable-next-line:class-name export class addChangeAcceptedtokens1648066794387 implements MigrationInterface { @@ -43,5 +42,5 @@ export class addChangeAcceptedtokens1648066794387 } } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1648085509369-addGivingBlockOrganizationToGivingBlockProjects.ts b/migration/1648085509369-addGivingBlockOrganizationToGivingBlockProjects.ts index bd5858e38..43b04944c 100644 --- a/migration/1648085509369-addGivingBlockOrganizationToGivingBlockProjects.ts +++ b/migration/1648085509369-addGivingBlockOrganizationToGivingBlockProjects.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class addGivingBlockOrganizationToGivingBlockProjects1648085509369 implements MigrationInterface { @@ -23,5 +22,5 @@ export class addGivingBlockOrganizationToGivingBlockProjects1648085509369 `); } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1648103938557-addSupportCustomTokensToOrganizations.ts b/migration/1648103938557-addSupportCustomTokensToOrganizations.ts index 2d17ff692..f4bb7528c 100644 --- a/migration/1648103938557-addSupportCustomTokensToOrganizations.ts +++ b/migration/1648103938557-addSupportCustomTokensToOrganizations.ts @@ -1,7 +1,6 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; import { ORGANIZATION_LABELS } from '../src/entities/organization'; -// tslint:disable-next-line:class-name export class addSupportCustomTokensToOrganizations1648103938557 implements MigrationInterface { diff --git a/migration/1649133177576-setCurrentTokensAsGivBackEligible.ts b/migration/1649133177576-setCurrentTokensAsGivBackEligible.ts index ecda9035c..4d611f95c 100644 --- a/migration/1649133177576-setCurrentTokensAsGivBackEligible.ts +++ b/migration/1649133177576-setCurrentTokensAsGivBackEligible.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class setCurrentTokensAsGivBackEligible1649133177576 implements MigrationInterface { diff --git a/migration/1649133587608-relateGivingBlocksTokensToOrganization.ts b/migration/1649133587608-relateGivingBlocksTokensToOrganization.ts index c859d0d59..d99f50e43 100644 --- a/migration/1649133587608-relateGivingBlocksTokensToOrganization.ts +++ b/migration/1649133587608-relateGivingBlocksTokensToOrganization.ts @@ -63,7 +63,6 @@ const givingBlockTokenNames = [ 'Fetch', ]; -// tslint:disable-next-line:class-name export class relateGivingBlocksTokensToOrganization1649133587608 implements MigrationInterface { @@ -104,5 +103,5 @@ export class relateGivingBlocksTokensToOrganization1649133587608 } } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1650907676905-RemoveNegativeDonationAmount.ts b/migration/1650907676905-RemoveNegativeDonationAmount.ts index 39927a707..ef9350a6a 100644 --- a/migration/1650907676905-RemoveNegativeDonationAmount.ts +++ b/migration/1650907676905-RemoveNegativeDonationAmount.ts @@ -13,5 +13,5 @@ export class RemoveNegativeDonationAmount1650907676905 } } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1651611664220-fixUserIdForDonations.ts b/migration/1651611664220-fixUserIdForDonations.ts index bada2a83b..5f1b816e6 100644 --- a/migration/1651611664220-fixUserIdForDonations.ts +++ b/migration/1651611664220-fixUserIdForDonations.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class fixUserIdForDonations1651611664220 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const donationTableExists = await queryRunner.hasTable('donation'); @@ -20,5 +19,5 @@ export class fixUserIdForDonations1651611664220 implements MigrationInterface { } } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1652369931352-fillDonationsPriceManually.ts b/migration/1652369931352-fillDonationsPriceManually.ts index d3a04c3db..4af4f9306 100644 --- a/migration/1652369931352-fillDonationsPriceManually.ts +++ b/migration/1652369931352-fillDonationsPriceManually.ts @@ -220,7 +220,6 @@ const donations = [ }, ]; -// tslint:disable-next-line:class-name export class fillDonationsPriceManually1652369931352 implements MigrationInterface { diff --git a/migration/1654142618696-fillAdminUserId.ts b/migration/1654142618696-fillAdminUserId.ts index 7066457de..372eb8dc4 100644 --- a/migration/1654142618696-fillAdminUserId.ts +++ b/migration/1654142618696-fillAdminUserId.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class fillAdminUserId1654142618696 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const projectTableExists = await queryRunner.hasTable('project'); diff --git a/migration/1654415838996-fillRelatedAddressesFromProjectsTable.ts b/migration/1654415838996-fillRelatedAddressesFromProjectsTable.ts index f4cbe368d..81798e6fb 100644 --- a/migration/1654415838996-fillRelatedAddressesFromProjectsTable.ts +++ b/migration/1654415838996-fillRelatedAddressesFromProjectsTable.ts @@ -1,5 +1,4 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -import config from '../src/config'; import { Project } from '../src/entities/project'; import { NETWORK_IDS } from '../src/provider'; import { ENVIRONMENTS } from '../src/utils/utils'; @@ -15,19 +14,19 @@ const insertRelatedAddress = async (params: { INSERT INTO project_address( "networkId", address, "projectId", "userId", "isRecipient") VALUES (${networkId}, '${project.walletAddress?.toLowerCase()}', ${ - project.id - }, ${Number(project.admin)}, true); + project.id + }, ${Number(project.admin)}, true); `, ); }; -// tslint:disable-next-line:class-name + export class fillRelatedAddressesFromProjectsTable1654415838996 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const projectTableExists = await queryRunner.hasTable('project'); if (!projectTableExists) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log( 'The project table doesnt exist, so there is no need to relate it to relatedAddreses', ); @@ -36,7 +35,7 @@ export class fillRelatedAddressesFromProjectsTable1654415838996 const userTableExists = await queryRunner.hasTable('user'); if (!userTableExists) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log( 'The user table doesnt exist, so there is no need to relate it to relatedAddresses', ); @@ -102,7 +101,7 @@ export class fillRelatedAddressesFromProjectsTable1654415838996 } } - async down(queryRunner: QueryRunner): Promise { + async down(_queryRunner: QueryRunner): Promise { // } } diff --git a/migration/1656398065898-addOrderForTokens.ts b/migration/1656398065898-addOrderForTokens.ts index 655450231..5e929ca4e 100644 --- a/migration/1656398065898-addOrderForTokens.ts +++ b/migration/1656398065898-addOrderForTokens.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class addOrderForTokens1656398065898 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { await queryRunner.query( diff --git a/migration/1657696335423-createMainCategoryTable.ts b/migration/1657696335423-createMainCategoryTable.ts index bddd32cee..e566fb1cc 100644 --- a/migration/1657696335423-createMainCategoryTable.ts +++ b/migration/1657696335423-createMainCategoryTable.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class createMainCategoryTable1657696335423 implements MigrationInterface { diff --git a/migration/1657696850026-seedMainCategories.ts b/migration/1657696850026-seedMainCategories.ts index 0fec2f6dd..6648c5a17 100644 --- a/migration/1657696850026-seedMainCategories.ts +++ b/migration/1657696850026-seedMainCategories.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class seedMainCategories1657696850026 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` diff --git a/migration/1657701834486-relateCategoriesToMainCategories.ts b/migration/1657701834486-relateCategoriesToMainCategories.ts index dd6e6b999..122cc4079 100644 --- a/migration/1657701834486-relateCategoriesToMainCategories.ts +++ b/migration/1657701834486-relateCategoriesToMainCategories.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class relateCategoriesToMainCategories1657701834486 implements MigrationInterface { diff --git a/migration/1657786628179-addIsActiveToCategories.ts b/migration/1657786628179-addIsActiveToCategories.ts index ba6c40b6d..3bec97242 100644 --- a/migration/1657786628179-addIsActiveToCategories.ts +++ b/migration/1657786628179-addIsActiveToCategories.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class addIsActiveToCategories1657786628179 implements MigrationInterface { diff --git a/migration/1659516575436-makeSomeFailedDonationsVerified.ts b/migration/1659516575436-makeSomeFailedDonationsVerified.ts index 4322e61e3..b62089074 100644 --- a/migration/1659516575436-makeSomeFailedDonationsVerified.ts +++ b/migration/1659516575436-makeSomeFailedDonationsVerified.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class makeSomeFailedDonationsVerified1659516575436 implements MigrationInterface { @@ -43,7 +42,7 @@ export class makeSomeFailedDonationsVerified1659516575436 ); } - async down(queryRunner: QueryRunner): Promise { + async down(_queryRunner: QueryRunner): Promise { // } } diff --git a/migration/1661116436720-addGoerliTokens.ts b/migration/1661116436720-addGoerliTokens.ts index 018a696a7..1057948c0 100644 --- a/migration/1661116436720-addGoerliTokens.ts +++ b/migration/1661116436720-addGoerliTokens.ts @@ -3,7 +3,6 @@ import { Token } from '../src/entities/token'; import seedTokens from './data/seedTokens'; import config from '../src/config'; -// tslint:disable-next-line:class-name export class addGoerliTokens1661116436720 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const environment = config.get('ENVIRONMENT') as string; @@ -63,5 +62,5 @@ export class addGoerliTokens1661116436720 implements MigrationInterface { ); } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1661163993626-ChangeProjectAddressToGoerli.ts b/migration/1661163993626-ChangeProjectAddressToGoerli.ts index 2bea34c07..ed9dd549e 100644 --- a/migration/1661163993626-ChangeProjectAddressToGoerli.ts +++ b/migration/1661163993626-ChangeProjectAddressToGoerli.ts @@ -4,11 +4,10 @@ export class ChangeProjectAddressToGoerli1661163993626 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { - const projectAddressTableExists = await queryRunner.hasTable( - 'project_address', - ); + const projectAddressTableExists = + await queryRunner.hasTable('project_address'); if (!projectAddressTableExists) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('The project_address table doesnt exist'); return; } @@ -20,11 +19,10 @@ export class ChangeProjectAddressToGoerli1661163993626 } async down(queryRunner: QueryRunner): Promise { - const projectAddressTableExists = await queryRunner.hasTable( - 'project_address', - ); + const projectAddressTableExists = + await queryRunner.hasTable('project_address'); if (!projectAddressTableExists) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('The project_address table doesnt exist'); return; } diff --git a/migration/1662877385100-createUserTable.ts b/migration/1662877385100-createUserTable.ts index fbcb900e4..0d9683224 100644 --- a/migration/1662877385100-createUserTable.ts +++ b/migration/1662877385100-createUserTable.ts @@ -1,12 +1,11 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class createUserTable1662877385100 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const userTableExists = await queryRunner.hasTable('public.user'); if (userTableExists) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('user table already exists'); return; } @@ -41,6 +40,5 @@ export class createUserTable1662877385100 implements MigrationInterface { ); } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1662877385200-createProjectTable.ts b/migration/1662877385200-createProjectTable.ts index a077b3431..11b0bccf4 100644 --- a/migration/1662877385200-createProjectTable.ts +++ b/migration/1662877385200-createProjectTable.ts @@ -1,12 +1,11 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class createProjectTable1662877385200 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const projectTableExists = await queryRunner.hasTable('project'); if (projectTableExists) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('Project table already exists'); return; } @@ -99,6 +98,5 @@ export class createProjectTable1662877385200 implements MigrationInterface { ); } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1662877385300-createPowerBoostingTable.ts b/migration/1662877385300-createPowerBoostingTable.ts index fcbbb82f4..fd9042ffb 100644 --- a/migration/1662877385300-createPowerBoostingTable.ts +++ b/migration/1662877385300-createPowerBoostingTable.ts @@ -1,16 +1,14 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class createPowerBoostingTable1662877385300 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { - const powerBoostingTableExists = await queryRunner.hasTable( - 'power_boosting', - ); + const powerBoostingTableExists = + await queryRunner.hasTable('power_boosting'); if (powerBoostingTableExists) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('createPowerBoostingTable power_boosting table exists'); return; } @@ -54,7 +52,6 @@ export class createPowerBoostingTable1662877385300 ); } - // tslint:disable-next-line:no-empty async down(queryRunner: QueryRunner): Promise { await queryRunner.query(`DROP TABLE IF EXISTS power_boosting CASCADE`); } diff --git a/migration/1662877385302-powerBalanceSnapshot.ts b/migration/1662877385302-powerBalanceSnapshot.ts index af1a6cdbb..2ed36ad73 100644 --- a/migration/1662877385302-powerBalanceSnapshot.ts +++ b/migration/1662877385302-powerBalanceSnapshot.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class powerBalanceSnapshot1662877385302 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` diff --git a/migration/1662877385303-percentageBalanceSnapshot.ts b/migration/1662877385303-percentageBalanceSnapshot.ts index a2cb05fcb..738b4aa72 100644 --- a/migration/1662877385303-percentageBalanceSnapshot.ts +++ b/migration/1662877385303-percentageBalanceSnapshot.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class percentageBalanceSnapshot1662877385303 implements MigrationInterface { diff --git a/migration/1662877385311-createPowerRoundTable.ts b/migration/1662877385311-createPowerRoundTable.ts index fa2a78a81..5fe860e94 100644 --- a/migration/1662877385311-createPowerRoundTable.ts +++ b/migration/1662877385311-createPowerRoundTable.ts @@ -1,12 +1,11 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class createPowerRoundTable1662877385311 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const powerRoundTableExists = await queryRunner.hasTable('power_round'); if (powerRoundTableExists) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('createPowerRoundTable power_round table exists'); return; } @@ -24,7 +23,6 @@ export class createPowerRoundTable1662877385311 implements MigrationInterface { ); } - // tslint:disable-next-line:no-empty async down(queryRunner: QueryRunner): Promise { await queryRunner.query(`DROP TABLE IF EXISTS power_round CASCADE`); } diff --git a/migration/1662877385339-UserProjectPowerView.ts b/migration/1662877385339-UserProjectPowerView.ts index 8e94e6fdf..da49f23c4 100644 --- a/migration/1662877385339-UserProjectPowerView.ts +++ b/migration/1662877385339-UserProjectPowerView.ts @@ -31,6 +31,5 @@ export class UserProjectPowerView1662877385339 implements MigrationInterface { ); } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1662915983385-ProjectPowerView.ts b/migration/1662915983385-ProjectPowerView.ts index bd9aa5b46..a3e1fded6 100644 --- a/migration/1662915983385-ProjectPowerView.ts +++ b/migration/1662915983385-ProjectPowerView.ts @@ -57,6 +57,5 @@ export class ProjectPowerView1662915983385 implements MigrationInterface { ); } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1663594895751-takePowerSnapshotProcedure.ts b/migration/1663594895751-takePowerSnapshotProcedure.ts index c3f61edc3..2936dec11 100644 --- a/migration/1663594895751-takePowerSnapshotProcedure.ts +++ b/migration/1663594895751-takePowerSnapshotProcedure.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class TakePowerBoostingSnapshotProcedure1663594895751 implements MigrationInterface { diff --git a/migration/1664367797442-changeMainCategoriesAndSubCategories.ts b/migration/1664367797442-changeMainCategoriesAndSubCategories.ts index 34a1523c1..ee2613c10 100644 --- a/migration/1664367797442-changeMainCategoriesAndSubCategories.ts +++ b/migration/1664367797442-changeMainCategoriesAndSubCategories.ts @@ -19,7 +19,6 @@ const updateSubCategory = async ( `); }; -// tslint:disable-next-line:class-name export class changeMainCategoriesAndSubCategories1664367797442 implements MigrationInterface { @@ -238,5 +237,5 @@ export class changeMainCategoriesAndSubCategories1664367797442 } } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1665917110542-seedNewCategories.ts b/migration/1665917110542-seedNewCategories.ts index 347563164..3bf240ef0 100644 --- a/migration/1665917110542-seedNewCategories.ts +++ b/migration/1665917110542-seedNewCategories.ts @@ -18,7 +18,6 @@ const addSubCategory = async ( ;`); }; -// tslint:disable-next-line:class-name export class seedNewCategories1665917110542 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { await Promise.all([ diff --git a/migration/1666068280230-deleteGnosisRecipientsOfGivingblocksProjects.ts b/migration/1666068280230-deleteGnosisRecipientsOfGivingblocksProjects.ts index f10777a81..351ff150c 100644 --- a/migration/1666068280230-deleteGnosisRecipientsOfGivingblocksProjects.ts +++ b/migration/1666068280230-deleteGnosisRecipientsOfGivingblocksProjects.ts @@ -1,16 +1,14 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class deleteGnosisRecipientsOfGivingblocksProjects1666068280230 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const projectTableExists = await queryRunner.hasTable('project'); - const projectAddressTableExists = await queryRunner.hasTable( - 'project_address', - ); + const projectAddressTableExists = + await queryRunner.hasTable('project_address'); if (!projectTableExists || !projectAddressTableExists) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('The project table or project_Address table doesnt exist', { projectAddressTableExists, projectTableExists, @@ -25,5 +23,5 @@ export class deleteGnosisRecipientsOfGivingblocksProjects1666068280230 `); } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1668411738120-ProjectFuturePowerView.ts b/migration/1668411738120-ProjectFuturePowerView.ts index 0f779c2af..58c2a95b5 100644 --- a/migration/1668411738120-ProjectFuturePowerView.ts +++ b/migration/1668411738120-ProjectFuturePowerView.ts @@ -56,6 +56,5 @@ export class ProjectFuturePowerView1668411738120 implements MigrationInterface { ); } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1670422136574-createSnashotHistoricTables.ts b/migration/1670422136574-createSnashotHistoricTables.ts index c7391dc01..f80418f57 100644 --- a/migration/1670422136574-createSnashotHistoricTables.ts +++ b/migration/1670422136574-createSnashotHistoricTables.ts @@ -1,11 +1,5 @@ -import { - MigrationInterface, - QueryRunner, - Table, - TableForeignKey, -} from 'typeorm'; +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; -// tslint:disable-next-line:class-name export class createSnashotHistoricTables1670422136574 implements MigrationInterface { diff --git a/migration/1670429143091-createGivPowerHistoricTablesProcedure.ts b/migration/1670429143091-createGivPowerHistoricTablesProcedure.ts index 0a1a85f23..a25742628 100644 --- a/migration/1670429143091-createGivPowerHistoricTablesProcedure.ts +++ b/migration/1670429143091-createGivPowerHistoricTablesProcedure.ts @@ -1,6 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -// tslint:disable-next-line:class-name export class createGivPowerHistoricTablesProcedure1670429143091 implements MigrationInterface { diff --git a/migration/1671448387986-LastSnapshotProjectPowerView.ts b/migration/1671448387986-LastSnapshotProjectPowerView.ts index b7d0486e5..220352c58 100644 --- a/migration/1671448387986-LastSnapshotProjectPowerView.ts +++ b/migration/1671448387986-LastSnapshotProjectPowerView.ts @@ -69,6 +69,5 @@ export class LastSnapshotProjectPowerView1671448387986 ); } - // tslint:disable-next-line:no-empty - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1672836674875-createTestAdminUser.ts b/migration/1672836674875-createTestAdminUser.ts index f0092b730..bf54010f0 100644 --- a/migration/1672836674875-createTestAdminUser.ts +++ b/migration/1672836674875-createTestAdminUser.ts @@ -1,13 +1,13 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; import config from '../src/config'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const bcrypt = require('bcrypt'); export class createTestAdminUser1672836674875 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const environment = config.get('ENVIRONMENT') as string; if (environment !== 'local' && environment !== 'test') { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('We just create admin user in local and test ENVs'); return; } diff --git a/migration/1676463494406-AddProjectDescriptionSummaryProjectUpdateContentSummary.ts b/migration/1676463494406-AddProjectDescriptionSummaryProjectUpdateContentSummary.ts index f9c97e413..c85430312 100644 --- a/migration/1676463494406-AddProjectDescriptionSummaryProjectUpdateContentSummary.ts +++ b/migration/1676463494406-AddProjectDescriptionSummaryProjectUpdateContentSummary.ts @@ -15,5 +15,5 @@ export class AddProjectDescriptionSummaryProjectUpdateContentSummary167646349440 ); } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1676472760533-FillProjectDescriptionSummaryProjectUpdateContentSummary.ts b/migration/1676472760533-FillProjectDescriptionSummaryProjectUpdateContentSummary.ts index 921399f31..fd8e97c0c 100644 --- a/migration/1676472760533-FillProjectDescriptionSummaryProjectUpdateContentSummary.ts +++ b/migration/1676472760533-FillProjectDescriptionSummaryProjectUpdateContentSummary.ts @@ -7,6 +7,7 @@ export class FillProjectDescriptionSummaryProjectUpdateContentSummary16764727605 { async up(queryRunner: QueryRunner): Promise { let skip = 0; + // eslint-disable-next-line no-constant-condition while (true) { const [projects, count] = await queryRunner.manager.findAndCount( Project, @@ -33,6 +34,7 @@ export class FillProjectDescriptionSummaryProjectUpdateContentSummary16764727605 } skip = 0; + // eslint-disable-next-line no-constant-condition while (true) { const [projectUpdates, count] = await queryRunner.manager.findAndCount(ProjectUpdate, { @@ -57,5 +59,5 @@ export class FillProjectDescriptionSummaryProjectUpdateContentSummary16764727605 } } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1677073819672-SetProjectReviewStatus.ts b/migration/1677073819672-SetProjectReviewStatus.ts index 195e93f88..6716dbb41 100644 --- a/migration/1677073819672-SetProjectReviewStatus.ts +++ b/migration/1677073819672-SetProjectReviewStatus.ts @@ -1,5 +1,4 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -import { Project, ReviewStatus } from '../src/entities/project'; export class SetProjectReviewStatus1677073819672 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { @@ -25,5 +24,5 @@ export class SetProjectReviewStatus1677073819672 implements MigrationInterface { `); } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1677742523974-addPolygonTokens.ts b/migration/1677742523974-addPolygonTokens.ts index 62043fdbd..383cc0182 100644 --- a/migration/1677742523974-addPolygonTokens.ts +++ b/migration/1677742523974-addPolygonTokens.ts @@ -1,10 +1,8 @@ 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'; -// tslint:disable-next-line:class-name export class addGoerliTokens1677742523974 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { await queryRunner.manager.save( @@ -37,5 +35,5 @@ export class addGoerliTokens1677742523974 implements MigrationInterface { } } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1680014857601-addCeloTokens.ts b/migration/1680014857601-addCeloTokens.ts index 6bc5a6f18..0741a54b6 100644 --- a/migration/1680014857601-addCeloTokens.ts +++ b/migration/1680014857601-addCeloTokens.ts @@ -4,7 +4,6 @@ import seedTokens from './data/seedTokens'; import config from '../src/config'; import { NETWORK_IDS } from '../src/provider'; -// tslint:disable-next-line:class-name export class addCeloTokens1680014857601 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const environment = config.get('ENVIRONMENT') as string; @@ -50,5 +49,5 @@ export class addCeloTokens1680014857601 implements MigrationInterface { } } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1680507337701-add_some_donations_to_db.ts b/migration/1680507337701-add_some_donations_to_db.ts index 4de4da992..64c84c1a9 100644 --- a/migration/1680507337701-add_some_donations_to_db.ts +++ b/migration/1680507337701-add_some_donations_to_db.ts @@ -1,8 +1,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; +import moment from 'moment'; import config from '../src/config'; import { Donation } from '../src/entities/donation'; import { NETWORK_IDS } from '../src/provider'; -import moment from 'moment'; // For seeing donations detail you can see this message ( if you have access to channel) // https://discord.com/channels/679428761438912522/928813033600475207/1089868809302724618 @@ -317,7 +317,7 @@ export class addSomeDonationsToDb1680507337701 implements MigrationInterface { const environment = config.get('ENVIRONMENT') as string; if (environment !== 'production') { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('We want to create these donations in production DB'); return; } @@ -347,7 +347,7 @@ export class addSomeDonationsToDb1680507337701 implements MigrationInterface { VALUES ('${tx.toWalletAddress}', ${tx.projectId}, '${tx.fromWalletAddress}', ${user.id}, ${tx.amount}, '${tx.currency}', '${tx.transactionId}', ${tx.transactionNetworkId}, false, ${tx.valueUsd}, 'verified', true, false, false, '${createdAt}'); `); } catch (e) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('Couldnt create donation error: ', e.message); } } diff --git a/migration/1680539882510-TransformBase64ImagesToIpfs.ts b/migration/1680539882510-TransformBase64ImagesToIpfs.ts index 7e215c757..9a2960159 100644 --- a/migration/1680539882510-TransformBase64ImagesToIpfs.ts +++ b/migration/1680539882510-TransformBase64ImagesToIpfs.ts @@ -5,9 +5,10 @@ import { changeBase64ToIpfsImageInHTML } from '../src/utils/documents'; export class TransformBase64ImagesToIpfs1680539882510 implements MigrationInterface { - async up(queryRunner: QueryRunner): Promise { + async up(_queryRunner: QueryRunner): Promise { // paginate through project updates let skip = 0; + // eslint-disable-next-line no-constant-condition while (true) { const [projectUpdates, count] = await ProjectUpdate.findAndCount({ where: { content: Like('%;base64%') }, @@ -19,7 +20,7 @@ export class TransformBase64ImagesToIpfs1680539882510 // transform base64 images to ipfs await Promise.all( projectUpdates.map(async ({ id, content }) => { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log( 'Transforming base64 images to ipfs for project update', id, @@ -34,6 +35,7 @@ export class TransformBase64ImagesToIpfs1680539882510 } skip = 0; + // eslint-disable-next-line no-constant-condition while (true) { const [projects, count] = await Project.findAndCount({ where: { description: Like('%;base64%') }, @@ -46,7 +48,7 @@ export class TransformBase64ImagesToIpfs1680539882510 await Promise.all( projects.map(async ({ id, description }) => { if (!description) return; - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('Transforming base64 images to ipfs for project', id); description = await changeBase64ToIpfsImageInHTML(description); return Project.update(id, { description }); @@ -58,5 +60,5 @@ export class TransformBase64ImagesToIpfs1680539882510 } } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1681125863016-create_some_test_admi_users.ts b/migration/1681125863016-create_some_test_admi_users.ts index d953c3771..71e557f72 100644 --- a/migration/1681125863016-create_some_test_admi_users.ts +++ b/migration/1681125863016-create_some_test_admi_users.ts @@ -2,7 +2,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; import config from '../src/config'; import { generateRandomEtheriumAddress } from '../test/testUtils'; import { UserRole } from '../src/entities/user'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const bcrypt = require('bcrypt'); export class createSomeTestAdmiUsers1681125863016 @@ -11,7 +11,7 @@ export class createSomeTestAdmiUsers1681125863016 async up(queryRunner: QueryRunner): Promise { const environment = config.get('ENVIRONMENT') as string; if (environment !== 'local' && environment !== 'test') { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('We just create admin user in local and test ENVs'); return; } @@ -19,22 +19,22 @@ export class createSomeTestAdmiUsers1681125863016 await queryRunner.query(` INSERT INTO public.user (email, "walletAddress", role,"loginType", name,"encryptedPassword") VALUES('campaignManager@giveth.io', '${generateRandomEtheriumAddress()}', '${ - UserRole.CAMPAIGN_MANAGER - }','wallet', 'test', '${hash}'); + UserRole.CAMPAIGN_MANAGER + }','wallet', 'test', '${hash}'); `); await queryRunner.query(` INSERT INTO public.user (email, "walletAddress", role,"loginType", name,"encryptedPassword") VALUES('reviewer@giveth.io', '${generateRandomEtheriumAddress()}', '${ - UserRole.VERIFICATION_FORM_REVIEWER - }','wallet', 'test', '${hash}'); + UserRole.VERIFICATION_FORM_REVIEWER + }','wallet', 'test', '${hash}'); `); await queryRunner.query(` INSERT INTO public.user (email, "walletAddress", role,"loginType", name,"encryptedPassword") VALUES('operator@giveth.io', '${generateRandomEtheriumAddress()}', '${ - UserRole.OPERATOR - }','wallet', 'test', '${hash}'); + UserRole.OPERATOR + }','wallet', 'test', '${hash}'); `); } diff --git a/migration/1683764388981-AddReferredTableRelation.ts b/migration/1683764388981-AddReferredTableRelation.ts index c637521c0..9cffe390a 100644 --- a/migration/1683764388981-AddReferredTableRelation.ts +++ b/migration/1683764388981-AddReferredTableRelation.ts @@ -12,9 +12,8 @@ export class AddReferredTableRelation1683764388981 { async up(queryRunner: QueryRunner): Promise { // Create referred_event table - const referredEventTableExists = await queryRunner.hasTable( - 'referred_event', - ); + const referredEventTableExists = + await queryRunner.hasTable('referred_event'); if (!referredEventTableExists) { await queryRunner.createTable( diff --git a/migration/1684654545845-add_some_mainnet_donations.ts b/migration/1684654545845-add_some_mainnet_donations.ts index 51e9096d5..4810afef3 100644 --- a/migration/1684654545845-add_some_mainnet_donations.ts +++ b/migration/1684654545845-add_some_mainnet_donations.ts @@ -1,8 +1,8 @@ +import moment from 'moment'; +import { MigrationInterface, QueryRunner } from 'typeorm'; import config from '../src/config'; import { Donation } from '../src/entities/donation'; import { NETWORK_IDS } from '../src/provider'; -import moment from 'moment'; -import { MigrationInterface, QueryRunner } from 'typeorm'; import { updateUserTotalDonated, updateUserTotalReceived, @@ -232,7 +232,7 @@ export class addSomeMainnetDonations1684654545845 const environment = config.get('ENVIRONMENT') as string; if (environment !== 'production') { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('We want to create these donations in production DB'); return; } diff --git a/migration/1689504711172-CreateProjectUserInstantPowerView.ts b/migration/1689504711172-CreateProjectUserInstantPowerView.ts index b268c10ef..0123f1dfd 100644 --- a/migration/1689504711172-CreateProjectUserInstantPowerView.ts +++ b/migration/1689504711172-CreateProjectUserInstantPowerView.ts @@ -25,5 +25,5 @@ export class CreateProjectUserInstantPowerView1689504711172 `); } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1690716071288-PowerBalanceSnapshotBalanceNullable.ts b/migration/1690716071288-PowerBalanceSnapshotBalanceNullable.ts index 3aa2f0c8a..b1e45d685 100644 --- a/migration/1690716071288-PowerBalanceSnapshotBalanceNullable.ts +++ b/migration/1690716071288-PowerBalanceSnapshotBalanceNullable.ts @@ -18,5 +18,5 @@ export class PowerBalanceSnapshotBalanceNullable1690716071288 ); } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1690790078452-AddMissingPowerBalanceSnapshots.ts b/migration/1690790078452-AddMissingPowerBalanceSnapshots.ts index 750ef2416..42e462e7c 100644 --- a/migration/1690790078452-AddMissingPowerBalanceSnapshots.ts +++ b/migration/1690790078452-AddMissingPowerBalanceSnapshots.ts @@ -13,5 +13,5 @@ export class AddMissingPowerBalanceSnapshots1690790078452 `); } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1692623384774-MigrateToBalanceAggregator.ts b/migration/1692623384774-MigrateToBalanceAggregator.ts index 148464e2b..a62702130 100644 --- a/migration/1692623384774-MigrateToBalanceAggregator.ts +++ b/migration/1692623384774-MigrateToBalanceAggregator.ts @@ -23,5 +23,5 @@ export class MigrateToBalanceAggregator1692623384774 `); } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts b/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts index e7c2e85f4..380074cb2 100644 --- a/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts +++ b/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class AddEligibleNetworksToQfRoundEntity1694295208252 implements MigrationInterface diff --git a/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts b/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts index d8d6df46f..62049761d 100644 --- a/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts +++ b/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts @@ -10,7 +10,7 @@ export class AddEligibleNetworksToPreviousQfRounds1694635872128 // Define the eligible network IDs based on the conditions const eligibleNetworks = environment !== 'production' - ? [1, 3, 5, 100, 137, 10, 420, 56, 42220, 44787] // Include testnets for staging + ? [1, 3, 5, 100, 137, 10, 11155420, 56, 42220, 44787] // Include testnets for staging : [1, 137, 56, 42220, 100, 10]; // Exclude testnets for non-staging // Update the "qf_round" table with the new eligibleNetworks values diff --git a/migration/1696918830123-add_octant_donations_to_db.ts b/migration/1696918830123-add_octant_donations_to_db.ts index 995907f78..7e4c163b9 100644 --- a/migration/1696918830123-add_octant_donations_to_db.ts +++ b/migration/1696918830123-add_octant_donations_to_db.ts @@ -1,7 +1,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; +import moment from 'moment'; import config from '../src/config'; import { AppDataSource } from '../src/orm'; -import moment from 'moment'; import { findProjectById } from '../src/repositories/projectRepository'; import { Project } from '../src/entities/project'; import { calculateGivbackFactor } from '../src/services/givbackService'; @@ -82,7 +82,7 @@ export class addOctantDonationsToDb1696918830123 implements MigrationInterface { const environment = config.get('ENVIRONMENT') as string; if (environment !== 'production') { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('We want to create these donations in production DB'); return; } diff --git a/migration/1702374813793-addChainTypeToProjectAddressAndDonation.ts b/migration/1702374813793-addChainTypeToProjectAddressAndDonation.ts index a8e513f14..0bdb5e891 100644 --- a/migration/1702374813793-addChainTypeToProjectAddressAndDonation.ts +++ b/migration/1702374813793-addChainTypeToProjectAddressAndDonation.ts @@ -26,5 +26,5 @@ export class addChainTypeToProjectAddressAndDonation1702374813793 await queryRunner.query(`UPDATE "token" SET "chainType" = 'EVM'`); } - async down(queryRunner: QueryRunner): Promise {} + async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1703044586989-addSolanaToken.ts b/migration/1703044586989-addSolanaToken.ts index 1bb1c526a..dd3fb8ddf 100644 --- a/migration/1703044586989-addSolanaToken.ts +++ b/migration/1703044586989-addSolanaToken.ts @@ -1,8 +1,8 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; import seedTokens from './data/seedTokens'; import { ChainType } from '../src/types/network'; import { SOLANA_SYSTEM_PROGRAM } from '../src/utils/networks'; import { ENVIRONMENTS } from '../src/utils/utils'; -import { MigrationInterface, QueryRunner } from 'typeorm'; import { NETWORK_IDS } from '../src/provider'; import { Token } from '../src/entities/token'; diff --git a/migration/1703398409668-add_missed_op_donations_to_db.ts b/migration/1703398409668-add_missed_op_donations_to_db.ts index 9ce3143c5..2cc2fb358 100644 --- a/migration/1703398409668-add_missed_op_donations_to_db.ts +++ b/migration/1703398409668-add_missed_op_donations_to_db.ts @@ -1,7 +1,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; +import moment from 'moment'; import config from '../src/config'; import { AppDataSource } from '../src/orm'; -import moment from 'moment'; import { findProjectById } from '../src/repositories/projectRepository'; import { Project } from '../src/entities/project'; import { calculateGivbackFactor } from '../src/services/givbackService'; @@ -236,7 +236,7 @@ export class addMissedOpDonationsToDb1703398409668 const environment = config.get('ENVIRONMENT') as string; if (environment !== 'production') { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('We just want to create these donations in production DB'); return; } @@ -261,15 +261,15 @@ export class addMissedOpDonationsToDb1703398409668 INSERT INTO donation ("toWalletAddress", "projectId", "fromWalletAddress", "userId", amount, currency, "transactionId", "transactionNetworkId", anonymous, "valueUsd", status, "segmentNotified", "isTokenEligibleForGivback", "isProjectVerified", "createdAt", "givbackFactor", "powerRound", "projectRank", "bottomRankInRound", "qfRoundId", "tokenAddress") VALUES ('${tx.toWalletAddress?.toLowerCase()}', ${ - tx.projectId - }, '${tx.fromWalletAddress?.toLocaleLowerCase()}', ${user.id}, ${ - tx.amount - }, '${tx.currency}', '${tx.transactionId?.toLocaleLowerCase()}', ${ - tx.transactionNetworkId - }, false, ${tx.valueUsd}, 'verified', + tx.projectId + }, '${tx.fromWalletAddress?.toLocaleLowerCase()}', ${user.id}, ${ + tx.amount + }, '${tx.currency}', '${tx.transactionId?.toLocaleLowerCase()}', ${ + tx.transactionNetworkId + }, false, ${tx.valueUsd}, 'verified', true, true, true, '${createdAt}', ${givbackFactor}, ${powerRound}, ${projectRank}, ${bottomRankInRound}, ${QF_ROUND_ID}, '${ - tx.tokenAddress - }'); + tx.tokenAddress + }'); `); await updateUserTotalDonated(user.id); @@ -281,7 +281,7 @@ export class addMissedOpDonationsToDb1703398409668 await refreshProjectDonationSummaryView(); } - async down(queryRunner: QueryRunner): Promise { + async down(_queryRunner: QueryRunner): Promise { // } } diff --git a/migration/1704991714385-addCoingeckoIdToOptimismTokens.ts b/migration/1704991714385-addCoingeckoIdToOptimismTokens.ts deleted file mode 100644 index 610a6dcee..000000000 --- a/migration/1704991714385-addCoingeckoIdToOptimismTokens.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; -import { NETWORK_IDS } from '../src/provider'; - -const tokenUpdates = { - ETH: 'ethereum', - OP: 'optimism', - WETH: 'weth', - DAI: 'dai', - LINK: 'chainlink', - WBTC: 'wrapped-bitcoin', - SNX: 'havven', - USDT: 'tether', - USDC: 'usd-coin', -}; - -export class addCoingeckoIdToOptimismTokens1704991714385 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - const tokens = await queryRunner.query( - `SELECT * FROM token WHERE "networkId" = $1 OR "networkId" = $2`, - [NETWORK_IDS.OPTIMISTIC, NETWORK_IDS.OPTIMISM_GOERLI], - ); - - for (const token of tokens) { - const coingeckoId = tokenUpdates[token.symbol]; - if (coingeckoId) { - await queryRunner.query( - ` - UPDATE token - SET "coingeckoId" = $1 - WHERE id = $2 - `, - [coingeckoId, token.id], - ); - } - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE public.token - SET "coingeckoId" = NULL - WHERE "networkId" = 10 OR "networkId" = 420 - `); - } -} diff --git a/migration/1706180533852-relateDonationToRecurringDonation.ts b/migration/1706180533852-relateDonationToRecurringDonation.ts index 5e9bcff83..8139ef9b0 100644 --- a/migration/1706180533852-relateDonationToRecurringDonation.ts +++ b/migration/1706180533852-relateDonationToRecurringDonation.ts @@ -7,7 +7,6 @@ export class relateDonationToRecurringDonation1706180533852 await queryRunner.query(` ALTER TABLE "donation" ADD COLUMN "recurringDonationId" integer, - ADD COLUMN "recurringDonation" integer; `); await queryRunner.query(` @@ -36,7 +35,6 @@ export class relateDonationToRecurringDonation1706180533852 await queryRunner.query(` ALTER TABLE "donation" DROP COLUMN "recurringDonationId", - DROP COLUMN "recurringDonation"; `); } } diff --git a/migration/1707738577647-addDraftDonationTable.ts b/migration/1707738577647-addDraftDonationTable.ts index 7f3875f78..cfb0a965c 100644 --- a/migration/1707738577647-addDraftDonationTable.ts +++ b/migration/1707738577647-addDraftDonationTable.ts @@ -70,5 +70,5 @@ export class AddDraftDonationTable1707738577647 implements MigrationInterface { `); } - public async down(queryRunner: QueryRunner): Promise {} + public async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1707834685578-addRecurringDonationMissingColumns.ts b/migration/1707834685578-addRecurringDonationMissingColumns.ts index 352767d56..286d2d886 100644 --- a/migration/1707834685578-addRecurringDonationMissingColumns.ts +++ b/migration/1707834685578-addRecurringDonationMissingColumns.ts @@ -9,5 +9,5 @@ export class AddRecurringDonationMissingColumns1707834685578 `); } - public async down(queryRunner: QueryRunner): Promise {} + public async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1707892354691-addDonationMatchedIdColumnToDraftDonation.ts b/migration/1707892354691-addDonationMatchedIdColumnToDraftDonation.ts index f7cf4dde4..365d106ce 100644 --- a/migration/1707892354691-addDonationMatchedIdColumnToDraftDonation.ts +++ b/migration/1707892354691-addDonationMatchedIdColumnToDraftDonation.ts @@ -9,5 +9,5 @@ export class AddDonationMatchedIdColumnToDraftDonation1707892354691 ); } - public async down(queryRunner: QueryRunner): Promise {} + public async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1708279692128-addArbitrumTokens.ts b/migration/1708279692128-addArbitrumTokens.ts index 0026a882d..79dd234ee 100644 --- a/migration/1708279692128-addArbitrumTokens.ts +++ b/migration/1708279692128-addArbitrumTokens.ts @@ -49,5 +49,5 @@ export class AddArbitrumTokens1708279692128 implements MigrationInterface { } } - public async down(queryRunner: QueryRunner): Promise {} + public async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1708280336872-project_actual_matching_view_v5.ts b/migration/1708280336872-project_actual_matching_view_v5.ts index defc9b3f6..95a84981e 100644 --- a/migration/1708280336872-project_actual_matching_view_v5.ts +++ b/migration/1708280336872-project_actual_matching_view_v5.ts @@ -90,7 +90,7 @@ export class ProjectActualMatchingViewV51708280336872 `); } - public async down(queryRunner: QueryRunner): Promise { + public async down(_queryRunner: QueryRunner): Promise { // } } diff --git a/migration/1708511332372-drop_user_passport_score_table.ts b/migration/1708511332372-drop_user_passport_score_table.ts index 2b95a1565..283f32769 100644 --- a/migration/1708511332372-drop_user_passport_score_table.ts +++ b/migration/1708511332372-drop_user_passport_score_table.ts @@ -12,7 +12,7 @@ export class DropUserPassportScoreTable1708511332372 `); } - async down(queryRunner: QueryRunner): Promise { + async down(_queryRunner: QueryRunner): Promise { // } } diff --git a/migration/1708511332373-project_actual_matching_v6.ts b/migration/1708511332373-project_actual_matching_v6.ts index a2374f41a..4d5c04c3a 100644 --- a/migration/1708511332373-project_actual_matching_v6.ts +++ b/migration/1708511332373-project_actual_matching_v6.ts @@ -89,7 +89,7 @@ export class ProjectActualMatchingV61708511332373 `); } - async down(queryRunner: QueryRunner): Promise { + async down(_queryRunner: QueryRunner): Promise { // } } diff --git a/migration/1708567213261-addStreamDonationTimestamps.ts b/migration/1708567213261-addStreamDonationTimestamps.ts new file mode 100644 index 000000000..4acf138ac --- /dev/null +++ b/migration/1708567213261-addStreamDonationTimestamps.ts @@ -0,0 +1,44 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class addStreamDonationTimestamps1708567213261 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumns('donation', [ + new TableColumn({ + name: 'virtualPeriodStart', + type: 'integer', + isNullable: true, + }), + new TableColumn({ + name: 'virtualPeriodEnd', + type: 'integer', + isNullable: true, + }), + ]); + + // Add columns to "recurring_donation" table + await queryRunner.addColumns('recurring_donation', [ + new TableColumn({ + name: 'amountStreamed', + type: 'real', + default: 0, + }), + new TableColumn({ + name: 'totalUsdStreamed', + type: 'real', + default: 0, + }), + ]); + } + + public async down(queryRunner: QueryRunner): Promise { + // Remove columns from "donation" + await queryRunner.dropColumn('donation', 'virtualPeriodEnd'); + await queryRunner.dropColumn('donation', 'virtualPeriodStart'); + + // Remove columns from "recurring_donation" + await queryRunner.dropColumn('recurring_donation', 'totalUsdStreamed'); + await queryRunner.dropColumn('recurring_donation', 'amountStreamed'); + } +} diff --git a/migration/1708943908102-create_matching_fund_donations_correctly.ts b/migration/1708943908102-create_matching_fund_donations_correctly.ts new file mode 100644 index 000000000..e775bd0fd --- /dev/null +++ b/migration/1708943908102-create_matching_fund_donations_correctly.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateMatchingFundDonationsCorrectly1708943908102 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + // First delete all donations that are related to a qfRound, we will fill it again later correctly + await queryRunner.query(` + DELETE FROM donation + WHERE "distributedFundQfRoundId" IS NOT NULL; + `); + await queryRunner.query(` + ALTER TABLE qf_round_history + ADD COLUMN "distributedFundTxDate" DATE NULL; + `); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE qf_round_history + DROP COLUMN "distributedFundTxDate"; + `); + } +} diff --git a/migration/1687383705794-AddOptimismGoerliTokens.ts b/migration/1708954413087-addOptimismSepoliaTokens.ts similarity index 83% rename from migration/1687383705794-AddOptimismGoerliTokens.ts rename to migration/1708954413087-addOptimismSepoliaTokens.ts index 7f483151e..aa0ad931f 100644 --- a/migration/1687383705794-AddOptimismGoerliTokens.ts +++ b/migration/1708954413087-addOptimismSepoliaTokens.ts @@ -3,21 +3,20 @@ import { Token } from '../src/entities/token'; import seedTokens from './data/seedTokens'; import { NETWORK_IDS } from '../src/provider'; import config from '../src/config'; - -export class AddOptimismGoerliTokens1687383705794 +export class AddOptimismSepoliaTokens1708954413087 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const environment = config.get('ENVIRONMENT') as string; if (environment === 'production') { - // We dont add optimism-goerli tokens in production ENV + // We dont add optimism-sepolia tokens in production ENV return; } await queryRunner.manager.save( Token, seedTokens - .filter(token => token.networkId === NETWORK_IDS.OPTIMISM_GOERLI) + .filter(token => token.networkId === NETWORK_IDS.OPTIMISM_SEPOLIA) .map(token => { const t = { ...token, @@ -29,7 +28,7 @@ export class AddOptimismGoerliTokens1687383705794 ); const tokens = await queryRunner.query(` SELECT * FROM token - WHERE "networkId" = ${NETWORK_IDS.OPTIMISM_GOERLI} + WHERE "networkId" = ${NETWORK_IDS.OPTIMISM_SEPOLIA} `); const givethOrganization = ( await queryRunner.query(`SELECT * FROM organization @@ -51,13 +50,13 @@ export class AddOptimismGoerliTokens1687383705794 public async down(queryRunner: QueryRunner): Promise { const environment = config.get('ENVIRONMENT') as string; if (environment === 'production') { - // We dont add optimism-goerli tokens in production ENV + // We dont add optimism-sepolia tokens in production ENV return; } const tokens = await queryRunner.query(` SELECT * FROM token - WHERE "networkId" = ${NETWORK_IDS.OPTIMISM_GOERLI} + WHERE "networkId" = ${NETWORK_IDS.OPTIMISM_SEPOLIA} `); await queryRunner.query( `DELETE FROM organization_tokens_token WHERE "tokenId" IN (${tokens @@ -67,7 +66,7 @@ export class AddOptimismGoerliTokens1687383705794 await queryRunner.query( ` DELETE from token - WHERE "networkId" = ${NETWORK_IDS.OPTIMISM_GOERLI} + WHERE "networkId" = ${NETWORK_IDS.OPTIMISM_SEPOLIA} `, ); } diff --git a/migration/1709204568033-addCoingeckoId.ts b/migration/1709204568033-addCoingeckoId.ts index 5a0a9af15..beddf31ad 100644 --- a/migration/1709204568033-addCoingeckoId.ts +++ b/migration/1709204568033-addCoingeckoId.ts @@ -1553,7 +1553,7 @@ const stagingTokensData: TokenData[] = [ export class AddCoingeckoId1709204568033 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const environment = config.get('ENVIRONMENT') as string; - let tokenData = + const tokenData = environment === 'production' ? productionTokensData : stagingTokensData; const repository = queryRunner.manager.getRepository(Token); @@ -1574,7 +1574,7 @@ export class AddCoingeckoId1709204568033 implements MigrationInterface { ); } - public async down(queryRunner: QueryRunner): Promise { + public async down(_queryRunner: QueryRunner): Promise { // } } diff --git a/migration/1709242321053-CreateProjectSocialMediaTable.ts b/migration/1709242321053-CreateProjectSocialMediaTable.ts new file mode 100644 index 000000000..3c915ce48 --- /dev/null +++ b/migration/1709242321053-CreateProjectSocialMediaTable.ts @@ -0,0 +1,53 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class CreateProjectSocialMediaTable1709242321053 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'project_social_media', + columns: [ + { + name: 'id', + type: 'int', + isPrimary: true, + isGenerated: true, + generationStrategy: 'increment', + }, + { + name: 'type', + type: 'varchar', + }, + { + name: 'link', + type: 'varchar', + }, + { + name: 'projectId', + type: 'int', + }, + { + name: 'userId', + type: 'int', + }, + ], + foreignKeys: [ + { + columnNames: ['projectId'], + referencedTableName: 'project', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }, + ], + }), + true, + ); + } + + async down(queryRunner: QueryRunner): Promise { + if (await queryRunner.hasTable('project_social_media')) { + await queryRunner.query(`DROP TABLE "project_social_media"`); + } + } +} diff --git a/migration/1709457251478-modify_recurring_donation_table.ts b/migration/1709457251478-modify_recurring_donation_table.ts new file mode 100644 index 000000000..7043dc36e --- /dev/null +++ b/migration/1709457251478-modify_recurring_donation_table.ts @@ -0,0 +1,45 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ModifyRecurringDonationTable1709457251478 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + // Delete all existing records + await queryRunner.query(`DELETE FROM "recurring_donation";`); + + // Drop the 'amount' column if it exists + await queryRunner.query( + `ALTER TABLE "recurring_donation" DROP COLUMN IF EXISTS "amount";`, + ); + + // Drop the 'interval' column if it exists + await queryRunner.query( + `ALTER TABLE "recurring_donation" DROP COLUMN IF EXISTS "interval";`, + ); + + await queryRunner.query( + `ALTER TABLE "recurring_donation" ADD "flowRate" character varying NOT NULL`, + ); + } + + async down(queryRunner: QueryRunner): Promise { + // Remove the 'flowRate' column + await queryRunner.query( + `ALTER TABLE "recurring_donation" DROP COLUMN "flowRate";`, + ); + + // Assuming 'amount' was of type NUMERIC and 'interval' was of type INTEGER + // Add the 'amount' column back + await queryRunner.query( + `ALTER TABLE "recurring_donation" ADD "amount" NUMERIC NOT NULL;`, + ); + + // Add the 'interval' column back + await queryRunner.query( + `ALTER TABLE "recurring_donation" ADD "interval" INTEGER NOT NULL;`, + ); + + // Since data was deleted in the `up` method, consider ways to restore it if necessary. + // This might involve restoring from a backup or other data recovery strategies, which cannot be done through migration scripts. + } +} diff --git a/migration/1709468854359-project_actual_matching_v6.ts b/migration/1709468854359-project_actual_matching_v6.ts index 4e26db5f8..084a61bb7 100644 --- a/migration/1709468854359-project_actual_matching_v6.ts +++ b/migration/1709468854359-project_actual_matching_v6.ts @@ -89,7 +89,7 @@ export class ProjectActualMatchingV61709468854359 `); } - async down(queryRunner: QueryRunner): Promise { + async down(_queryRunner: QueryRunner): Promise { // } } diff --git a/migration/1709625907739-project_actual_matching_v8.ts b/migration/1709625907739-project_actual_matching_v8.ts index 95de0078f..1803f4b2c 100644 --- a/migration/1709625907739-project_actual_matching_v8.ts +++ b/migration/1709625907739-project_actual_matching_v8.ts @@ -156,7 +156,7 @@ export class ProjectActualMatchingV81709625907739 `); } - async down(queryRunner: QueryRunner): Promise { + async down(_queryRunner: QueryRunner): Promise { // Logic to revert the changes made by the up method, if necessary } } diff --git a/migration/1709740250342-project_actual_matching_v9.ts b/migration/1709740250342-project_actual_matching_v9.ts index d89984e40..15e7a33f8 100644 --- a/migration/1709740250342-project_actual_matching_v9.ts +++ b/migration/1709740250342-project_actual_matching_v9.ts @@ -159,5 +159,5 @@ export class projectActualMatchingV91709740250342 `); } - public async down(queryRunner: QueryRunner): Promise {} + public async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1709755736838-project_actual_matching_v10.ts b/migration/1709755736838-project_actual_matching_v10.ts index fd738c196..0505617f8 100644 --- a/migration/1709755736838-project_actual_matching_v10.ts +++ b/migration/1709755736838-project_actual_matching_v10.ts @@ -159,5 +159,5 @@ export class projectActualMatchingV101709755736838 `); } - public async down(queryRunner: QueryRunner): Promise {} + public async down(_queryRunner: QueryRunner): Promise {} } diff --git a/migration/1710146146539-set_default_value_for_finished_in_recurring_donation.ts b/migration/1710146146539-set_default_value_for_finished_in_recurring_donation.ts new file mode 100644 index 000000000..a840dd933 --- /dev/null +++ b/migration/1710146146539-set_default_value_for_finished_in_recurring_donation.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SetDefaultValueForFinishedInRecurringDonation1710146146539 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE recurring_donation ALTER COLUMN finished SET DEFAULT false;`, + ); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE recurring_donation ALTER COLUMN finished DROP DEFAULT;`, + ); + } +} diff --git a/migration/1710322367912-project_actual_matching_v11_.ts b/migration/1710322367912-project_actual_matching_v11_.ts new file mode 100644 index 000000000..186fb4ae0 --- /dev/null +++ b/migration/1710322367912-project_actual_matching_v11_.ts @@ -0,0 +1,169 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ProjectActualMatchingV11_1710322367912 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP MATERIALIZED VIEW IF EXISTS project_actual_matching_view; + CREATE MATERIALIZED VIEW project_actual_matching_view AS + + + WITH ProjectsAndRounds AS ( + SELECT + p.id AS "projectId", + p.slug, + p.title, + qr.id as "qfId", + qr."minimumPassportScore", + qr."eligibleNetworks", + STRING_AGG(DISTINCT CONCAT(pa."networkId", '-', pa."address"), ', ') AS "networkAddresses" + FROM + public.project p + CROSS JOIN public.qf_round qr + LEFT JOIN project_address pa ON pa."projectId" = p.id AND pa."networkId" = ANY(qr."eligibleNetworks") AND pa."isRecipient" = true + group by + p.id, + qr.id + ), + DonationsBeforeAnalysis AS ( + SELECT + par."projectId", + par.slug, + par.title, + par."qfId", + par."networkAddresses", + par."minimumPassportScore" as "minimumPassportScore", + COALESCE(SUM(d."valueUsd"), 0) AS "allUsdReceived", + COUNT(DISTINCT CASE WHEN d."fromWalletAddress" IS NOT NULL THEN d."fromWalletAddress" END) AS "totalDonors", + ARRAY_AGG(DISTINCT d.id) FILTER (WHERE d.id IS NOT NULL) AS "donationIdsBeforeAnalysis" + FROM + ProjectsAndRounds par + LEFT JOIN public.donation d ON d."projectId" = par."projectId" AND d."qfRoundId" = par."qfId" AND d."status" = 'verified' AND d."transactionNetworkId" = ANY(par."eligibleNetworks") + GROUP BY + par."projectId", + par.title, + par."networkAddresses", + par.slug, + par."qfId", + par."minimumPassportScore" + ), + UserProjectDonations AS ( + SELECT + par."projectId", + par."qfId" AS "qfRoundId", + d2."userId", + d2."fromWalletAddress", + d2."qfRoundUserScore", + COALESCE(SUM(d2."valueUsd"), 0) AS "totalValueUsd", + ARRAY_AGG(DISTINCT d2.id) FILTER (WHERE d2.id IS NOT NULL) AS "userDonationIds" + FROM + ProjectsAndRounds par + LEFT JOIN public.donation d2 ON d2."projectId" = par."projectId" AND d2."qfRoundId" = par."qfId" AND d2."status" = 'verified' AND d2."transactionNetworkId" = ANY(par."eligibleNetworks") + GROUP BY + par."projectId", + par."qfId", + d2."userId", + d2."fromWalletAddress", + d2."qfRoundUserScore" + ), + QualifiedUserDonations AS ( + SELECT + upd."userId", + upd."fromWalletAddress", + upd."projectId", + upd."qfRoundId", + upd."totalValueUsd", + upd."userDonationIds", + upd."qfRoundUserScore" + FROM + UserProjectDonations upd + WHERE + upd."totalValueUsd" >= (SELECT "minimumValidUsdValue" FROM public.qf_round WHERE id = upd."qfRoundId") + AND upd."qfRoundUserScore" >= (SELECT "minimumPassportScore" FROM public.qf_round WHERE id = upd."qfRoundId") + AND NOT EXISTS ( + SELECT 1 + FROM project_fraud pf + WHERE pf."projectId" = upd."projectId" + AND pf."qfRoundId" = upd."qfRoundId" + ) + AND NOT EXISTS ( + SELECT 1 + FROM sybil s + WHERE s."userId" = upd."userId" + AND s."qfRoundId" = upd."qfRoundId" + ) + AND NOT EXISTS ( + SELECT 1 + FROM project verified_project + JOIN project_address ON verified_project."id" = project_address."projectId" + WHERE verified_project.verified = true + AND lower(project_address."address") = lower(upd."fromWalletAddress") + ) + + ), + DonationIDsAggregated AS ( + SELECT + qud."projectId", + qud."qfRoundId", + ARRAY_AGG(DISTINCT unnested_ids) AS uniqueDonationIds + FROM + QualifiedUserDonations qud, + LATERAL UNNEST(qud."userDonationIds") AS unnested_ids + GROUP BY qud."projectId", qud."qfRoundId" + ), + DonationsAfterAnalysis AS ( + SELECT + da."projectId", + da.slug, + da.title, + da."qfId", + COALESCE(SUM(qud."totalValueUsd"), 0) AS "allUsdReceivedAfterSybilsAnalysis", + COUNT(DISTINCT qud."fromWalletAddress") AS "uniqueQualifiedDonors", + SUM(SQRT(qud."totalValueUsd")) AS "donationsSqrtRootSum", + POWER(SUM(SQRT(qud."totalValueUsd")), 2) as "donationsSqrtRootSumSquared", + dia.uniqueDonationIds AS "donationIdsAfterAnalysis", + ARRAY_AGG(DISTINCT qud."userId") AS "uniqueUserIdsAfterAnalysis", + ARRAY_AGG(qud."totalValueUsd") AS "totalValuesOfUserDonationsAfterAnalysis" + FROM + DonationsBeforeAnalysis da + LEFT JOIN QualifiedUserDonations qud ON da."projectId" = qud."projectId" AND da."qfId" = qud."qfRoundId" + LEFT JOIN DonationIDsAggregated dia ON da."projectId" = dia."projectId" AND da."qfId" = dia."qfRoundId" + GROUP BY + da."projectId", + da.slug, + da.title, + da."qfId", + dia."uniquedonationids", + da."networkAddresses" + ) + + SELECT + da."projectId", + da.title, + da.slug, + da."networkAddresses", + da."qfId" AS "qfRoundId", + da."donationIdsBeforeAnalysis", + da."allUsdReceived", + da."totalDonors", + daa."donationIdsAfterAnalysis", + daa."allUsdReceivedAfterSybilsAnalysis", + daa."uniqueQualifiedDonors", + daa."donationsSqrtRootSum", + daa."donationsSqrtRootSumSquared", + daa."uniqueUserIdsAfterAnalysis", + daa."totalValuesOfUserDonationsAfterAnalysis" + FROM + DonationsBeforeAnalysis da + INNER JOIN DonationsAfterAnalysis daa ON da."projectId" = daa."projectId" AND da."qfId" = daa."qfId"; + + CREATE INDEX idx_project_actual_matching_project_id ON project_actual_matching_view USING hash ("projectId"); + CREATE INDEX idx_project_actual_matching_qf_round_id ON project_actual_matching_view USING hash ("qfRoundId"); + `); + } + + async down(_queryRunner: QueryRunner): Promise { + // + } +} diff --git a/migration/1710322367912-project_actual_matching_v12_.ts b/migration/1710322367912-project_actual_matching_v12_.ts index 3c6f11d8b..56f89e525 100644 --- a/migration/1710322367912-project_actual_matching_v12_.ts +++ b/migration/1710322367912-project_actual_matching_v12_.ts @@ -163,7 +163,7 @@ export class ProjectActualMatchingV11_1710322367913 `); } - async down(queryRunner: QueryRunner): Promise { + async down(_queryRunner: QueryRunner): Promise { // } } diff --git a/migration/1710768644383-project_actual_matching_v13_.ts b/migration/1710768644383-project_actual_matching_v13_.ts index 5c16dbedf..b273b91ad 100644 --- a/migration/1710768644383-project_actual_matching_v13_.ts +++ b/migration/1710768644383-project_actual_matching_v13_.ts @@ -172,7 +172,7 @@ export class ProjectActualMatchingV13_1710768644383 `); } - async down(queryRunner: QueryRunner): Promise { + async down(_queryRunner: QueryRunner): Promise { // } } diff --git a/migration/1710977505681-slugUniqueIndex.ts b/migration/1710977505681-slugUniqueIndex.ts new file mode 100644 index 000000000..5f7fcc692 --- /dev/null +++ b/migration/1710977505681-slugUniqueIndex.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SlugUniqueIndex1710977505681 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_unique_slug" ON "project" ("slug")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_unique_slug"`); + } +} diff --git a/migration/1711613263251-add_isArchived_to_recurring_donation.ts b/migration/1711613263251-add_isArchived_to_recurring_donation.ts new file mode 100644 index 000000000..3b10f3090 --- /dev/null +++ b/migration/1711613263251-add_isArchived_to_recurring_donation.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddIsArchivedToRecurringDonation1711613263251 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE recurring_donation + ADD COLUMN IF NOT EXISTS "isArchived" BOOLEAN DEFAULT FALSE; + `); + + // update status of all archived donations to isArchived = true and status:ended + await queryRunner.query(` + UPDATE recurring_donation + SET "isArchived" = TRUE, status = 'ended' + WHERE status = 'archived'; + `); + } + + async down(queryRunner: QueryRunner): Promise { + // update status of all archived donations to isArchived = true and status:ended + await queryRunner.query(` + UPDATE recurring_donation + status = 'archived' + WHERE "isArchived" = TRUE; + `); + + await queryRunner.query(` + ALTER TABLE recurring_donation + DROP COLUMN IF EXISTS "isArchived"; + `); + } +} diff --git a/migration/1712044723561-add_isBatch_column_to_recurring_donation_table.ts b/migration/1712044723561-add_isBatch_column_to_recurring_donation_table.ts new file mode 100644 index 000000000..17d9aeef2 --- /dev/null +++ b/migration/1712044723561-add_isBatch_column_to_recurring_donation_table.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddIsBatchColumnToRecurringDonationTable1712044723561 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE recurring_donation + ADD COLUMN IF NOT EXISTS "isBatch" BOOLEAN DEFAULT FALSE; + `); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE recurring_donation + DROP COLUMN IF EXISTS "isBatch"; + `); + } +} diff --git a/migration/1712146623379-addStreamBalanceWarningToUser.ts b/migration/1712146623379-addStreamBalanceWarningToUser.ts new file mode 100644 index 000000000..0bc3a1bdd --- /dev/null +++ b/migration/1712146623379-addStreamBalanceWarningToUser.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddStreamBalanceWarningToUser1712146623379 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ADD "streamBalanceWarning" jsonb DEFAULT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" DROP COLUMN "streamBalanceWarning"`, + ); + } +} diff --git a/migration/1712205556308-ProjectActualMatchingV12.ts b/migration/1712205556308-ProjectActualMatchingV12.ts new file mode 100644 index 000000000..d5e226ab8 --- /dev/null +++ b/migration/1712205556308-ProjectActualMatchingV12.ts @@ -0,0 +1,174 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ProjectActualMatchingV121712205556308 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP MATERIALIZED VIEW IF EXISTS project_actual_matching_view; + CREATE MATERIALIZED VIEW project_actual_matching_view AS + + WITH ProjectsAndRounds AS ( + SELECT + p.id AS "projectId", + u.email, + p.slug, + p.title, + qr.id as "qfId", + qr."minimumPassportScore", + qr."eligibleNetworks", + STRING_AGG(DISTINCT CONCAT(pa."networkId", '-', pa."address"), ', ') AS "networkAddresses" + FROM + public.project p + CROSS JOIN public.qf_round qr + INNER JOIN public."user" u on p."adminUserId" = u.id + LEFT JOIN project_address pa ON pa."projectId" = p.id AND pa."networkId" = ANY(qr."eligibleNetworks") AND pa."isRecipient" = true + group by + p.id, + qr.id, + u.email + ), + DonationsBeforeAnalysis AS ( + SELECT + par."projectId", + par.slug, + par.email, + par.title, + par."qfId", + par."networkAddresses", + par."minimumPassportScore" as "minimumPassportScore", + COALESCE(SUM(d."valueUsd"), 0) AS "allUsdReceived", + COUNT(DISTINCT CASE WHEN d."fromWalletAddress" IS NOT NULL THEN d."fromWalletAddress" END) AS "totalDonors", + ARRAY_AGG(DISTINCT d.id) FILTER (WHERE d.id IS NOT NULL) AS "donationIdsBeforeAnalysis" + FROM + ProjectsAndRounds par + LEFT JOIN public.donation d ON d."projectId" = par."projectId" AND d."qfRoundId" = par."qfId" AND d."status" = 'verified' AND d."transactionNetworkId" = ANY(par."eligibleNetworks") + GROUP BY + par."projectId", + par.title, + par."networkAddresses", + par.slug, + par."qfId", + par."minimumPassportScore", + par.email + ), + UserProjectDonations AS ( + SELECT + par."projectId", + par."qfId" AS "qfRoundId", + d2."userId", + d2."fromWalletAddress", + d2."qfRoundUserScore", + COALESCE(SUM(d2."valueUsd"), 0) AS "totalValueUsd", + ARRAY_AGG(DISTINCT d2.id) FILTER (WHERE d2.id IS NOT NULL) AS "userDonationIds" + FROM + ProjectsAndRounds par + LEFT JOIN public.donation d2 ON d2."projectId" = par."projectId" AND d2."qfRoundId" = par."qfId" AND d2."status" = 'verified' AND d2."transactionNetworkId" = ANY(par."eligibleNetworks") + GROUP BY + par."projectId", + par."qfId", + d2."userId", + d2."fromWalletAddress", + d2."qfRoundUserScore" + ), + QualifiedUserDonations AS ( + SELECT + upd."userId", + upd."fromWalletAddress", + upd."projectId", + upd."qfRoundId", + upd."totalValueUsd", + upd."userDonationIds", + upd."qfRoundUserScore" + FROM + UserProjectDonations upd + WHERE + upd."totalValueUsd" >= (SELECT "minimumValidUsdValue" FROM public.qf_round WHERE id = upd."qfRoundId") + AND upd."qfRoundUserScore" >= (SELECT "minimumPassportScore" FROM public.qf_round WHERE id = upd."qfRoundId") + AND NOT EXISTS ( + SELECT 1 + FROM project_fraud pf + WHERE pf."projectId" = upd."projectId" + AND pf."qfRoundId" = upd."qfRoundId" + ) + AND NOT EXISTS ( + SELECT 1 + FROM sybil s + WHERE s."userId" = upd."userId" + AND s."qfRoundId" = upd."qfRoundId" + ) + AND NOT EXISTS ( + SELECT 1 + FROM project verified_project + JOIN project_address ON verified_project."id" = project_address."projectId" + WHERE verified_project.verified = true + AND lower(project_address."address") = lower(upd."fromWalletAddress") + ) + + ), + DonationIDsAggregated AS ( + SELECT + qud."projectId", + qud."qfRoundId", + ARRAY_AGG(DISTINCT unnested_ids) AS uniqueDonationIds + FROM + QualifiedUserDonations qud, + LATERAL UNNEST(qud."userDonationIds") AS unnested_ids + GROUP BY qud."projectId", qud."qfRoundId" + ), + DonationsAfterAnalysis AS ( + SELECT + da."projectId", + da.slug, + da.title, + da."qfId", + COALESCE(SUM(qud."totalValueUsd"), 0) AS "allUsdReceivedAfterSybilsAnalysis", + COUNT(DISTINCT qud."fromWalletAddress") AS "uniqueQualifiedDonors", + SUM(SQRT(qud."totalValueUsd")) AS "donationsSqrtRootSum", + POWER(SUM(SQRT(qud."totalValueUsd")), 2) as "donationsSqrtRootSumSquared", + dia.uniqueDonationIds AS "donationIdsAfterAnalysis", + ARRAY_AGG(DISTINCT qud."userId") AS "uniqueUserIdsAfterAnalysis", + ARRAY_AGG(qud."totalValueUsd") AS "totalValuesOfUserDonationsAfterAnalysis" + FROM + DonationsBeforeAnalysis da + LEFT JOIN QualifiedUserDonations qud ON da."projectId" = qud."projectId" AND da."qfId" = qud."qfRoundId" + LEFT JOIN DonationIDsAggregated dia ON da."projectId" = dia."projectId" AND da."qfId" = dia."qfRoundId" + GROUP BY + da."projectId", + da.slug, + da.title, + da."qfId", + dia."uniquedonationids", + da."networkAddresses" + ) + + SELECT + da."projectId", + da.email, + da.title, + da.slug, + da."networkAddresses", + da."qfId" AS "qfRoundId", + da."donationIdsBeforeAnalysis", + da."allUsdReceived", + da."totalDonors", + daa."donationIdsAfterAnalysis", + daa."allUsdReceivedAfterSybilsAnalysis", + daa."uniqueQualifiedDonors", + daa."donationsSqrtRootSum", + daa."donationsSqrtRootSumSquared", + daa."uniqueUserIdsAfterAnalysis", + daa."totalValuesOfUserDonationsAfterAnalysis" + FROM + DonationsBeforeAnalysis da + INNER JOIN DonationsAfterAnalysis daa ON da."projectId" = daa."projectId" AND da."qfId" = daa."qfId"; + + CREATE INDEX idx_project_actual_matching_project_id ON project_actual_matching_view USING hash ("projectId"); + CREATE INDEX idx_project_actual_matching_qf_round_id ON project_actual_matching_view USING hash ("qfRoundId"); + `); + } + + public async down(_queryRunner: QueryRunner): Promise { + // + } +} diff --git a/migration/1712735731871-ProjectDonationSummeryViewV2.ts b/migration/1712735731871-ProjectDonationSummeryViewV2.ts new file mode 100644 index 000000000..dcfb84484 --- /dev/null +++ b/migration/1712735731871-ProjectDonationSummeryViewV2.ts @@ -0,0 +1,58 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ProjectDonationSummeryViewV21712735731871 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + DROP MATERIALIZED VIEW IF EXISTS project_donation_summary_view; + + CREATE MATERIALIZED VIEW project_donation_summary_view AS + WITH unique_donors AS ( + SELECT + d."projectId", + d."userId" + FROM + "donation" d + WHERE + d."status" = 'verified' + AND d."recurringDonationId" IS NULL + AND d."valueUsd" > 0 + + UNION + + SELECT + rd."projectId", + rd."donorId" AS "userId" + FROM + "recurring_donation" rd + WHERE + rd."status" = 'active' + ) + SELECT + d."projectId", + SUM(CASE WHEN d."status" = 'verified' AND d."valueUsd" > 0 THEN d."valueUsd" ELSE 0 END) AS "sumVerifiedDonations", + COUNT(DISTINCT u."userId") AS "uniqueDonorsCount" + FROM + "donation" d + JOIN + unique_donors u ON d."projectId" = u."projectId" + GROUP BY + d."projectId"; + + CREATE INDEX idx_project_donation_summary_project_id ON project_donation_summary_view USING hash ("projectId"); + + + `, + ); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + DROP MATERIALIZED VIEW project_donation_summary_view; + `, + ); + } +} diff --git a/migration/1712745858472-ProjectDonationSummeryViewV3.ts b/migration/1712745858472-ProjectDonationSummeryViewV3.ts new file mode 100644 index 000000000..dd28808e2 --- /dev/null +++ b/migration/1712745858472-ProjectDonationSummeryViewV3.ts @@ -0,0 +1,56 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ProjectDonationSummeryViewV31712745858472 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + DROP MATERIALIZED VIEW IF EXISTS project_donation_summary_view; + + CREATE MATERIALIZED VIEW project_donation_summary_view AS + WITH unique_donors AS ( + SELECT + "projectId", + "userId" + FROM + "donation" + WHERE + "status" = 'verified' + AND "recurringDonationId" IS NULL + AND "valueUsd" > 0 + + UNION + + SELECT + "projectId", + "donorId" AS "userId" + FROM + "recurring_donation" + WHERE + "status" = 'active' + ) + SELECT + d."projectId", + (SELECT "totalDonations" FROM "project" WHERE "id" = d."projectId") AS "sumVerifiedDonations", + COUNT(DISTINCT u."userId") AS "uniqueDonorsCount" + FROM + unique_donors u + JOIN + "donation" d ON u."projectId" = d."projectId" + GROUP BY + d."projectId"; + + CREATE INDEX idx_project_donation_summary_project_id ON project_donation_summary_view USING hash ("projectId"); + `, + ); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + DROP MATERIALIZED VIEW project_donation_summary_view; + `, + ); + } +} diff --git a/migration/1712853017092-UserNewRoleQfManager.ts b/migration/1712853017092-UserNewRoleQfManager.ts new file mode 100644 index 000000000..3da4baa5f --- /dev/null +++ b/migration/1712853017092-UserNewRoleQfManager.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +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'; + `, + ); + } + + 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'; + `, + ); + } +} diff --git a/migration/1713185131862-add_origin_to_recurring_donation.ts b/migration/1713185131862-add_origin_to_recurring_donation.ts new file mode 100644 index 000000000..2bd70fb11 --- /dev/null +++ b/migration/1713185131862-add_origin_to_recurring_donation.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddOriginToRecurringDonation1713185131862 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE recurring_donation + ADD COLUMN IF NOT EXISTS "origin" text; + `); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE recurring_donation + DROP COLUMN IF EXISTS "origin"; + `); + } +} diff --git a/migration/1713185610773-create_draft_recurring_donation_table.ts b/migration/1713185610773-create_draft_recurring_donation_table.ts new file mode 100644 index 000000000..8ad7dec53 --- /dev/null +++ b/migration/1713185610773-create_draft_recurring_donation_table.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateDraftRecurringDonationTable1713185610773 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE draft_recurring_donation ( + id SERIAL PRIMARY KEY, + "networkId" INT NOT NULL, + "flowRate" TEXT NOT NULL, + "chainType" VARCHAR(255) DEFAULT 'EVM' NOT NULL, + "currency" VARCHAR(255) NOT NULL, + "isBatch" BOOLEAN DEFAULT false, + "anonymous" BOOLEAN DEFAULT false, + "isForUpdate" BOOLEAN DEFAULT false, + "projectId" INT, + "donorId" INT, + "status" VARCHAR(255) DEFAULT 'pending' NOT NULL, + "matchedRecurringDonationId" INT, + "origin" TEXT, + "errorMessage" TEXT, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + ); +`); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS draft_recurring_donation;`); + } +} diff --git a/migration/1713238270135-AddQfRoundTitleAndDescription.ts b/migration/1713238270135-AddQfRoundTitleAndDescription.ts new file mode 100644 index 000000000..3656aa1ad --- /dev/null +++ b/migration/1713238270135-AddQfRoundTitleAndDescription.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddQfRoundTitleAndDescription1713238270135 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE IF EXISTS "qf_round" + ADD COLUMN IF NOT EXISTS "title" text, + ADD COLUMN IF NOT EXISTS "description" text; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "qf_round" + DROP COLUMN "title", + DROP COLUMN "description"; + `); + } +} diff --git a/migration/1713441337834-ProjectDonationSummeryV4.ts b/migration/1713441337834-ProjectDonationSummeryV4.ts new file mode 100644 index 000000000..3366bb17a --- /dev/null +++ b/migration/1713441337834-ProjectDonationSummeryV4.ts @@ -0,0 +1,56 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ProjectDonationSummeryV41713441337834 + implements MigrationInterface +{ + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + DROP MATERIALIZED VIEW IF EXISTS project_donation_summary_view; + + CREATE MATERIALIZED VIEW project_donation_summary_view AS + WITH unique_donors AS ( + SELECT + "projectId", + "userId" + FROM + "donation" + WHERE + "status" = 'verified' + AND "recurringDonationId" IS NULL + AND "valueUsd" > 0 + + UNION + + SELECT + "projectId", + "donorId" AS "userId" + FROM + "recurring_donation" + WHERE + "status" IN ('active', 'ended') + ) + SELECT + d."projectId", + (SELECT "totalDonations" FROM "project" WHERE "id" = d."projectId") AS "sumVerifiedDonations", + COUNT(DISTINCT u."userId") AS "uniqueDonorsCount" + FROM + unique_donors u + JOIN + "donation" d ON u."projectId" = d."projectId" + GROUP BY + d."projectId"; + + CREATE INDEX idx_project_donation_summary_project_id ON project_donation_summary_view USING hash ("projectId"); + `, + ); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + DROP MATERIALIZED VIEW project_donation_summary_view; + `, + ); + } +} diff --git a/migration/1713545913826-project_actual_matching_v14_.ts b/migration/1713545913826-project_actual_matching_v14_.ts index 6c9a95093..c50b8f5aa 100644 --- a/migration/1713545913826-project_actual_matching_v14_.ts +++ b/migration/1713545913826-project_actual_matching_v14_.ts @@ -172,5 +172,5 @@ export class projectActualMatchingV14_1713545913826 `); } - 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 3b5a620d8..be4fe6dec 100644 --- a/migration/data/seedTokens.ts +++ b/migration/data/seedTokens.ts @@ -1052,38 +1052,22 @@ const seedTokens: ITokenData[] = [ networkId: NETWORK_IDS.POLYGON, }, - // OPTIMISM Goerli tokens + // OPTIMISM Sepolia tokens { - name: 'OPTIMISM Goerli native token', + name: 'OPTIMISM Sepolia native token', symbol: 'ETH', address: '0x0000000000000000000000000000000000000000', decimals: 18, - networkId: NETWORK_IDS.OPTIMISM_GOERLI, + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, coingeckoId: 'ethereum', + isStableCoin: true, }, { - name: 'OPTIMISM Goerli OP token', - symbol: 'OP', - address: '0x4200000000000000000000000000000000000042', - decimals: 18, - networkId: NETWORK_IDS.OPTIMISM_GOERLI, - coingeckoId: 'optimism', - }, - { - name: 'Wrapped Ether', - symbol: 'WETH', - address: '0x4200000000000000000000000000000000000006', - decimals: 18, - networkId: NETWORK_IDS.OPTIMISM_GOERLI, - coingeckoId: 'weth', - }, - { - name: 'Dai', - symbol: 'DAI', - address: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', + name: 'Optimism Sepolia GIV test token', + symbol: 'GIV', + address: '0x2f2c819210191750F2E11F7CfC5664a0eB4fd5e6', decimals: 18, - networkId: NETWORK_IDS.OPTIMISM_GOERLI, - coingeckoId: 'dai', + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, }, // OPTIMISTIC tokens diff --git a/migration/tslint.json b/migration/tslint.json index 8fc2357bb..e66437238 100644 --- a/migration/tslint.json +++ b/migration/tslint.json @@ -1,5 +1,5 @@ { - "extends": "../tslint.json", + "extends": "../.eslintrc", "rules": { "no-empty": false } diff --git a/package-lock.json b/package-lock.json index 7ed91ac4b..46cb7f720 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "@sentry/node": "6.16.1", "@sentry/tracing": "^6.2.0", "@solana/web3.js": "^1.87.6", - "@types/bcryptjs": "^2.4.2", "@types/connect-redis": "0.0.23", "@types/cors": "^2.8.10", "@types/dotenv": "^8.2.0", @@ -31,14 +30,12 @@ "@types/jsonwebtoken": "^8.5.0", "@types/marked": "^4.0.8", "@types/node": "^14.14.31", - "@types/uuid": "^7.0.4", "@uniswap/sdk": "^3.0.3", "abi-decoder": "^2.4.0", "adminjs": "6.8.3", "axios": "^1.6.7", "axios-retry": "^3.9.1", "bcrypt": "^5.0.1", - "bcryptjs": "^2.4.3", "body-parser": "^1.19.0", "bull": "^4.12.2", "bunyan": "^1.8.15", @@ -50,7 +47,6 @@ "cors": "^2.8.5", "csvtojson": "^2.0.10", "dotenv": "^8.2.0", - "eth-sig-util": "^3.0.1", "ethers": "^5.7.2", "express": "^4.18.2", "express-formidable": "^1.2.0", @@ -75,7 +71,6 @@ "marked": "^4.2.5", "moment": "^2.29.4", "node-cron": "^3.0.2", - "nodemailer": "^6.5.0", "patch-package": "^6.5.1", "rate-limit-redis": "^4.2.0", "reflect-metadata": "^0.1.13", @@ -97,22 +92,35 @@ "@types/lodash": "^4.14.197", "@types/mocha": "^8.2.1", "@types/node-cron": "^3.0.0", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", "chai": "^4.3.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-unused-imports": "^3.1.0", "husky": "^4.3.8", "lint-staged": "^10.5.4", "mocha": "^10.2.0", - "prettier": "^2.4.1", + "prettier": "^3.2.5", "sinon": "^13.0.1", "ts-node": "10.9.2", - "ts-node-dev": "2.0.0", - "tslint": "^6.1.3", - "tslint-config-prettier": "^1.18.0", - "tslint-plugin-prettier": "^2.3.0" + "ts-node-dev": "2.0.0" }, "engines": { "node": ">=16.14.2" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@adminjs/design-system": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/@adminjs/design-system/-/design-system-3.1.5.tgz", @@ -2528,6 +2536,111 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@ethereumjs/common": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.6.5.tgz", @@ -3434,6 +3547,61 @@ "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, "node_modules/@hypnosphi/create-react-context": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz", @@ -3894,6 +4062,41 @@ ], "peer": true }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@nomicfoundation/ethereumjs-block": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-4.0.0.tgz", @@ -4184,6 +4387,18 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@popperjs/core": { "version": "2.11.6", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", @@ -4410,12 +4625,12 @@ "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" }, "node_modules/@safe-global/api-kit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@safe-global/api-kit/-/api-kit-2.0.0.tgz", - "integrity": "sha512-Tz6pLEmhhv/ROsYSjVzoR8qw4YK72yNPJCFcK97kSvNJQpM2+HpRVYNjB53rY0IkvP0kVFvF6Ogp/BJri8g1Pw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@safe-global/api-kit/-/api-kit-2.2.0.tgz", + "integrity": "sha512-y9EetRZXIFs5HmIk1blmL38Rbzxlt79cEuYGDEdmQJNa6SQ7OJdO4Eoy2hMFleZIhTKhoOWVsyEfdZnRPtsq2g==", "dependencies": { - "@safe-global/protocol-kit": "^2.0.0", - "@safe-global/safe-core-sdk-types": "^3.0.0", + "@safe-global/protocol-kit": "^3.0.1", + "@safe-global/safe-core-sdk-types": "^4.0.1", "ethers": "^6.7.1", "node-fetch": "^2.7.0" } @@ -4524,12 +4739,12 @@ } }, "node_modules/@safe-global/protocol-kit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@safe-global/protocol-kit/-/protocol-kit-2.0.0.tgz", - "integrity": "sha512-alnSxNZKC1ssKrFG5ytluu9kNKGwBifb1xhOyCqwMnm72JksbCEo0UWlNvaeCiYMwhYvMyS++mfxcLAsV/8Gfw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@safe-global/protocol-kit/-/protocol-kit-3.0.1.tgz", + "integrity": "sha512-7S2QCvIDw3NsErF0f8tIfiTBz32btCAkw7IYuQFPc+G7clLrvDNhDaZYSoDsa8F0EoEhn+605VA7XP//iL6AIg==", "dependencies": { - "@noble/hashes": "^1.3.2", - "@safe-global/safe-deployments": "^1.28.0", + "@noble/hashes": "^1.3.3", + "@safe-global/safe-deployments": "^1.33.0", "ethereumjs-util": "^7.1.5", "ethers": "^6.7.1", "semver": "^7.5.4", @@ -4538,6 +4753,11 @@ "web3-utils": "^1.10.3" } }, + "node_modules/@safe-global/protocol-kit/node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" + }, "node_modules/@safe-global/protocol-kit/node_modules/@noble/curves": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", @@ -4597,9 +4817,9 @@ } }, "node_modules/@safe-global/protocol-kit/node_modules/ethers": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.9.0.tgz", - "integrity": "sha512-pmfNyQzc2mseLe91FnT2vmNaTt8dDzhxZ/xItAV7uGsF4dI4ek2ufMu3rAkgQETL/TIs0GS5A+U05g9QyWnv3Q==", + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.11.1.tgz", + "integrity": "sha512-mxTAE6wqJQAbp5QAe/+o+rXOID7Nw91OZXvgpjDa1r4fAbq2Nu314oEZSbjoRLacuCzs7kUC3clEvkCQowffGg==", "funding": [ { "type": "individual", @@ -4611,7 +4831,7 @@ } ], "dependencies": { - "@adraffy/ens-normalize": "1.10.0", + "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", "@types/node": "18.15.13", @@ -4646,9 +4866,9 @@ } }, "node_modules/@safe-global/protocol-kit/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -4690,16 +4910,21 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/@safe-global/safe-core-sdk-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@safe-global/safe-core-sdk-types/-/safe-core-sdk-types-3.0.1.tgz", - "integrity": "sha512-2AdlK6GJ5YEZXrQwFsHFwQScnNo3OonF3O6KzVeMc0/7OAuOTYBzKq1jzju2Eck6Z8UNPUinlHoF2Zb2pvTKhw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@safe-global/safe-core-sdk-types/-/safe-core-sdk-types-4.0.1.tgz", + "integrity": "sha512-cXW6petRWqUw1n04ZhVPgjzIL65FkAMqbPwkFAAlQ1lBxTt6xdxktLoAhgEDlqLNGibvncsNvKhxa1ib4T9MGg==", "dependencies": { - "@safe-global/safe-deployments": "^1.28.0", + "@safe-global/safe-deployments": "^1.33.0", "ethers": "^6.7.1", "web3-core": "^1.10.3", "web3-utils": "^1.10.3" } }, + "node_modules/@safe-global/safe-core-sdk-types/node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" + }, "node_modules/@safe-global/safe-core-sdk-types/node_modules/@noble/curves": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", @@ -4733,9 +4958,9 @@ "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" }, "node_modules/@safe-global/safe-core-sdk-types/node_modules/ethers": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.9.0.tgz", - "integrity": "sha512-pmfNyQzc2mseLe91FnT2vmNaTt8dDzhxZ/xItAV7uGsF4dI4ek2ufMu3rAkgQETL/TIs0GS5A+U05g9QyWnv3Q==", + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.11.1.tgz", + "integrity": "sha512-mxTAE6wqJQAbp5QAe/+o+rXOID7Nw91OZXvgpjDa1r4fAbq2Nu314oEZSbjoRLacuCzs7kUC3clEvkCQowffGg==", "funding": [ { "type": "individual", @@ -4747,7 +4972,7 @@ } ], "dependencies": { - "@adraffy/ens-normalize": "1.10.0", + "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", "@types/node": "18.15.13", @@ -4785,9 +5010,9 @@ } }, "node_modules/@safe-global/safe-deployments": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@safe-global/safe-deployments/-/safe-deployments-1.29.0.tgz", - "integrity": "sha512-rXTktZblfklQyPe2JLK7GtXW/jH8htE6oP9MQHpVU5K/98OLkR4ApLAzlJscQEcyCaK+XOQunOk/gQYK1M2IpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/@safe-global/safe-deployments/-/safe-deployments-1.33.0.tgz", + "integrity": "sha512-G9qGMsha6idMnDuk98dE//inQL09w97hcQ5ZTdSWIHCzJ9mFdN0K4DH2afjZOwdt+Y4g8gZmY3z+kR38MPEToQ==", "dependencies": { "semver": "^7.3.7" } @@ -4804,9 +5029,9 @@ } }, "node_modules/@safe-global/safe-deployments/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -5999,11 +6224,6 @@ "@types/babel-types": "*" } }, - "node_modules/@types/bcryptjs": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", - "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==" - }, "node_modules/@types/bn.js": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz", @@ -6204,6 +6424,18 @@ "ioredis": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, "node_modules/@types/jsonwebtoken": { "version": "8.5.9", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz", @@ -6405,9 +6637,9 @@ } }, "node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==" + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==" }, "node_modules/@types/send": { "version": "0.17.4", @@ -6454,11 +6686,6 @@ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, - "node_modules/@types/uuid": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.5.tgz", - "integrity": "sha512-hKB88y3YHL8oPOs/CNlaXtjWn93+Bs48sDQR37ZUqG2tLeCS7EA1cmnkKsuQsub9OKEB/y/Rw9zqJqqNSbqVlQ==" - }, "node_modules/@types/validator": { "version": "13.7.11", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.11.tgz", @@ -6481,86 +6708,396 @@ "@types/node": "*" } }, - "node_modules/@uniswap/lib": { - "version": "4.0.1-alpha", - "resolved": "https://registry.npmjs.org/@uniswap/lib/-/lib-4.0.1-alpha.tgz", - "integrity": "sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/@uniswap/sdk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@uniswap/sdk/-/sdk-3.0.3.tgz", - "integrity": "sha512-t4s8bvzaCFSiqD2qfXIm3rWhbdnXp+QjD3/mRaeVDHK7zWevs6RGEb1ohMiNgOCTZANvBayb4j8p+XFdnMBadQ==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz", + "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==", + "dev": true, "dependencies": { - "@uniswap/v2-core": "^1.0.0", - "big.js": "^5.2.2", - "decimal.js-light": "^2.5.0", - "jsbi": "^3.1.1", - "tiny-invariant": "^1.1.0", - "tiny-warning": "^1.0.3", - "toformat": "^2.0.0" + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/type-utils": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": ">=10" + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@ethersproject/address": "^5.0.0-beta", - "@ethersproject/contracts": "^5.0.0-beta", - "@ethersproject/networks": "^5.0.0-beta", - "@ethersproject/providers": "^5.0.0-beta", - "@ethersproject/solidity": "^5.0.0-beta" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@uniswap/sdk-core": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@uniswap/sdk-core/-/sdk-core-3.2.1.tgz", - "integrity": "sha512-HhMRLdcQlUTU0wP2r2H+kpKhiut5qBuuFVrp51kfe2wbdyUtT+/kGDeA4RJRPJBoDjQdrU4g0B7Obu0X3WJaoQ==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { - "@ethersproject/address": "^5.0.2", - "big.js": "^5.2.2", - "decimal.js-light": "^2.5.0", - "jsbi": "^3.1.4", - "tiny-invariant": "^1.1.0", - "toformat": "^2.0.0" + "yallist": "^4.0.0" }, "engines": { "node": ">=10" } }, - "node_modules/@uniswap/swap-router-contracts": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@uniswap/swap-router-contracts/-/swap-router-contracts-1.3.0.tgz", - "integrity": "sha512-iKvCuRkHXEe0EMjOf8HFUISTIhlxI57kKFllf3C3PUIE0HmwxrayyoflwAz5u/TRsFGYqJ9IjX2UgzLCsrNa5A==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, "dependencies": { - "@openzeppelin/contracts": "3.4.2-solc-0.7", - "@uniswap/v2-core": "1.0.1", - "@uniswap/v3-core": "1.0.0", - "@uniswap/v3-periphery": "1.4.1", - "dotenv": "^14.2.0", - "hardhat-watcher": "^2.1.1" + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, - "node_modules/@uniswap/swap-router-contracts/node_modules/@uniswap/v3-periphery": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@uniswap/v3-periphery/-/v3-periphery-1.4.1.tgz", - "integrity": "sha512-Ab0ZCKOQrQMKIcpBTezTsEhWfQjItd0TtkCG8mPhoQu+wC67nPaf4hYUhM6wGHeFUmDiYY5MpEQuokB0ENvoTg==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", + "dev": true, "dependencies": { - "@openzeppelin/contracts": "3.4.2-solc-0.7", - "@uniswap/lib": "^4.0.1-alpha", - "@uniswap/v2-core": "1.0.1", - "@uniswap/v3-core": "1.0.0", - "base64-sol": "1.0.1", - "hardhat-watcher": "^2.1.1" + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4" }, "engines": { - "node": ">=10" - } - }, + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz", + "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", + "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@uniswap/lib": { + "version": "4.0.1-alpha", + "resolved": "https://registry.npmjs.org/@uniswap/lib/-/lib-4.0.1-alpha.tgz", + "integrity": "sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/sdk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@uniswap/sdk/-/sdk-3.0.3.tgz", + "integrity": "sha512-t4s8bvzaCFSiqD2qfXIm3rWhbdnXp+QjD3/mRaeVDHK7zWevs6RGEb1ohMiNgOCTZANvBayb4j8p+XFdnMBadQ==", + "dependencies": { + "@uniswap/v2-core": "^1.0.0", + "big.js": "^5.2.2", + "decimal.js-light": "^2.5.0", + "jsbi": "^3.1.1", + "tiny-invariant": "^1.1.0", + "tiny-warning": "^1.0.3", + "toformat": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@ethersproject/address": "^5.0.0-beta", + "@ethersproject/contracts": "^5.0.0-beta", + "@ethersproject/networks": "^5.0.0-beta", + "@ethersproject/providers": "^5.0.0-beta", + "@ethersproject/solidity": "^5.0.0-beta" + } + }, + "node_modules/@uniswap/sdk-core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@uniswap/sdk-core/-/sdk-core-3.2.1.tgz", + "integrity": "sha512-HhMRLdcQlUTU0wP2r2H+kpKhiut5qBuuFVrp51kfe2wbdyUtT+/kGDeA4RJRPJBoDjQdrU4g0B7Obu0X3WJaoQ==", + "dependencies": { + "@ethersproject/address": "^5.0.2", + "big.js": "^5.2.2", + "decimal.js-light": "^2.5.0", + "jsbi": "^3.1.4", + "tiny-invariant": "^1.1.0", + "toformat": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/swap-router-contracts": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@uniswap/swap-router-contracts/-/swap-router-contracts-1.3.0.tgz", + "integrity": "sha512-iKvCuRkHXEe0EMjOf8HFUISTIhlxI57kKFllf3C3PUIE0HmwxrayyoflwAz5u/TRsFGYqJ9IjX2UgzLCsrNa5A==", + "dependencies": { + "@openzeppelin/contracts": "3.4.2-solc-0.7", + "@uniswap/v2-core": "1.0.1", + "@uniswap/v3-core": "1.0.0", + "@uniswap/v3-periphery": "1.4.1", + "dotenv": "^14.2.0", + "hardhat-watcher": "^2.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/swap-router-contracts/node_modules/@uniswap/v3-periphery": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@uniswap/v3-periphery/-/v3-periphery-1.4.1.tgz", + "integrity": "sha512-Ab0ZCKOQrQMKIcpBTezTsEhWfQjItd0TtkCG8mPhoQu+wC67nPaf4hYUhM6wGHeFUmDiYY5MpEQuokB0ENvoTg==", + "dependencies": { + "@openzeppelin/contracts": "3.4.2-solc-0.7", + "@uniswap/lib": "^4.0.1-alpha", + "@uniswap/v2-core": "1.0.1", + "@uniswap/v3-core": "1.0.0", + "base64-sol": "1.0.1", + "hardhat-watcher": "^2.1.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@uniswap/swap-router-contracts/node_modules/dotenv": { "version": "14.3.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-14.3.2.tgz", @@ -6737,6 +7274,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", @@ -7057,12 +7603,24 @@ "devOptional": true }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "sprintf-js": "~1.0.2" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array-flatten": { @@ -7070,9 +7628,133 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, - "node_modules/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "node_modules/array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.filter": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz", + "integrity": "sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz", + "integrity": "sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "engines": { "node": ">=8" @@ -7178,9 +7860,12 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -7370,11 +8055,6 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, - "node_modules/bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" - }, "node_modules/bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", @@ -7889,12 +8569,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8844,6 +9530,39 @@ "node": ">=10" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/degenerator": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.4.tgz", @@ -8937,6 +9656,30 @@ "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -9117,14 +9860,135 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-abstract": { + "version": "1.22.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz", + "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.1", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.0", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.5", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "hasInstallScript": true, "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { @@ -9176,52 +10040,432 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.1.0.tgz", + "integrity": "sha512-9l1YFCzXKkw1qtAru1RWUtG2EVDZY0a0eChKXcL+EZ5jitG7qxdctu4RnvhOJHv4xfmUf7h+JJPINlVpGhZMrw==", + "dev": true, + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "6 - 7", + "eslint": "8" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "node_modules/eslint/node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=4.0" + "node": "*" + } + }, + "node_modules/eslint/node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" }, - "optionalDependencies": { - "source-map": "~0.6.1" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/eslint-plugin-prettier": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.7.0.tgz", - "integrity": "sha512-CStQYJgALoQBw3FsBzH0VOVDRnJ/ZimUlpLm226U8qgqYJfPOY/CPK6wyRInMxh73HSKg5wyRwdS4BVYYHwokA==", + "node_modules/eslint/node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "dependencies": { - "fast-diff": "^1.1.1", - "jest-docblock": "^21.0.0" + "prelude-ls": "^1.2.1" }, "engines": { - "node": ">=4.0.0" + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" }, - "peerDependencies": { - "prettier": ">= 0.11.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/esm": { @@ -9233,6 +10477,42 @@ "node": ">=6" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esniff/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -9245,6 +10525,48 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", @@ -9321,18 +10643,6 @@ "ultron": "~1.1.0" } }, - "node_modules/eth-sig-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-3.0.1.tgz", - "integrity": "sha512-0Us50HiGGvZgjtWTyAI/+qTzYPMLy5Q451D0Xy68bxq1QMWdoOddDwGvsqcFT27uohKgalM9z/yxplyt+mY2iQ==", - "deprecated": "Deprecated in favor of '@metamask/eth-sig-util'", - "dependencies": { - "ethereumjs-abi": "^0.6.8", - "ethereumjs-util": "^5.1.1", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.0" - } - }, "node_modules/ethereum-bloom-filters": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.10.tgz", @@ -9367,6 +10677,7 @@ "version": "0.6.8", "resolved": "https://registry.npmjs.org/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz", "integrity": "sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA==", + "peer": true, "dependencies": { "bn.js": "^4.11.8", "ethereumjs-util": "^6.0.0" @@ -9376,6 +10687,7 @@ "version": "4.11.6", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "peer": true, "dependencies": { "@types/node": "*" } @@ -9383,12 +10695,14 @@ "node_modules/ethereumjs-abi/node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "peer": true }, "node_modules/ethereumjs-abi/node_modules/ethereumjs-util": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz", "integrity": "sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==", + "peer": true, "dependencies": { "@types/bn.js": "^4.11.3", "bn.js": "^4.11.0", @@ -9399,25 +10713,6 @@ "rlp": "^2.2.3" } }, - "node_modules/ethereumjs-util": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", - "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", - "dependencies": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/ethereumjs-util/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, "node_modules/ethers": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", @@ -9487,6 +10782,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", + "peer": true, "dependencies": { "is-hex-prefixed": "1.0.0", "strip-hex-prefix": "1.0.0" @@ -9496,6 +10792,15 @@ "npm": ">=3" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -9742,11 +11047,27 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -9783,6 +11104,15 @@ "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -9791,6 +11121,18 @@ "pend": "~1.2.0" } }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/file-uri-to-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", @@ -9923,11 +11265,31 @@ "flat": "cli.js" } }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/flat-util": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/flat-util/-/flat-util-1.1.9.tgz", "integrity": "sha512-BOTMw/6rbbxVjv5JQvwgGMc2/6wWGd2VeyTvnzvvE49VRjS0tTxLbry/QVP1yPw8SaAOBYsnixmzruXoqjdUHA==" }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, "node_modules/follow-redirects": { "version": "1.15.5", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", @@ -10138,9 +11500,30 @@ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/functional-red-black-tree": { "version": "1.0.1", @@ -10148,6 +11531,15 @@ "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "peer": true }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gauge": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", @@ -10220,13 +11612,18 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10260,6 +11657,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-uri": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", @@ -10351,6 +11765,41 @@ "node": ">=4" } }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/google-auth-library": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.6.tgz", @@ -10463,6 +11912,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/graphql": { "version": "16.8.1", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", @@ -11035,15 +12490,13 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "peer": true }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-flag": { @@ -11051,7 +12504,29 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-symbols": { @@ -11066,11 +12541,11 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -11106,6 +12581,17 @@ "minimalistic-assert": "^1.0.1" } }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -11528,6 +13014,15 @@ } ] }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/immutable": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz", @@ -11549,6 +13044,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -11588,6 +13092,20 @@ "node": ">=8" } }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/io-ts": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-1.10.4.tgz", @@ -11621,9 +13139,9 @@ } }, "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" }, "node_modules/ip-regex": { "version": "4.3.0", @@ -11656,11 +13174,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -11672,6 +13218,22 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", @@ -11732,11 +13294,26 @@ } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "dependencies": { - "has": "^1.0.3" + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11859,6 +13436,18 @@ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -11867,6 +13456,21 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", @@ -11876,6 +13480,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -11903,6 +13516,22 @@ "@types/estree": "*" } }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", @@ -11923,6 +13552,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -11934,15 +13578,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", "has-tostringtag": "^1.0.0" }, "engines": { @@ -11952,6 +13593,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -11973,6 +13643,18 @@ "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==" }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -12072,12 +13754,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, - "node_modules/jest-docblock": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", - "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", - "dev": true - }, "node_modules/jest-worker": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", @@ -12138,13 +13814,11 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -12210,6 +13884,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -13004,11 +14684,6 @@ "markdown-it": "bin/markdown-it.js" } }, - "node_modules/markdown-it/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, "node_modules/markdown-it/node_modules/entities": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", @@ -13117,6 +14792,15 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -13348,11 +15032,6 @@ "node": ">=6" } }, - "node_modules/mocha/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, "node_modules/mocha/node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -13392,17 +15071,6 @@ "node": "*" } }, - "node_modules/mocha/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/mocha/node_modules/minimatch": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", @@ -13729,6 +15397,12 @@ "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", "peer": true }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", @@ -13883,14 +15557,6 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==" }, - "node_modules/nodemailer": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.8.0.tgz", - "integrity": "sha512-EjYvSmHzekz6VNkNd12aUqAco+bOkRe3Of5jVhltqKhEsjw/y0PYPJfp83+s9Wzh1dspYAkUW/YNQ350NATbSQ==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -13990,13 +15656,22 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object-path": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz", @@ -14005,6 +15680,54 @@ "node": ">= 10.12.0" } }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.2.tgz", + "integrity": "sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==", + "dev": true, + "dependencies": { + "array.prototype.filter": "^1.0.3", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.0.0" + } + }, "node_modules/object.omit": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-3.0.0.tgz", @@ -14027,6 +15750,23 @@ "node": ">=0.10.0" } }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obliterator": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", @@ -14832,6 +16572,14 @@ "node": ">=10" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -14889,20 +16637,32 @@ } }, "node_modules/prettier": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz", - "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -15336,8 +17096,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "peer": true + ] }, "node_modules/quick-lru": { "version": "5.1.1", @@ -15775,6 +17534,24 @@ "@babel/runtime": "^7.8.4" } }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regexpu-core": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", @@ -15904,11 +17681,11 @@ } }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -15959,6 +17736,16 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", @@ -16080,6 +17867,29 @@ } } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/run-parallel-limit": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz", @@ -16109,6 +17919,30 @@ "integrity": "sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==", "peer": true }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -16139,6 +17973,23 @@ "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", "optional": true }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -16291,6 +18142,37 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -16523,9 +18405,9 @@ } }, "node_modules/socks/node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" }, "node_modules/solc": { "version": "0.7.3", @@ -16640,12 +18522,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -16777,6 +18653,51 @@ "node": ">=8" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", @@ -17246,6 +19167,22 @@ "node": ">=4.5" } }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tar": { "version": "6.1.13", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", @@ -17341,6 +19278,12 @@ "resolved": "https://registry.npmjs.org/text-mask-addons/-/text-mask-addons-3.8.0.tgz", "integrity": "sha512-VSZSdc/tKn4zGxgpJ+uNBzoW1t472AoAFIlbw1K7hSNXz0DfSBYDJNRxLqgxOfWw1BY2z6DQpm7g0sYZn5qLpg==" }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -17505,6 +19448,18 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -17549,296 +19504,109 @@ } }, "node_modules/ts-node-dev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", - "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", - "dev": true, - "dependencies": { - "chokidar": "^3.5.1", - "dynamic-dedupe": "^0.3.0", - "minimist": "^1.2.6", - "mkdirp": "^1.0.4", - "resolve": "^1.0.0", - "rimraf": "^2.6.1", - "source-map-support": "^0.5.12", - "tree-kill": "^1.2.2", - "ts-node": "^10.4.0", - "tsconfig": "^7.0.0" - }, - "bin": { - "ts-node-dev": "lib/bin.js", - "tsnd": "lib/bin.js" - }, - "engines": { - "node": ">=0.8.0" - }, - "peerDependencies": { - "node-notifier": "*", - "typescript": "*" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/ts-node-dev/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/ts-node/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/tsconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", - "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", - "dev": true, - "dependencies": { - "@types/strip-bom": "^3.0.0", - "@types/strip-json-comments": "0.0.30", - "strip-bom": "^3.0.0", - "strip-json-comments": "^2.0.0" - } - }, - "node_modules/tsconfig/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "node_modules/tslint": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", - "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", - "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.3", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.13.0", - "tsutils": "^2.29.0" - }, - "bin": { - "tslint": "bin/tslint" - }, - "engines": { - "node": ">=4.8.0" - }, - "peerDependencies": { - "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" - } - }, - "node_modules/tslint-config-prettier": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz", - "integrity": "sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==", - "dev": true, - "bin": { - "tslint-config-prettier-check": "bin/check.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/tslint-plugin-prettier": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslint-plugin-prettier/-/tslint-plugin-prettier-2.3.0.tgz", - "integrity": "sha512-F9e4K03yc9xuvv+A0v1EmjcnDwpz8SpCD8HzqSDe0eyg34cBinwn9JjmnnRrNAs4HdleRQj7qijp+P/JTxt4vA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", "dev": true, "dependencies": { - "eslint-plugin-prettier": "^2.2.0", - "lines-and-columns": "^1.1.6", - "tslib": "^1.7.1" + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" }, "engines": { - "node": ">= 4" + "node": ">=0.8.0" }, "peerDependencies": { - "prettier": "^1.9.0 || ^2.0.0", - "tslint": "^5.0.0 || ^6.0.0" - } - }, - "node_modules/tslint-plugin-prettier/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/tslint/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" + "node-notifier": "*", + "typescript": "*" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/tslint/node_modules/builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/tslint/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/ts-node-dev/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "glob": "^7.1.3" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tslint/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" + "bin": { + "rimraf": "bin.js" } }, - "node_modules/tslint/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/tslint/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "node_modules/tslint/node_modules/diff": { + "node_modules/ts-node/node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.3.1" } }, - "node_modules/tslint/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/tslint/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", "dev": true, - "engines": { - "node": ">=4" + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" } }, - "node_modules/tslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" } }, - "node_modules/tslint/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { - "minimist": "^1.2.6" + "minimist": "^1.2.0" }, "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/tslint/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" + "json5": "lib/cli.js" } }, - "node_modules/tslint/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/tslint/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsort": { "version": "0.0.1", @@ -17846,24 +19614,6 @@ "integrity": "sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw==", "peer": true }, - "node_modules/tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "peerDependencies": { - "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -17878,12 +19628,14 @@ "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "peer": true }, "node_modules/tweetnacl-util": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", - "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==", + "peer": true }, "node_modules/twitter-api-sdk": { "version": "1.2.1", @@ -18020,6 +19772,79 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz", + "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -18326,6 +20151,21 @@ "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/unbzip2-stream": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", @@ -18336,9 +20176,9 @@ } }, "node_modules/undici": { - "version": "5.28.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz", - "integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==", + "version": "5.28.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", + "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", "peer": true, "dependencies": { "@fastify/busboy": "^2.0.0" @@ -18610,27 +20450,27 @@ } }, "node_modules/web3": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.10.3.tgz", - "integrity": "sha512-DgUdOOqC/gTqW+VQl1EdPxrVRPB66xVNtuZ5KD4adVBtko87hkgM8BTZ0lZ8IbUfnQk6DyjcDujMiH3oszllAw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.10.4.tgz", + "integrity": "sha512-kgJvQZjkmjOEKimx/tJQsqWfRDPTTcBfYPa9XletxuHLpHcXdx67w8EFn5AW3eVxCutE9dTVHgGa9VYe8vgsEA==", "hasInstallScript": true, "dependencies": { - "web3-bzz": "1.10.3", - "web3-core": "1.10.3", - "web3-eth": "1.10.3", - "web3-eth-personal": "1.10.3", - "web3-net": "1.10.3", - "web3-shh": "1.10.3", - "web3-utils": "1.10.3" + "web3-bzz": "1.10.4", + "web3-core": "1.10.4", + "web3-eth": "1.10.4", + "web3-eth-personal": "1.10.4", + "web3-net": "1.10.4", + "web3-shh": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-bzz": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.10.3.tgz", - "integrity": "sha512-XDIRsTwekdBXtFytMpHBuun4cK4x0ZMIDXSoo1UVYp+oMyZj07c7gf7tNQY5qZ/sN+CJIas4ilhN25VJcjSijQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.10.4.tgz", + "integrity": "sha512-ZZ/X4sJ0Uh2teU9lAGNS8EjveEppoHNQiKlOXAjedsrdWuaMErBPdLQjXfcrYvN6WM6Su9PMsAxf3FXXZ+HwQw==", "hasInstallScript": true, "dependencies": { "@types/node": "^12.12.6", @@ -18647,53 +20487,53 @@ "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" }, "node_modules/web3-core": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.10.3.tgz", - "integrity": "sha512-Vbk0/vUNZxJlz3RFjAhNNt7qTpX8yE3dn3uFxfX5OHbuon5u65YEOd3civ/aQNW745N0vGUlHFNxxmn+sG9DIw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.10.4.tgz", + "integrity": "sha512-B6elffYm81MYZDTrat7aEhnhdtVE3lDBUZft16Z8awYMZYJDbnykEbJVS+l3mnA7AQTnSDr/1MjWofGDLBJPww==", "dependencies": { "@types/bn.js": "^5.1.1", "@types/node": "^12.12.6", "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-core-requestmanager": "1.10.3", - "web3-utils": "1.10.3" + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-core-requestmanager": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-helpers": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.10.3.tgz", - "integrity": "sha512-Yv7dQC3B9ipOc5sWm3VAz1ys70Izfzb8n9rSiQYIPjpqtJM+3V4EeK6ghzNR6CO2es0+Yu9CtCkw0h8gQhrTxA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.10.4.tgz", + "integrity": "sha512-r+L5ylA17JlD1vwS8rjhWr0qg7zVoVMDvWhajWA5r5+USdh91jRUYosp19Kd1m2vE034v7Dfqe1xYRoH2zvG0g==", "dependencies": { - "web3-eth-iban": "1.10.3", - "web3-utils": "1.10.3" + "web3-eth-iban": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-method": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.10.3.tgz", - "integrity": "sha512-VZ/Dmml4NBmb0ep5PTSg9oqKoBtG0/YoMPei/bq/tUdlhB2dMB79sbeJPwx592uaV0Vpk7VltrrrBv5hTM1y4Q==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.10.4.tgz", + "integrity": "sha512-uZTb7flr+Xl6LaDsyTeE2L1TylokCJwTDrIVfIfnrGmnwLc6bmTWCCrm71sSrQ0hqs6vp/MKbQYIYqUN0J8WyA==", "dependencies": { "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.10.3", - "web3-core-promievent": "1.10.3", - "web3-core-subscriptions": "1.10.3", - "web3-utils": "1.10.3" + "web3-core-helpers": "1.10.4", + "web3-core-promievent": "1.10.4", + "web3-core-subscriptions": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-promievent": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.10.3.tgz", - "integrity": "sha512-HgjY+TkuLm5uTwUtaAfkTgRx/NzMxvVradCi02gy17NxDVdg/p6svBHcp037vcNpkuGeFznFJgULP+s2hdVgUQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.10.4.tgz", + "integrity": "sha512-2de5WnJQ72YcIhYwV/jHLc4/cWJnznuoGTJGD29ncFQHAfwW/MItHFSVKPPA5v8AhJe+r6y4Y12EKvZKjQVBvQ==", "dependencies": { "eventemitter3": "4.0.4" }, @@ -18707,27 +20547,27 @@ "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==" }, "node_modules/web3-core-requestmanager": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.10.3.tgz", - "integrity": "sha512-VT9sKJfgM2yBOIxOXeXiDuFMP4pxzF6FT+y8KTLqhDFHkbG3XRe42Vm97mB/IvLQCJOmokEjl3ps8yP1kbggyw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.10.4.tgz", + "integrity": "sha512-vqP6pKH8RrhT/2MoaU+DY/OsYK9h7HmEBNCdoMj+4ZwujQtw/Mq2JifjwsJ7gits7Q+HWJwx8q6WmQoVZAWugg==", "dependencies": { "util": "^0.12.5", - "web3-core-helpers": "1.10.3", - "web3-providers-http": "1.10.3", - "web3-providers-ipc": "1.10.3", - "web3-providers-ws": "1.10.3" + "web3-core-helpers": "1.10.4", + "web3-providers-http": "1.10.4", + "web3-providers-ipc": "1.10.4", + "web3-providers-ws": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-subscriptions": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.10.3.tgz", - "integrity": "sha512-KW0Mc8sgn70WadZu7RjQ4H5sNDJ5Lx8JMI3BWos+f2rW0foegOCyWhRu33W1s6ntXnqeBUw5rRCXZRlA3z+HNA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.10.4.tgz", + "integrity": "sha512-o0lSQo/N/f7/L76C0HV63+S54loXiE9fUPfHFcTtpJRQNDBVsSDdWRdePbWwR206XlsBqD5VHApck1//jEafTw==", "dependencies": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.10.3" + "web3-core-helpers": "1.10.4" }, "engines": { "node": ">=8.0.0" @@ -18744,43 +20584,43 @@ "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" }, "node_modules/web3-eth": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.10.3.tgz", - "integrity": "sha512-Uk1U2qGiif2mIG8iKu23/EQJ2ksB1BQXy3wF3RvFuyxt8Ft9OEpmGlO7wOtAyJdoKzD5vcul19bJpPcWSAYZhA==", - "dependencies": { - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-core-subscriptions": "1.10.3", - "web3-eth-abi": "1.10.3", - "web3-eth-accounts": "1.10.3", - "web3-eth-contract": "1.10.3", - "web3-eth-ens": "1.10.3", - "web3-eth-iban": "1.10.3", - "web3-eth-personal": "1.10.3", - "web3-net": "1.10.3", - "web3-utils": "1.10.3" + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.10.4.tgz", + "integrity": "sha512-Sql2kYKmgt+T/cgvg7b9ce24uLS7xbFrxE4kuuor1zSCGrjhTJ5rRNG8gTJUkAJGKJc7KgnWmgW+cOfMBPUDSA==", + "dependencies": { + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-core-subscriptions": "1.10.4", + "web3-eth-abi": "1.10.4", + "web3-eth-accounts": "1.10.4", + "web3-eth-contract": "1.10.4", + "web3-eth-ens": "1.10.4", + "web3-eth-iban": "1.10.4", + "web3-eth-personal": "1.10.4", + "web3-net": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-abi": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.10.3.tgz", - "integrity": "sha512-O8EvV67uhq0OiCMekqYsDtb6FzfYzMXT7VMHowF8HV6qLZXCGTdB/NH4nJrEh2mFtEwVdS6AmLFJAQd2kVyoMQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.10.4.tgz", + "integrity": "sha512-cZ0q65eJIkd/jyOlQPDjr8X4fU6CRL1eWgdLwbWEpo++MPU/2P4PFk5ZLAdye9T5Sdp+MomePPJ/gHjLMj2VfQ==", "dependencies": { "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.10.3" + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-accounts": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.10.3.tgz", - "integrity": "sha512-8MipGgwusDVgn7NwKOmpeo3gxzzd+SmwcWeBdpXknuyDiZSQy9tXe+E9LeFGrmys/8mLLYP79n3jSbiTyv+6pQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.10.4.tgz", + "integrity": "sha512-ysy5sVTg9snYS7tJjxVoQAH6DTOTkRGR8emEVCWNGLGiB9txj+qDvSeT0izjurS/g7D5xlMAgrEHLK1Vi6I3yg==", "dependencies": { "@ethereumjs/common": "2.6.5", "@ethereumjs/tx": "3.5.2", @@ -18788,10 +20628,10 @@ "eth-lib": "0.2.8", "scrypt-js": "^3.0.1", "uuid": "^9.0.0", - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" @@ -18825,64 +20665,64 @@ } }, "node_modules/web3-eth-contract": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.10.3.tgz", - "integrity": "sha512-Y2CW61dCCyY4IoUMD4JsEQWrILX4FJWDWC/Txx/pr3K/+fGsBGvS9kWQN5EsVXOp4g7HoFOfVh9Lf7BmVVSRmg==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.10.4.tgz", + "integrity": "sha512-Q8PfolOJ4eV9TvnTj1TGdZ4RarpSLmHnUnzVxZ/6/NiTfe4maJz99R0ISgwZkntLhLRtw0C7LRJuklzGYCNN3A==", "dependencies": { "@types/bn.js": "^5.1.1", - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-core-promievent": "1.10.3", - "web3-core-subscriptions": "1.10.3", - "web3-eth-abi": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-core-promievent": "1.10.4", + "web3-core-subscriptions": "1.10.4", + "web3-eth-abi": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-ens": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.10.3.tgz", - "integrity": "sha512-hR+odRDXGqKemw1GFniKBEXpjYwLgttTES+bc7BfTeoUyUZXbyDHe5ifC+h+vpzxh4oS0TnfcIoarK0Z9tFSiQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.10.4.tgz", + "integrity": "sha512-LLrvxuFeVooRVZ9e5T6OWKVflHPFgrVjJ/jtisRWcmI7KN/b64+D/wJzXqgmp6CNsMQcE7rpmf4CQmJCrTdsgg==", "dependencies": { "content-hash": "^2.5.2", "eth-ens-namehash": "2.0.8", - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-promievent": "1.10.3", - "web3-eth-abi": "1.10.3", - "web3-eth-contract": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-promievent": "1.10.4", + "web3-eth-abi": "1.10.4", + "web3-eth-contract": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-iban": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.10.3.tgz", - "integrity": "sha512-ZCfOjYKAjaX2TGI8uif5ah+J3BYFuo+47JOIV1RIz2l7kD9VfnxvRH5UiQDRyMALQC7KFd2hUqIEtHklapNyKA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.10.4.tgz", + "integrity": "sha512-0gE5iNmOkmtBmbKH2aTodeompnNE8jEyvwFJ6s/AF6jkw9ky9Op9cqfzS56AYAbrqEFuClsqB/AoRves7LDELw==", "dependencies": { "bn.js": "^5.2.1", - "web3-utils": "1.10.3" + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-personal": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.10.3.tgz", - "integrity": "sha512-avrQ6yWdADIvuNQcFZXmGLCEzulQa76hUOuVywN7O3cklB4nFc/Gp3yTvD3bOAaE7DhjLQfhUTCzXL7WMxVTsw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.10.4.tgz", + "integrity": "sha512-BRa/hs6jU1hKHz+AC/YkM71RP3f0Yci1dPk4paOic53R4ZZG4MgwKRkJhgt3/GPuPliwS46f/i5A7fEGBT4F9w==", "dependencies": { "@types/node": "^12.12.6", - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-net": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-net": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" @@ -18894,27 +20734,27 @@ "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" }, "node_modules/web3-net": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.10.3.tgz", - "integrity": "sha512-IoSr33235qVoI1vtKssPUigJU9Fc/Ph0T9CgRi15sx+itysmvtlmXMNoyd6Xrgm9LuM4CIhxz7yDzH93B79IFg==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.10.4.tgz", + "integrity": "sha512-mKINnhOOnZ4koA+yV2OT5s5ztVjIx7IY9a03w6s+yao/BUn+Luuty0/keNemZxTr1E8Ehvtn28vbOtW7Ids+Ow==", "dependencies": { - "web3-core": "1.10.3", - "web3-core-method": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-method": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-http": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.10.3.tgz", - "integrity": "sha512-6dAgsHR3MxJ0Qyu3QLFlQEelTapVfWNTu5F45FYh8t7Y03T1/o+YAkVxsbY5AdmD+y5bXG/XPJ4q8tjL6MgZHw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.10.4.tgz", + "integrity": "sha512-m2P5Idc8hdiO0l60O6DSCPw0kw64Zgi0pMjbEFRmxKIck2Py57RQMu4bxvkxJwkF06SlGaEQF8rFZBmuX7aagQ==", "dependencies": { "abortcontroller-polyfill": "^1.7.5", "cross-fetch": "^4.0.0", "es6-promise": "^4.2.8", - "web3-core-helpers": "1.10.3" + "web3-core-helpers": "1.10.4" }, "engines": { "node": ">=8.0.0" @@ -18948,24 +20788,24 @@ } }, "node_modules/web3-providers-ipc": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.10.3.tgz", - "integrity": "sha512-vP5WIGT8FLnGRfswTxNs9rMfS1vCbMezj/zHbBe/zB9GauBRTYVrUo2H/hVrhLg8Ut7AbsKZ+tCJ4mAwpKi2hA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.10.4.tgz", + "integrity": "sha512-YRF/bpQk9z3WwjT+A6FI/GmWRCASgd+gC0si7f9zbBWLXjwzYAKG73bQBaFRAHex1hl4CVcM5WUMaQXf3Opeuw==", "dependencies": { "oboe": "2.1.5", - "web3-core-helpers": "1.10.3" + "web3-core-helpers": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-ws": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.10.3.tgz", - "integrity": "sha512-/filBXRl48INxsh6AuCcsy4v5ndnTZ/p6bl67kmO9aK1wffv7CT++DrtclDtVMeDGCgB3van+hEf9xTAVXur7Q==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.10.4.tgz", + "integrity": "sha512-j3FBMifyuFFmUIPVQR4pj+t5ILhAexAui0opgcpu9R5LxQrLRUZxHSnU+YO25UycSOa/NAX8A+qkqZNpcFAlxA==", "dependencies": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.10.3", + "web3-core-helpers": "1.10.4", "websocket": "^1.0.32" }, "engines": { @@ -18978,24 +20818,24 @@ "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==" }, "node_modules/web3-shh": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.10.3.tgz", - "integrity": "sha512-cAZ60CPvs9azdwMSQ/PSUdyV4PEtaW5edAZhu3rCXf6XxQRliBboic+AvwUvB6j3eswY50VGa5FygfVmJ1JVng==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.10.4.tgz", + "integrity": "sha512-cOH6iFFM71lCNwSQrC3niqDXagMqrdfFW85hC9PFUrAr3PUrIem8TNstTc3xna2bwZeWG6OBy99xSIhBvyIACw==", "hasInstallScript": true, "dependencies": { - "web3-core": "1.10.3", - "web3-core-method": "1.10.3", - "web3-core-subscriptions": "1.10.3", - "web3-net": "1.10.3" + "web3-core": "1.10.4", + "web3-core-method": "1.10.4", + "web3-core-subscriptions": "1.10.4", + "web3-net": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-utils": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.3.tgz", - "integrity": "sha512-OqcUrEE16fDBbGoQtZXWdavsPzbGIDc5v3VrRTZ0XrIpefC/viZ1ZU9bGEemazyS0catk/3rkOOxpzTfY+XsyQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.4.tgz", + "integrity": "sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==", "dependencies": { "@ethereumjs/util": "^8.1.0", "bn.js": "^5.2.1", @@ -19133,6 +20973,22 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-pm-runs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", @@ -19143,16 +20999,15 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", + "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.1" }, "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index 3114b45db..7ac059eb1 100644 --- a/package.json +++ b/package.json @@ -16,23 +16,12 @@ "@sentry/node": "6.16.1", "@sentry/tracing": "^6.2.0", "@solana/web3.js": "^1.87.6", - "@types/bcryptjs": "^2.4.2", - "@types/connect-redis": "0.0.23", - "@types/cors": "^2.8.10", - "@types/dotenv": "^8.2.0", - "@types/express": "^4.17.21", - "@types/graphql-upload": "15.0.2", - "@types/jsonwebtoken": "^8.5.0", - "@types/marked": "^4.0.8", - "@types/node": "^14.14.31", - "@types/uuid": "^7.0.4", "@uniswap/sdk": "^3.0.3", "abi-decoder": "^2.4.0", "adminjs": "6.8.3", "axios": "^1.6.7", "axios-retry": "^3.9.1", "bcrypt": "^5.0.1", - "bcryptjs": "^2.4.3", "body-parser": "^1.19.0", "bull": "^4.12.2", "bunyan": "^1.8.15", @@ -44,7 +33,6 @@ "cors": "^2.8.5", "csvtojson": "^2.0.10", "dotenv": "^8.2.0", - "eth-sig-util": "^3.0.1", "ethers": "^5.7.2", "express": "^4.18.2", "express-formidable": "^1.2.0", @@ -69,7 +57,6 @@ "marked": "^4.2.5", "moment": "^2.29.4", "node-cron": "^3.0.2", - "nodemailer": "^6.5.0", "patch-package": "^6.5.1", "rate-limit-redis": "^4.2.0", "reflect-metadata": "^0.1.13", @@ -80,12 +67,11 @@ "twitter-api-sdk": "^1.0.9", "type-graphql": "2.0.0-beta.1", "typedi": "0.8.0", - "typeorm": "0.3.20", - "typescript": "^4.9.4" + "typeorm": "0.3.20" }, "lint-staged": { "*.ts": [ - "tslint --fix", + "eslint --fix", "standard --fix", "git add" ] @@ -94,25 +80,38 @@ "@types/axios": "^0.14.0", "@types/bull": "^3.15.5", "@types/chai": "^4.2.15", - "@types/html-to-text": "^9.0.0", + "@types/connect-redis": "0.0.23", + "@types/cors": "^2.8.10", + "@types/dotenv": "^8.2.0", + "@types/express": "^4.17.21", + "@types/graphql-upload": "15.0.2", + "@types/jsonwebtoken": "^8.5.0", "@types/lodash": "^4.14.197", + "@types/marked": "^4.0.8", "@types/mocha": "^8.2.1", + "@types/node": "^14.14.31", "@types/node-cron": "^3.0.0", + "@types/html-to-text": "^9.0.0", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", "chai": "^4.3.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-unused-imports": "^3.1.0", "husky": "^4.3.8", "lint-staged": "^10.5.4", "mocha": "^10.2.0", - "prettier": "^2.4.1", + "prettier": "^3.2.5", "sinon": "^13.0.1", "ts-node": "10.9.2", "ts-node-dev": "2.0.0", - "tslint": "^6.1.3", - "tslint-config-prettier": "^1.18.0", - "tslint-plugin-prettier": "^2.3.0" + "typescript": "^4.9.4" }, "scripts": { - "tslint": "tslint -c tslint.json '{src,test}/**/*.ts'", - "tslint:fix": "tslint -c tslint.json --fix '{src,test,migration}/**/*.ts'", + "eslint": "eslint {src,test,migration}/**/*.ts", + "eslint:fix": "eslint --fix {src,test,migration}/**/*.ts", "test": "NODE_ENV=test mocha --config ./.mocharc.all-test.json", "test:syncProjectsRequiredForListing": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/syncProjectsRequiredForListing.test.ts", "test:backupDonationImport": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/backupDonationImport.test.ts", @@ -143,6 +142,7 @@ "test:campaignResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/campaignResolver.test.ts", "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: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", @@ -163,6 +163,7 @@ "test:anchorContractAddressRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/anchorContractAddressRepository.test.ts", "test:recurringDonationRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/recurringDonationRepository.test.ts", "test:userPassportScoreRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/userPassportScoreRepository.test.ts", + "test:recurringDonationService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/recurringDonationService.test.ts", "test:dbCronRepository": "NODE_ENV=test mocha -t 90000 ./test/pre-test-scripts.ts ./src/repositories/dbCronRepository.test.ts", "test:powerBoostingResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/powerBoostingResolver.test.ts", "test:userProjectPowerResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/userProjectPowerResolver.test.ts", @@ -173,6 +174,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: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", "test:reactionsService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/reactionsService.test.ts", @@ -190,6 +192,7 @@ "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: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", "test:donationTab": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/server/adminJs/tabs/donationTab.test.ts ", "test:sybilTab": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/server/adminJs/tabs/sybilTab.test.ts ", "test:projectFraudTab": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/server/adminJs/tabs/projectFraudTab.test.ts", @@ -204,10 +207,10 @@ "db:migrate:revert:local": "NODE_ENV=development npx typeorm-ts-node-commonjs migration:revert -d ./src/ormconfig.ts", "db:migrate:run:production": "NODE_ENV=production npx typeorm-ts-node-commonjs migration:run -d ./src/ormconfig.ts", "db:migrate:rever:productiont": "NODE_ENV=production npx typeorm-ts-node-commonjs migration:revert -d ./src/ormconfig.ts", - "prettify": "prettier --write '**/*.ts*'", + "prettify": "prettier --write '**/*.ts*' '**/*.test.ts*'", "db:migrate:seedToken:run": "NODE_ENV=development ts-node ./node_modules/typeorm/cli -f ./src/seedToken-ormconfig.ts migration:run", "db:migrate:seedToken:revert": "NODE_ENV=development ts-node ./node_modules/typeorm/cli -f ./src/seedToken-ormconfig.ts migration:revert", - "build": "rm -rf ./build && tsc && mkdir ./build/config && mkdir ./build/src/server/adminJs/tabs/components && cp -r src/server/adminJs/tabs/components/* ./build/src/server/adminJs/tabs/components/ && mkdir ./build/src/utils/locales && cp -r ./src/utils/locales/* ./build/src/utils/locales/ ", + "build": "rm -rf ./build && tsc && mkdir ./build/config && mkdir ./build/src/server/adminJs/tabs/components && cp -r src/server/adminJs/tabs/components/* ./build/src/server/adminJs/tabs/components/ && mkdir ./build/src/utils/locales && cp -r ./src/utils/locales/* ./build/src/utils/locales/ && cp -r ./src/abi build/src/abi ", "dev": "NODE_ENV=development node ./build/src/index.js", "production": "NODE_ENV=production node ./build/src/index.js", "start:docker:server": "npm run db:migrate:run:production && npm run production", @@ -216,7 +219,7 @@ }, "husky": { "hooks": { - "pre-commit": "npm run tslint" + "pre-commit": "npm run eslint" } }, "standard": { diff --git a/src/abi/anchorContractAbi.json b/src/abi/anchorContractAbi.json new file mode 100644 index 000000000..eb7fc6541 --- /dev/null +++ b/src/abi/anchorContractAbi.json @@ -0,0 +1,47 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_param1", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_param2", + "type": "string" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "_innerParam1", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_innerParam2", + "type": "string" + } + ], + "internalType": "struct TupleName", + "name": "_tupleParam", + "type": "tuple" + }, + { + "internalType": "address", + "name": "_param3", + "type": "address" + }, + { + "internalType": "address[]", + "name": "_param4", + "type": "address[]" + } + ], + "name": "createProfile", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/adapters/adaptersFactory.ts b/src/adapters/adaptersFactory.ts index 9eaeb39b5..7c5964527 100644 --- a/src/adapters/adaptersFactory.ts +++ b/src/adapters/adaptersFactory.ts @@ -19,6 +19,9 @@ import { GivPowerBalanceAggregatorAdapter } from './givPowerBalanceAggregator/gi import { GivPowerBalanceAggregatorAdapterMock } from './givPowerBalanceAggregator/givPowerBalanceAggregatorAdapterMock'; import { DonationSaveBackupAdapter } from './donationSaveBackup/donationSaveBackupAdapter'; import { DonationSaveBackupMockAdapter } from './donationSaveBackup/DonationSaveBackupMockAdapter'; +import { SuperFluidAdapter } from './superFluid/superFluidAdapter'; +import { SuperFluidMockAdapter } from './superFluid/superFluidMockAdapter'; +import { SuperFluidAdapterInterface } from './superFluid/superFluidAdapterInterface'; const discordAdapter = new DiscordAdapter(); const googleAdapter = new GoogleAdapter(); @@ -130,3 +133,17 @@ export const getDonationSaveBackupAdapter = () => { return mockDonationSaveBackupAdapter; } }; + +const superFluidAdapter = new SuperFluidAdapter(); +const superFluidMockAdapter = new SuperFluidMockAdapter(); + +export const getSuperFluidAdapter = (): SuperFluidAdapterInterface => { + switch (process.env.SUPER_FLUID_ADAPTER) { + case 'superfluid': + return superFluidAdapter; + case 'mock': + return superFluidMockAdapter; + default: + return superFluidMockAdapter; + } +}; diff --git a/src/adapters/chainvine/chainvineAdapter.ts b/src/adapters/chainvine/chainvineAdapter.ts index 271ebc1e5..0c52d6bc0 100644 --- a/src/adapters/chainvine/chainvineAdapter.ts +++ b/src/adapters/chainvine/chainvineAdapter.ts @@ -1,11 +1,10 @@ +import { ChainvineClient } from '@chainvine/sdk/lib'; +import { Response } from 'express'; import { ChainvineAdapterInterface, LinkDonorToChainvineReferrerType, NotifyChainVineInputType, } from './chainvineAdapterInterface'; - -import { ChainvineClient } from '@chainvine/sdk/lib'; -import { Response } from 'express'; import { errorMessages } from '../../utils/errorMessages'; import { logger } from '../../utils/logger'; @@ -76,9 +75,8 @@ export class ChainvineAdapter implements ChainvineAdapterInterface { walletAddress: string, ): Promise { try { - const chainvineResult = await this.ChainvineSDK.getReferralUrl( - walletAddress, - ); + const chainvineResult = + await this.ChainvineSDK.getReferralUrl(walletAddress); // https://app.chainvine.xyz/giveth?referrer_id=xxxxxxxxxxxxxxxxxxxxxxxxx const referralUrl = new URL(chainvineResult?.url); const referredId = referralUrl.searchParams.get('referrer_id'); diff --git a/src/adapters/chainvine/chainvineMockAdapter.ts b/src/adapters/chainvine/chainvineMockAdapter.ts index 7d9a0468e..318603074 100644 --- a/src/adapters/chainvine/chainvineMockAdapter.ts +++ b/src/adapters/chainvine/chainvineMockAdapter.ts @@ -1,8 +1,4 @@ -import { - ChainvineAdapterInterface, - LinkDonorToChainvineReferrerType, - NotifyChainVineInputType, -} from './chainvineAdapterInterface'; +import { ChainvineAdapterInterface } from './chainvineAdapterInterface'; import { generateRandomEtheriumAddress, generateHexNumber, @@ -19,21 +15,19 @@ export class ChainvineMockAdapter implements ChainvineAdapterInterface { return Promise.resolve(cachedReferralIds[referrerId]); } - notifyChainVine(params: NotifyChainVineInputType): Promise { + notifyChainVine(): Promise { return Promise.resolve(undefined); } - registerClickEvent(referrerId: string): Promise { + registerClickEvent(): Promise { return Promise.resolve(undefined); } - linkDonorToReferrer(params: LinkDonorToChainvineReferrerType): Promise { + linkDonorToReferrer(): Promise { return Promise.resolve(undefined); } - async generateChainvineId( - walletAddress: string, - ): Promise { + async generateChainvineId(): Promise { return generateHexNumber(10); } } diff --git a/src/adapters/donationSaveBackup/DonationSaveBackupMockAdapter.ts b/src/adapters/donationSaveBackup/DonationSaveBackupMockAdapter.ts index 4de1f8755..29ff72d5d 100644 --- a/src/adapters/donationSaveBackup/DonationSaveBackupMockAdapter.ts +++ b/src/adapters/donationSaveBackup/DonationSaveBackupMockAdapter.ts @@ -6,36 +6,29 @@ import { export class DonationSaveBackupMockAdapter implements DonationSaveBackupInterface { - async getNotImportedDonationsFromBackup(params: { - limit: number; - }): Promise { + async getNotImportedDonationsFromBackup(): Promise< + FetchedSavedFailDonationInterface[] + > { return []; } - async getSingleDonationFromBackupByTxHash( - txHash: string, - ): Promise { + async getSingleDonationFromBackupByTxHash(): Promise { return null; } - async markDonationAsImported(donationMongoId: string): Promise { + async markDonationAsImported(): Promise { // } - async unmarkDonationAsImported(donationMongoId: string): Promise { + async unmarkDonationAsImported(): Promise { // } - async getSingleDonationFromBackupById( - donationMongoId: string, - ): Promise { + async getSingleDonationFromBackupById(): Promise { return null; } - markDonationAsImportError( - donationMongoId: string, - errorMessage, - ): Promise { + markDonationAsImportError(): Promise { return Promise.resolve(undefined); } } diff --git a/src/adapters/donationSaveBackup/donationSaveBackupAdapter.ts b/src/adapters/donationSaveBackup/donationSaveBackupAdapter.ts index dea9bd4f8..c4bd380f4 100644 --- a/src/adapters/donationSaveBackup/donationSaveBackupAdapter.ts +++ b/src/adapters/donationSaveBackup/donationSaveBackupAdapter.ts @@ -2,9 +2,9 @@ // it must filter objects by those doesn't have `imported` field with true value // also must support pagination +import axios from 'axios'; import { logger } from '../../utils/logger'; import config from '../../config'; -import axios from 'axios'; import { DonationSaveBackupInterface, FetchedSavedFailDonationInterface, diff --git a/src/adapters/gitcoin/gitcoinMockAdapter.ts b/src/adapters/gitcoin/gitcoinMockAdapter.ts index 23f381fe9..721eb6827 100644 --- a/src/adapters/gitcoin/gitcoinMockAdapter.ts +++ b/src/adapters/gitcoin/gitcoinMockAdapter.ts @@ -1,4 +1,3 @@ -import { generateRandomEtheriumAddress } from '../../../test/testUtils'; import { GitcoinAdapterInterface, SigningMessageAndNonceResponse, diff --git a/src/adapters/givPowerBalanceAggregator/givPowerBalanceAggregatorAdapter.ts b/src/adapters/givPowerBalanceAggregator/givPowerBalanceAggregatorAdapter.ts index 75041c204..69c2e4df1 100644 --- a/src/adapters/givPowerBalanceAggregator/givPowerBalanceAggregatorAdapter.ts +++ b/src/adapters/givPowerBalanceAggregator/givPowerBalanceAggregatorAdapter.ts @@ -1,5 +1,3 @@ -// @ts-ignore - import axios from 'axios'; import { BalancesAtTimestampInputParams, diff --git a/src/adapters/givPowerBalanceAggregator/givPowerBalanceAggregatorAdapterMock.ts b/src/adapters/givPowerBalanceAggregator/givPowerBalanceAggregatorAdapterMock.ts index fc6ee9dfd..8af86f797 100644 --- a/src/adapters/givPowerBalanceAggregator/givPowerBalanceAggregatorAdapterMock.ts +++ b/src/adapters/givPowerBalanceAggregator/givPowerBalanceAggregatorAdapterMock.ts @@ -1,13 +1,11 @@ +import _ from 'lodash'; import { BalancesAtTimestampInputParams, BalanceResponse, - BalanceUpdatedAfterDateInputParams, LatestBalanceInputParams, - NetworksInputParams, IGivPowerBalanceAggregator, } from '../../types/GivPowerBalanceAggregator'; import { convertTimeStampToSeconds } from '../../utils/utils'; -import _ from 'lodash'; export class GivPowerBalanceAggregatorAdapterMock implements IGivPowerBalanceAggregator @@ -47,9 +45,7 @@ export class GivPowerBalanceAggregatorAdapterMock }); } - async getBalancesUpdatedAfterDate( - params: BalanceUpdatedAfterDateInputParams, - ): Promise { + async getBalancesUpdatedAfterDate(): Promise { // Mocked data return [ { @@ -67,9 +63,7 @@ export class GivPowerBalanceAggregatorAdapterMock ]; } - async getLeastIndexedBlockTimeStamp( - params: NetworksInputParams, - ): Promise { + async getLeastIndexedBlockTimeStamp(): Promise { // Mocked data return convertTimeStampToSeconds(new Date().getTime()); } diff --git a/src/adapters/givpowerSubgraph/givPowerSubgraphAdapter.test.ts b/src/adapters/givpowerSubgraph/givPowerSubgraphAdapter.test.ts index d28989e2a..bcdd7accd 100644 --- a/src/adapters/givpowerSubgraph/givPowerSubgraphAdapter.test.ts +++ b/src/adapters/givpowerSubgraph/givPowerSubgraphAdapter.test.ts @@ -1,5 +1,5 @@ -import { formatGivPowerBalance } from './givPowerSubgraphAdapter'; import { assert } from 'chai'; +import { formatGivPowerBalance } from './givPowerSubgraphAdapter'; import { generateRandomEtheriumAddress } from '../../../test/testUtils'; import { givPowerSubgraphAdapter } from '../adaptersFactory'; diff --git a/src/adapters/givpowerSubgraph/givPowerSubgraphAdapter.ts b/src/adapters/givpowerSubgraph/givPowerSubgraphAdapter.ts index dee0b9547..4e340e0b5 100644 --- a/src/adapters/givpowerSubgraph/givPowerSubgraphAdapter.ts +++ b/src/adapters/givpowerSubgraph/givPowerSubgraphAdapter.ts @@ -1,10 +1,10 @@ +import BigNumber from 'bignumber.js'; +import axios from 'axios'; import { BlockInfo, IGivPowerSubgraphAdapter, UnipoolBalance, } from './IGivPowerSubgraphAdapter'; -import BigNumber from 'bignumber.js'; -import axios from 'axios'; const _toBN = (n: string | number) => new BigNumber(n); export const formatGivPowerBalance = (balance: string | number): number => diff --git a/src/adapters/givpowerSubgraph/givPowerSubgraphAdapterMock.ts b/src/adapters/givpowerSubgraph/givPowerSubgraphAdapterMock.ts index 9e0a4dd98..fb4a17e1a 100644 --- a/src/adapters/givpowerSubgraph/givPowerSubgraphAdapterMock.ts +++ b/src/adapters/givpowerSubgraph/givPowerSubgraphAdapterMock.ts @@ -38,12 +38,9 @@ export class GivPowerSubgraphAdapterMock implements IGivPowerSubgraphAdapter { return Promise.resolve({ timestamp: 1000, number: 1000 }); } - getUserPowerBalanceUpdatedAfterTimestamp(params: { - timestamp: number; - blockNumber: number; - take: number; - skip: number; - }): Promise<{ [p: string]: UnipoolBalance }> { + getUserPowerBalanceUpdatedAfterTimestamp(): Promise<{ + [p: string]: UnipoolBalance; + }> { if (this.nextCallResult) { const customResult = this.nextCallResult; this.nextCallResult = null; diff --git a/src/adapters/notifications/MockNotificationAdapter.ts b/src/adapters/notifications/MockNotificationAdapter.ts index 4fc2492d3..2609538a1 100644 --- a/src/adapters/notifications/MockNotificationAdapter.ts +++ b/src/adapters/notifications/MockNotificationAdapter.ts @@ -1,33 +1,37 @@ import { BroadCastNotificationInputParams, NotificationAdapterInterface, + OrttoPerson, ProjectsHaveNewRankingInputParam, } from './NotificationAdapterInterface'; import { Donation } from '../../entities/donation'; import { Project } from '../../entities/project'; import { User } from '../../entities/user'; import { logger } from '../../utils/logger'; +import { RecurringDonation } from '../../entities/recurringDonation'; export class MockNotificationAdapter implements NotificationAdapterInterface { - async updateOrttoUser(params: { - firstName?: string; - lastName?: string; - email?: string; - userId?: string; - }): Promise { - logger.debug('MockNotificationAdapter updateOrttoUser', { - firstName: params.firstName, - lastName: params.lastName, - email: params.email, - userId: params.userId, - }); + async updateOrttoPeople(params: OrttoPerson[]): Promise { + logger.debug('MockNotificationAdapter updateOrttoPeople', params); + return Promise.resolve(undefined); + } + + userSuperTokensCritical(): Promise { return Promise.resolve(undefined); } donationReceived(params: { - donation: Donation; + donation: Donation | RecurringDonation; project: Project; }): Promise { + if (params.donation instanceof RecurringDonation) { + logger.debug('MockNotificationAdapter donationReceived', { + projectSlug: params.project.slug, + donationTxHash: params.donation.txHash, + donationNetworkId: params.donation.networkId, + }); + return Promise.resolve(undefined); + } logger.debug('MockNotificationAdapter donationReceived', { projectSlug: params.project.slug, donationTxHash: params.donation.transactionId, diff --git a/src/adapters/notifications/NotificationAdapterInterface.ts b/src/adapters/notifications/NotificationAdapterInterface.ts index b9999a0e3..cad690687 100644 --- a/src/adapters/notifications/NotificationAdapterInterface.ts +++ b/src/adapters/notifications/NotificationAdapterInterface.ts @@ -1,7 +1,7 @@ import { Donation } from '../../entities/donation'; import { Project } from '../../entities/project'; -import { User } from '../../entities/user'; -import exp from 'constants'; +import { UserStreamBalanceWarning, User } from '../../entities/user'; +import { RecurringDonation } from '../../entities/recurringDonation'; export interface BroadCastNotificationInputParams { broadCastNotificationId: number; @@ -19,22 +19,25 @@ export interface ProjectsHaveNewRankingInputParam { newBottomRank: number; } +export interface OrttoPerson { + fields: { + 'str::first': string; + 'str::last': string; + 'str::email': string; + 'str:cm:user-id'?: string; + 'int:cm:number-of-donations'?: number; + 'int:cm:total-donations-value'?: number; + 'dtz:cm:lastdonationdate'?: Date; + }; + tags: string[]; + unset_tags: string[]; +} + export interface NotificationAdapterInterface { - updateOrttoUser(params: { - firstName?: string; - lastName?: string; - email?: string; - userId?: string; - totalDonated?: number; - donationsCount?: string; - lastDonationDate?: Date | null; - GIVbacksRound?: number; - QFRound?: string; - donationChain?: string; - }): Promise; + updateOrttoPeople(params: OrttoPerson[]): Promise; donationReceived(params: { - donation: Donation; + donation: Donation | RecurringDonation; project: Project; user: User | null; }): Promise; @@ -50,6 +53,15 @@ export interface NotificationAdapterInterface { userId: number; }): Promise; + userSuperTokensCritical(params: { + user: User; + eventName: UserStreamBalanceWarning; + tokenSymbol: string; + project: Project; + isEnded: boolean; + networkName: string; + }): Promise; + projectVerified(params: { project: Project }): Promise; projectBoosted(params: { projectId: number; userId: number }): Promise; projectBoostedBatch(params: { diff --git a/src/adapters/notifications/NotificationCenterAdapter.ts b/src/adapters/notifications/NotificationCenterAdapter.ts index 7ceec83ca..a7cbe71b9 100644 --- a/src/adapters/notifications/NotificationCenterAdapter.ts +++ b/src/adapters/notifications/NotificationCenterAdapter.ts @@ -1,20 +1,20 @@ import axios from 'axios'; +import Bull from 'bull'; import { BroadCastNotificationInputParams, NotificationAdapterInterface, + OrttoPerson, ProjectsHaveNewRankingInputParam, } from './NotificationAdapterInterface'; import { Donation } from '../../entities/donation'; import { Project } from '../../entities/project'; -import { User } from '../../entities/user'; +import { UserStreamBalanceWarning, User } from '../../entities/user'; import { createBasicAuthentication } from '../../utils/utils'; import { logger } from '../../utils/logger'; import { NOTIFICATIONS_EVENT_NAMES } from '../../analytics/analytics'; -import Bull from 'bull'; import { redisConfig } from '../../redis'; import config from '../../config'; - import { findProjectById } from '../../repositories/projectRepository'; import { findAllUsers, @@ -23,7 +23,10 @@ import { } from '../../repositories/userRepository'; import { buildProjectLink } from './NotificationCenterUtils'; import { buildTxLink } from '../../utils/networks'; -import { findTokenByNetworkAndAddress } from '../../utils/tokenUtils'; +import { RecurringDonation } from '../../entities/recurringDonation'; +import { getTokenPrice } from '../../services/priceService'; +import { Token } from '../../entities/token'; +import { toFixNumber } from '../../services/donationService'; const notificationCenterUsername = process.env.NOTIFICATION_CENTER_USERNAME; const notificationCenterPassword = process.env.NOTIFICATION_CENTER_PASSWORD; const notificationCenterBaseUrl = process.env.NOTIFICATION_CENTER_BASE_URL; @@ -40,6 +43,7 @@ const sendBroadcastNotificationsQueue = new Bull( redis: redisConfig, }, ); + export class NotificationCenterAdapter implements NotificationAdapterInterface { readonly authorizationHeader: string; constructor() { @@ -52,71 +56,52 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { } } - async updateOrttoUser(params: { - firstName?: string; - lastName?: string; - email?: string; - userId?: string; - totalDonated?: number; - donationsCount?: string; - lastDonationDate?: Date | null; - GIVbacksRound?: number; - QFRound?: string; - donationChain?: string; + async userSuperTokensCritical(params: { + user: User; + eventName: UserStreamBalanceWarning; + tokenSymbol: string; + project: Project; + isEnded: boolean; + networkName: string; }): Promise { - try { - const { - firstName, - lastName, + logger.debug('userSuperTokensCritical', { params }); + const { eventName, tokenSymbol, project, user, isEnded, networkName } = + params; + const { email, walletAddress } = user; + const payload = { + userId: user.id, + email: user.email, + tokenSymbol, + isEnded, + }; + await sendProjectRelatedNotificationsQueue.add({ + project, + user: { email, - userId, - totalDonated, - donationsCount, - lastDonationDate, - GIVbacksRound, - QFRound, - donationChain, - } = params; - logger.debug('updateOrttoUser has been called', params); - const fields = { - 'str::first': firstName || '', - 'str::last': lastName || '', - 'str::email': email || '', - }; - if (process.env.ENVIRONMENT === 'production') { - // On production, we should update Ortto user profile based on user-id to avoid touching real users data - fields['str:cm:user-id'] = userId; - } - if (donationsCount) { - fields['int:cm:number-of-donations'] = Number(donationsCount); - } - if (totalDonated) { - // Ortto automatically adds three decimal points to integers - fields['int:cm:total-donations-value'] = - Number(totalDonated?.toFixed(3)) * 1000; - } - if (lastDonationDate) { - fields['dtz:cm:lastdonationdate'] = lastDonationDate; - } - const tags: string[] = []; - if (GIVbacksRound) { - tags.push(`GIVbacks ${GIVbacksRound}`); - } - if (QFRound) { - tags.push(`QF Donor ${QFRound}`); - } - if (donationChain) { - tags.push(`Donated on ${donationChain}`); - } + walletAddress: walletAddress!, + }, + eventName, + sendEmail: true, + metadata: { + ...payload, + networkName, + recurringDonationTab: `${process.env.WEBSITE_URL}/account?tab=recurring-donations`, + }, + segment: { + payload, + }, + }); + return; + } + + async updateOrttoPeople(people: OrttoPerson[]): Promise { + // TODO we should me this to notification-center, it's not good that we call Ortto directly + try { const data = { - people: [ - { - fields, - tags, - }, - ], + people, async: false, }; + logger.debug('updateOrttoPeople has been called:', people); const orttoConfig = { method: 'post', maxBodyLength: Infinity, @@ -129,7 +114,7 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { }; await axios.request(orttoConfig); } catch (e) { - logger.error('updateOrttoUser >> error', e); + logger.error('updateOrttoPeople >> error', e); } } @@ -167,15 +152,51 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { } async donationReceived(params: { - donation: Donation; + donation: Donation | RecurringDonation; project: Project; user: User | null; }): Promise { - if (params.donation.valueUsd <= 1) return; - const { project, donation, user } = params; + const isRecurringDonation = donation instanceof RecurringDonation; + let transactionId: string, transactionNetworkId: number; + if (isRecurringDonation) { + transactionId = donation.txHash; + transactionNetworkId = donation.networkId; + const token = await Token.findOneBy({ + symbol: donation.currency, + networkId: transactionNetworkId, + }); + const amount = + (Number(donation.flowRate) / 10 ** (token?.decimals || 18)) * 2628000; // convert flowRate in wei from per second to per month + const price = await getTokenPrice(transactionNetworkId, token!); + const donationValueUsd = toFixNumber(amount * price, 4); + logger.debug('donationReceived (recurring) has been called', { + params, + amount, + price, + donationValueUsd, + token, + }); + if (donationValueUsd <= 20) return; + } else { + transactionId = donation.transactionId; + transactionNetworkId = donation.transactionNetworkId; + const donationValueUsd = donation.valueUsd; + logger.debug('donationReceived has been called', { + params, + transactionId, + transactionNetworkId, + donationValueUsd, + }); + if (donationValueUsd <= 1) return; + } + await sendProjectRelatedNotificationsQueue.add({ project, + user: { + email: user?.email as string, + walletAddress: user?.walletAddress as string, + }, eventName: NOTIFICATIONS_EVENT_NAMES.DONATION_RECEIVED, sendEmail: true, segment: { @@ -187,17 +208,15 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { }, trackId: 'donation-received-' + - donation.transactionNetworkId + + transactionNetworkId + '-' + - donation.transactionId, + transactionId + + '-' + + isRecurringDonation, }); } - async donationSent(params: { - donation: Donation; - project: Project; - donor: User; - }): Promise { + async donationSent(): Promise { return; // const { project, donor, donation } = params; // await sendProjectRelatedNotificationsQueue.add({ @@ -471,10 +490,7 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { }); } - async projectReceivedHeartReaction(params: { - project: Project; - userId: number; - }): Promise { + async projectReceivedHeartReaction(): Promise { return; // const { project } = params; // await sendProjectRelatedNotificationsQueue.add({ @@ -491,11 +507,11 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { // }); } - ProfileIsCompleted(params: { user: User }): Promise { + ProfileIsCompleted(): Promise { return Promise.resolve(undefined); } - ProfileNeedToBeCompleted(params: { user: User }): Promise { + ProfileNeedToBeCompleted(): Promise { return Promise.resolve(undefined); } @@ -686,7 +702,7 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { } // commenting for now to test load of notification center. - async projectEdited(params: { project: Project }): Promise { + async projectEdited(): Promise { return; // const { project } = params; // const projectOwner = project?.adminUser as User; @@ -794,7 +810,6 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { }): Promise { const { project, donationInfo } = params; const { txLink, reason } = donationInfo; - const projectOwner = project?.adminUser as User; const now = Date.now(); await sendProjectRelatedNotificationsQueue.add({ @@ -866,7 +881,6 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { if (!project) { continue; } - const projectOwner = project.adminUser; let eventName; // https://github.com/Giveth/impact-graph/issues/774#issuecomment-1542337083 @@ -909,14 +923,35 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { const getEmailDataDonationAttributes = async (params: { user: User; project: Project; - donation: Donation; + donation: Donation | RecurringDonation; }) => { const { user, project, donation } = params; - const token = await findTokenByNetworkAndAddress( - donation.transactionNetworkId, - donation.tokenAddress!, - ); - const symbol = token?.symbol; + const isRecurringDonation = donation instanceof RecurringDonation; + let amount: number, + transactionId: string, + transactionNetworkId: number, + toWalletAddress: string | undefined, + donationValueUsd: number | undefined, + donationValueEth: number | undefined, + transakStatus: string | undefined; + if (isRecurringDonation) { + transactionId = donation.txHash; + transactionNetworkId = donation.networkId; + const token = await Token.findOneBy({ + symbol: donation.currency, + networkId: transactionNetworkId, + }); + amount = + (Number(donation.flowRate) / 10 ** (token?.decimals || 18)) * 2628000; // convert flowRate in wei from per second to per month + } else { + amount = Number(donation.amount); + transactionId = donation.transactionId; + transactionNetworkId = donation.transactionNetworkId; + toWalletAddress = donation.toWalletAddress.toLowerCase(); + donationValueUsd = donation.valueUsd; + donationValueEth = donation.valueEth; + transakStatus = donation.transakStatus; + } return { email: user.email, title: project.title, @@ -925,27 +960,30 @@ const getEmailDataDonationAttributes = async (params: { projectOwnerId: project.admin, slug: project.slug, projectLink: `${process.env.WEBSITE_URL}/project/${project.slug}`, - amount: Number(donation.amount), - token: symbol, - transactionId: donation.transactionId.toLowerCase(), - transactionNetworkId: Number(donation.transactionNetworkId), - transactionLink: buildTxLink( - donation.transactionId, - donation.transactionNetworkId, - ), + amount, + isRecurringDonation, + token: donation.currency, + transactionId: transactionId.toLowerCase(), + transactionNetworkId: Number(transactionNetworkId), + transactionLink: buildTxLink(transactionId, transactionNetworkId), currency: donation.currency, - createdAt: new Date(), - toWalletAddress: donation.toWalletAddress.toLowerCase(), - donationValueUsd: donation.valueUsd, - donationValueEth: donation.valueEth, + createdAt: donation.createdAt, + toWalletAddress, + donationValueUsd, + donationValueEth, verified: Boolean(project.verified), - transakStatus: donation.transakStatus, + transakStatus, }; }; const getEmailDataProjectAttributes = async (params: { project: Project }) => { const { project } = params; - const user = await findUserById(project.adminUserId); + let user: User | null; + if (project.adminUser?.email) { + user = project.adminUser; + } else { + user = await findUserById(project.adminUserId); + } return { email: user?.email, title: project.title, @@ -958,6 +996,78 @@ const getEmailDataProjectAttributes = async (params: { project: Project }) => { }; }; +export const getOrttoPersonAttributes = (params: { + firstName?: string; + lastName?: string; + email?: string; + userId?: string; + totalDonated?: number; + donationsCount?: string; + lastDonationDate?: Date | null; + GIVbacksRound?: number; + QFDonor?: string; + QFProjectOwnerAdded?: string; + QFProjectOwnerRemoved?: string; + donationChain?: string; +}): OrttoPerson => { + const { + firstName, + lastName, + email, + userId, + totalDonated, + donationsCount, + lastDonationDate, + GIVbacksRound, + QFDonor, + QFProjectOwnerAdded, + QFProjectOwnerRemoved, + donationChain, + } = params; + const fields = { + 'str::first': firstName || '', + 'str::last': lastName || '', + 'str::email': email || '', + }; + if (process.env.ENVIRONMENT === 'production') { + // On production, we should update Ortto user profile based on user-id to avoid touching real users data + fields['str:cm:user-id'] = userId; + } + if (donationsCount) { + fields['int:cm:number-of-donations'] = Number(donationsCount); + } + if (totalDonated) { + // Ortto automatically adds three decimal points to integers + fields['int:cm:total-donations-value'] = + Number(totalDonated?.toFixed(3)) * 1000; + } + if (lastDonationDate) { + fields['dtz:cm:lastdonationdate'] = lastDonationDate; + } + const tags: string[] = []; + const unsetTags: string[] = []; + if (GIVbacksRound) { + tags.push(`GIVbacks ${GIVbacksRound}`); + } + if (QFDonor) { + tags.push(`QF Donor ${QFDonor}`); + } + if (QFProjectOwnerAdded) { + tags.push(`QF Project Owner ${QFProjectOwnerAdded}`); + } + if (donationChain) { + tags.push(`Donated on ${donationChain}`); + } + if (QFProjectOwnerRemoved) { + unsetTags.push(`QF Project Owner ${QFProjectOwnerRemoved}`); + } + return { + fields, + tags, + unset_tags: unsetTags, + }; +}; + const sendProjectRelatedNotification = async (params: { project: Project; eventName: NOTIFICATIONS_EVENT_NAMES; @@ -976,7 +1086,7 @@ const sendProjectRelatedNotification = async (params: { const projectLink = buildProjectLink(eventName, project.slug); const data: SendNotificationBody = { eventName, - email: receivedUser.email, + email: segment?.payload?.email || receivedUser.email, sendEmail: sendEmail || false, sendSegment: Boolean(segment), userWalletAddress: receivedUser.walletAddress as string, diff --git a/src/adapters/oauth2/discordAdapter.ts b/src/adapters/oauth2/discordAdapter.ts index 80b994393..181affc26 100644 --- a/src/adapters/oauth2/discordAdapter.ts +++ b/src/adapters/oauth2/discordAdapter.ts @@ -1,8 +1,8 @@ +import axios from 'axios'; import { GetUserInfoByOauth2Output, SocialNetworkOauth2AdapterInterface, } from './SocialNetworkOauth2AdapterInterface'; -import axios from 'axios'; import { logger } from '../../utils/logger'; export class DiscordAdapter implements SocialNetworkOauth2AdapterInterface { diff --git a/src/adapters/oauth2/googleAdapter.ts b/src/adapters/oauth2/googleAdapter.ts index 17b3a7a72..87df5305a 100644 --- a/src/adapters/oauth2/googleAdapter.ts +++ b/src/adapters/oauth2/googleAdapter.ts @@ -1,16 +1,12 @@ +import { stringify } from 'querystring'; +import axios from 'axios'; +import { decode, JwtPayload } from 'jsonwebtoken'; import { GetUserInfoByOauth2Output, SocialNetworkOauth2AdapterInterface, } from './SocialNetworkOauth2AdapterInterface'; -import axios from 'axios'; -import { stringify } from 'querystring'; -import { decode, JwtPayload } from 'jsonwebtoken'; import { logger } from '../../utils/logger'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; const clientId = process.env.GOOGLE_CLIENT_ID; const clientSecret = process.env.GOOGLE_CLIENT_SECRET; diff --git a/src/adapters/oauth2/linkedinAdapter.ts b/src/adapters/oauth2/linkedinAdapter.ts index 298e7b97b..d063226c8 100644 --- a/src/adapters/oauth2/linkedinAdapter.ts +++ b/src/adapters/oauth2/linkedinAdapter.ts @@ -1,16 +1,11 @@ +import { stringify } from 'querystring'; +import axios from 'axios'; import { GetUserInfoByOauth2Output, SocialNetworkOauth2AdapterInterface, } from './SocialNetworkOauth2AdapterInterface'; -import { stringify } from 'querystring'; -import axios from 'axios'; -import { decode, JwtPayload } from 'jsonwebtoken'; import { logger } from '../../utils/logger'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; const clientId = process.env.LINKEDIN_CLIENT_ID; const clientSecret = process.env.LINKEDIN_CLIENT_SECRET; diff --git a/src/adapters/oauth2/mockOauth2Adapter.ts b/src/adapters/oauth2/mockOauth2Adapter.ts index 851f18edd..b44992154 100644 --- a/src/adapters/oauth2/mockOauth2Adapter.ts +++ b/src/adapters/oauth2/mockOauth2Adapter.ts @@ -2,24 +2,15 @@ import { GetUserInfoByOauth2Output, SocialNetworkOauth2AdapterInterface, } from './SocialNetworkOauth2AdapterInterface'; -import { findSocialProfileById } from '../../repositories/socialProfileRepository'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../../utils/errorMessages'; -import { logger } from '../../utils/logger'; +import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; export class MockOauth2Adapter implements SocialNetworkOauth2AdapterInterface { - async getAuthUrl(params: { trackId: string }): Promise { + async getAuthUrl(): Promise { // return `${process.env.GIVETH_IO_BACKEND_BASE_URL}/socialProfiles/callback/${socialProfile?.socialNetwork}`; throw new Error(i18n.__(translationErrorMessagesKeys.NOT_IMPLEMENTED)); } - async getUserInfoByOauth2Code(params: { - oauth2Code: string; - state: string; - }): Promise { + async getUserInfoByOauth2Code(): Promise { // const socialProfile = await findSocialProfileById(Number(params.state)); // if (!socialProfile) { // logger.error('getUserInfoByOauth2Code mockAdapter error'); diff --git a/src/adapters/oauth2/twitterAdapter.ts b/src/adapters/oauth2/twitterAdapter.ts index 2d21e2988..ba43e147d 100644 --- a/src/adapters/oauth2/twitterAdapter.ts +++ b/src/adapters/oauth2/twitterAdapter.ts @@ -1,12 +1,11 @@ +import { auth } from 'twitter-api-sdk'; +import { OAuth2User } from 'twitter-api-sdk/dist/OAuth2User'; +import axios from 'axios'; +import { logger } from '../../utils/logger'; import { GetUserInfoByOauth2Output, SocialNetworkOauth2AdapterInterface, } from './SocialNetworkOauth2AdapterInterface'; -import { auth, Client } from 'twitter-api-sdk'; -import { OAuth2User } from 'twitter-api-sdk/dist/OAuth2User'; -import { logger } from '../../utils/logger'; -import { generateRandomString } from '../../utils/utils'; -import axios from 'axios'; export class TwitterAdapter implements SocialNetworkOauth2AdapterInterface { private authClient: OAuth2User; diff --git a/src/adapters/price/CoingeckoPriceAdapter.ts b/src/adapters/price/CoingeckoPriceAdapter.ts index f22f2aaa0..c13925468 100644 --- a/src/adapters/price/CoingeckoPriceAdapter.ts +++ b/src/adapters/price/CoingeckoPriceAdapter.ts @@ -1,9 +1,9 @@ +import axios from 'axios'; import { GetTokenPriceAtDateParams, GetTokenPriceParams, PriceAdapterInterface, } from './PriceAdapterInterface'; -import axios from 'axios'; import { getRedisObject, setObjectInRedis } from '../../redis'; import { logger } from '../../utils/logger'; diff --git a/src/adapters/price/CryptoComparePriceAdapter.ts b/src/adapters/price/CryptoComparePriceAdapter.ts index 185bea307..240e3eba5 100644 --- a/src/adapters/price/CryptoComparePriceAdapter.ts +++ b/src/adapters/price/CryptoComparePriceAdapter.ts @@ -1,10 +1,8 @@ +import axios from 'axios'; import { GetTokenPriceParams, PriceAdapterInterface, } from './PriceAdapterInterface'; -import { CHAIN_ID } from '@giveth/monoswap/dist/src/sdk/sdkFactory'; -import { getMonoSwapTokenPrices } from '../../services/donationService'; -import axios from 'axios'; import { getRedisObject, setObjectInRedis } from '../../redis'; import { logger } from '../../utils/logger'; diff --git a/src/adapters/price/MonoswapPriceAdapter.ts b/src/adapters/price/MonoswapPriceAdapter.ts index d0537b3cb..ff12b202e 100644 --- a/src/adapters/price/MonoswapPriceAdapter.ts +++ b/src/adapters/price/MonoswapPriceAdapter.ts @@ -1,8 +1,8 @@ +import { CHAIN_ID } from '@giveth/monoswap/dist/src/sdk/sdkFactory'; import { GetTokenPriceParams, PriceAdapterInterface, } from './PriceAdapterInterface'; -import { CHAIN_ID } from '@giveth/monoswap/dist/src/sdk/sdkFactory'; import { getMonoSwapTokenPrices } from '../../services/donationService'; import { logger } from '../../utils/logger'; diff --git a/src/adapters/superFluid/superFluidAdapter.ts b/src/adapters/superFluid/superFluidAdapter.ts new file mode 100644 index 000000000..53c8b0f62 --- /dev/null +++ b/src/adapters/superFluid/superFluidAdapter.ts @@ -0,0 +1,236 @@ +import axios from 'axios'; +import { logger } from '../../utils/logger'; +import { isProduction } from '../../utils/utils'; +import { + FlowUpdatedEvent, + SuperFluidAdapterInterface, +} from './superFluidAdapterInterface'; + +const superFluidGraphqlUrl = + 'https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-optimism-mainnet'; +const superFluidGraphqlStagingUrl = + 'https://optimism-sepolia.subgraph.x.superfluid.dev'; + +const subgraphUrl = isProduction + ? superFluidGraphqlUrl + : superFluidGraphqlStagingUrl; + +// Define your GraphQL query as a string and prepare your variables +const accountQuery = ` + query getAccountBalances($id: ID!) { + account(id: $id) { + id + accountTokenSnapshots { + token { + id + name + symbol + } + maybeCriticalAtTimestamp + } + } + } +`; + +const getFlowsQuery = ` + query FlowUpdatedEvents($where: FlowUpdatedEvent_filter) { + flowUpdatedEvents(where: $where) { + id + flowOperator + flowRate + transactionHash + receiver + sender + token + timestamp + } + } +`; + +/* EXAMPLE PAYLOAD + { + "id": "0x8c3bf3eb2639b2326ff937d041292da2e79adbbf-0xd964ab7e202bab8fbaa28d5ca2b2269a5497cf68-0x1305f6b6df9dc47159d12eb7ac2804d4a33173c2-0.0-0.0", + "flowRate": "462962962962962", + "startedAtTimestamp": "1617118948", + "startedAtBlockNumber": "12658248", + "stoppedAtTimestamp": "1626702963", + "stoppedAtBlockNumber": "17035432", + "totalAmountStreamed": "4437043981481472252430", + "chainId": 137, + "token": { + "id": "0x1305f6b6df9dc47159d12eb7ac2804d4a33173c2", + "symbol": "DAIx", + "name": "Super DAI (PoS)", + "underlyingAddress": "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063" + }, + "sender": "0x8c3bf3eb2639b2326ff937d041292da2e79adbbf", + "receiver": "0xd964ab7e202bab8fbaa28d5ca2b2269a5497cf68", + "startedAtEvent": "0x241d2db890d58d2d9980ad214580c4f3ea22021e2b8dd89387a6257fceebef9d", + "stoppedAtEvent": "0x4d8e9edec495fdcdeece9061267f7eef1a96923378c8940ca5ab09439d08d2fd", + "virtualPeriods": [ + { + "startTime": 1617249600, + "endTime": 1619827199, + "amount": "-1193332870370367888200", + "amountFiat": "-1193.9934002402638654" + }, + { + "startTime": 1619827200, + "endTime": 1622505599, + "amount": "-1239999537037034457800", + "amountFiat": "-1241.1103421307001618" + }, + { + "startTime": 1622505600, + "endTime": 1625097599, + "amount": "-1199999537037034541000", + "amountFiat": "-1202.7360631768932664" + }, + { + "startTime": 1625097600, + "endTime": 1626702963, + "amount": "-743223611111109565210", + "amountFiat": "-745.57117099624968902" + } + ] + }, +*/ +export class SuperFluidAdapter implements SuperFluidAdapterInterface { + async streamPeriods(params: { + address: string; + chain: number; + start: number; + end: number; + priceGranularity: string; + virtualization: string; + currency: string; + recurringDonationTxHash: string; + }) { + const { + address, + chain, + start, + end, + priceGranularity, + virtualization, + currency, + recurringDonationTxHash, + } = params; + try { + const response = await axios.get( + 'https://accounting.superfluid.dev/v1/stream-periods', + { + params: { + addresses: address, + chains: chain, + start, + end, + priceGranularity, + virtualization, + currency, + }, + }, + ); + // Fetch the stream table with the recurringDonation TxHash + const filteredData = response.data.filter(streamTable => + streamTable.startedAtEvent + .toLowerCase() + .includes(recurringDonationTxHash.toLowerCase()), + ); + return filteredData[0]; + } catch (e) { + logger.error(e); + } + } + + /* RESPONSE + { + "data": { + "account": { + "id": "0x0000000000000000000000000000000000000000", + "accountTokenSnapshots": [ + { + "token": { + "id": "0x01c45fab099f8cda5621d3d97e0978df65706090", + "name": "RedCoin" + }, + "maybeCriticalAtTimestamp": null + }, + { + "token": { + "id": "0x0942570634a80bcd096873afc9b112a900492fd7", + "name": "REX Shirt" + }, + "maybeCriticalAtTimestamp": null + } + ] + } + }, + */ + + // Optimism works + async accountBalance(accountId: string) { + try { + const response = await axios.post(subgraphUrl, { + query: accountQuery, + variables: { + id: accountId?.toLowerCase(), + }, + }); + + return response.data.data.account?.accountTokenSnapshots; + } catch (e) { + logger.error(e); + } + } + + async getFlowByTxHash(params: { + receiver: string; + sender: string; + flowRate: string; + transactionHash: string; + }): Promise { + try { + const response = await axios.post(subgraphUrl, { + query: getFlowsQuery, + variables: { + where: params, + orderBy: 'timestamp', + orderDirection: 'asc', + }, + }); + const flowUpdates = response.data?.data + ?.flowUpdatedEvents as FlowUpdatedEvent[]; + return flowUpdates?.[0]; + } catch (e) { + logger.error('getFlowByReceiverSenderFlowRate error', e); + throw e; + } + } + + async getFlowByReceiverSenderFlowRate(params: { + receiver: string; + sender: string; + flowRate: string; + timestamp_gt: number; + }): Promise { + try { + logger.debug('getFlowByReceiverSenderFlowRate has been called', params); + + const response = await axios.post(subgraphUrl, { + query: getFlowsQuery, + variables: { + where: params, + orderBy: 'timestamp', + orderDirection: 'asc', + }, + }); + const flowUpdates = response.data?.data + ?.flowUpdatedEvents as FlowUpdatedEvent[]; + return flowUpdates?.[0]; + } catch (e) { + logger.error('getFlowByReceiverSenderFlowRate error', e); + throw e; + } + } +} diff --git a/src/adapters/superFluid/superFluidAdapterInterface.ts b/src/adapters/superFluid/superFluidAdapterInterface.ts new file mode 100644 index 000000000..569059bcd --- /dev/null +++ b/src/adapters/superFluid/superFluidAdapterInterface.ts @@ -0,0 +1,36 @@ +export interface FlowUpdatedEvent { + id: string; + flowOperator: string; + flowRate: string; + transactionHash: string; + receiver: string; + sender: string; + token: string; + timestamp: string; +} + +export interface SuperFluidAdapterInterface { + streamPeriods(params: { + address: string; + chain: number; + start: number; + end: number; + priceGranularity: string; + virtualization: string; + currency: string; + recurringDonationTxHash: string; + }): Promise; + accountBalance(accountId: string): Promise; + getFlowByTxHash(params: { + receiver: string; + sender: string; + flowRate: string; + transactionHash: string; + }): Promise; + getFlowByReceiverSenderFlowRate(params: { + receiver: string; + sender: string; + flowRate: string; + timestamp_gt: number; + }): Promise; +} diff --git a/src/adapters/superFluid/superFluidMockAdapter.ts b/src/adapters/superFluid/superFluidMockAdapter.ts new file mode 100644 index 000000000..bd99190f8 --- /dev/null +++ b/src/adapters/superFluid/superFluidMockAdapter.ts @@ -0,0 +1,99 @@ +import { + FlowUpdatedEvent, + SuperFluidAdapterInterface, +} from './superFluidAdapterInterface'; + +export class SuperFluidMockAdapter implements SuperFluidAdapterInterface { + async streamPeriods() { + return { + id: '0x8c3bf3eb2639b2326ff937d041292da2e79adbbf-0xd964ab7e202bab8fbaa28d5ca2b2269a5497cf68-0x1305f6b6df9dc47159d12eb7ac2804d4a33173c2-0.0-0.0', + flowRate: '462962962962962', + startedAtTimestamp: '1617118948', + startedAtBlockNumber: '12658248', + stoppedAtTimestamp: '1626702963', + stoppedAtBlockNumber: '17035432', + totalAmountStreamed: '4437043981481472252430', + chainId: 137, + token: { + id: '0x1305f6b6df9dc47159d12eb7ac2804d4a33173c2', + symbol: 'DAIx', + name: 'Super DAI (PoS)', + underlyingAddress: '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', + }, + sender: '0x8c3bf3eb2639b2326ff937d041292da2e79adbbf', + receiver: '0xd964ab7e202bab8fbaa28d5ca2b2269a5497cf68', + startedAtEvent: + '0x241d2db890d58d2d9980ad214580c4f3ea22021e2b8dd89387a6257fceebef9d', + stoppedAtEvent: + '0x4d8e9edec495fdcdeece9061267f7eef1a96923378c8940ca5ab09439d08d2fd', + virtualPeriods: [ + { + startTime: 1617249600, + endTime: 1619827199, + amount: '-1193332870370367888200', + amountFiat: '-1193.9934002402638654', + }, + { + startTime: 1619827200, + endTime: 1622505599, + amount: '-1239999537037034457800', + amountFiat: '-1241.1103421307001618', + }, + { + startTime: 1622505600, + endTime: 1625097599, + amount: '-1199999537037034541000', + amountFiat: '-1202.7360631768932664', + }, + { + startTime: 1625097600, + endTime: 1626702963, + amount: '-743223611111109565210', + amountFiat: '-745.57117099624968902', + }, + ], + }; + } + + async accountBalance() { + return { + id: '0x0000000000000000000000000000000000000000', + accountTokenSnapshots: [ + { + token: { + id: '0x01c45fab099f8cda5621d3d97e0978df65706090', + name: 'ETHx', + symbol: 'ETHx', + }, + maybeCriticalAtTimestamp: 1738525894, + }, + { + token: { + id: '0x0942570634a80bcd096873afc9b112a900492fd7', + name: 'Daix', + symbol: 'Daix', + }, + maybeCriticalAtTimestamp: 1738525894, + }, + ], + }; + } + + getFlowByReceiverSenderFlowRate(_params: { + receiver: string; + sender: string; + flowRate: string; + timestamp_gt: number; + }): Promise { + return Promise.resolve(undefined); + } + + getFlowByTxHash(_params: { + receiver: string; + sender: string; + flowRate: string; + transactionHash: string; + }): Promise { + return Promise.resolve(undefined); + } +} diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index 742991cb9..62478d0e9 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -16,7 +16,6 @@ export enum NOTIFICATIONS_EVENT_NAMES { PROJECT_VERIFIED_USERS_WHO_SUPPORT = 'Project verified - Users who supported', // https://github.com/Giveth/impact-graph/issues/624#issuecomment-1240364389 - PROJECT_REJECTED = 'Project unverified', PROJECT_NOT_REVIEWED = 'Project not reviewed', PROJECT_UNVERIFIED = 'Project unverified', VERIFICATION_FORM_REJECTED = 'Form rejected', @@ -44,4 +43,7 @@ export enum NOTIFICATIONS_EVENT_NAMES { PROJECT_HAS_RISEN_IN_THE_RANK = 'Your Project has risen in the rank', PROJECT_HAS_A_NEW_RANK = 'Your project has a new rank', YOUR_PROJECT_GOT_A_RANK = 'Your project got a rank', + SUPER_TOKENS_BALANCE_WEEK = 'One week left in stream balance', + SUPER_TOKENS_BALANCE_MONTH = 'One month left in stream balance', + SUPER_TOKENS_BALANCE_DEPLETED = 'Stream balance depleted', } diff --git a/src/auth/userCheck.ts b/src/auth/userCheck.ts index 927ba21c0..296d9a1ab 100644 --- a/src/auth/userCheck.ts +++ b/src/auth/userCheck.ts @@ -1,13 +1,10 @@ import { AuthChecker } from 'type-graphql'; import { Context } from '../context'; -export const userCheck: AuthChecker = ( - { root, args, context, info }, - roles, -) => { +export const userCheck: AuthChecker = () => { // here we can read the user from context // and check his permission in the db against the `roles` argument - // that comes from the `@Authorized` decorator, eg. ["ADMIN", "MODERATOR"] + // that comes from the `@Authorized` decorator, e.g. ["ADMIN", "MODERATOR"] return true; // or false if access is denied }; diff --git a/src/config.ts b/src/config.ts index 247960355..386b11a33 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,7 +10,7 @@ const loadConfigResult = dotenv.config({ }); if (loadConfigResult.error) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('Load process.env error', { path: configPath, error: loadConfigResult.error, @@ -31,7 +31,6 @@ const envVars = [ 'DROP_DATABASE', 'SEED_PASSWORD', 'APOLLO_KEY', - 'REGISTER_USERNAME_PASSWORD', 'STRIPE_KEY', 'STRIPE_SECRET', 'STRIPE_APPLICATION_FEE', @@ -59,7 +58,6 @@ const envVars = [ 'DONATION_VERIFICAITON_EXPIRATION_HOURS', ]; -// tslint:disable-next-line:class-name interface requiredEnv { JWT_SECRET: string; JWT_MAX_AGE: string; @@ -74,7 +72,6 @@ interface requiredEnv { DROP_DATABASE: string; SEED_PASSWORD: string; APOLLO_KEY: string; - REGISTER_USERNAME_PASSWORD: string; STRIPE_KEY: string; STRIPE_SECRET: string; diff --git a/src/constants/redisPrefixes.ts b/src/constants/redisPrefixes.ts deleted file mode 100644 index b8dc5b277..000000000 --- a/src/constants/redisPrefixes.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const confirmUserPrefix = 'user-confirmation:'; -export const forgotPasswordPrefix = 'forgot-password:'; diff --git a/src/entities/Country.ts b/src/entities/Country.ts index 93bb974c2..10268e895 100644 --- a/src/entities/Country.ts +++ b/src/entities/Country.ts @@ -1,4 +1,4 @@ -import { Field, ID, ObjectType } from 'type-graphql'; +import { Field, ObjectType } from 'type-graphql'; @ObjectType() export class Country { diff --git a/src/entities/CronJob.ts b/src/entities/CronJob.ts index 2f5c2d00e..e3743a558 100644 --- a/src/entities/CronJob.ts +++ b/src/entities/CronJob.ts @@ -1,4 +1,4 @@ -import { Field, ID, ObjectType, Int, Float } from 'type-graphql'; +import { Field, ID, ObjectType } from 'type-graphql'; import { PrimaryGeneratedColumn, Column, Entity, BaseEntity } from 'typeorm'; @ObjectType() @@ -7,7 +7,7 @@ import { PrimaryGeneratedColumn, Column, Entity, BaseEntity } from 'typeorm'; }) // Postgres cron jobs export class CronJob extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn({ name: 'jobid' }) readonly id: number; diff --git a/src/entities/ProjectActualMatchingView.ts b/src/entities/ProjectActualMatchingView.ts index b63f68538..d6c1bee50 100644 --- a/src/entities/ProjectActualMatchingView.ts +++ b/src/entities/ProjectActualMatchingView.ts @@ -1,24 +1,20 @@ import { Field, ObjectType } from 'type-graphql'; import { - Entity, Column, - Index, PrimaryColumn, BaseEntity, ViewEntity, ManyToOne, - RelationId, ViewColumn, JoinColumn, } from 'typeorm'; import { Project } from './project'; -import { string } from 'joi'; @ViewEntity('project_actual_matching_view', { synchronize: false }) @ObjectType() export class ProjectActualMatchingView extends BaseEntity { - @Field(type => Project) - @ManyToOne(type => Project, project => project.projectEstimatedMatchingView) + @Field(_type => Project) + @ManyToOne(_type => Project, project => project.projectEstimatedMatchingView) @JoinColumn({ referencedColumnName: 'id' }) project: Project; @@ -33,6 +29,10 @@ export class ProjectActualMatchingView extends BaseEntity { @PrimaryColumn() qfRoundId: number; + @ViewColumn() + @Column({ nullable: true }) + email?: string; + // Sum of the square root of the value in USD of the donations @ViewColumn() @Column('double precision') diff --git a/src/entities/ProjectEstimatedMatchingView.ts b/src/entities/ProjectEstimatedMatchingView.ts index 518e70ce9..7714df0ec 100644 --- a/src/entities/ProjectEstimatedMatchingView.ts +++ b/src/entities/ProjectEstimatedMatchingView.ts @@ -1,13 +1,11 @@ import { Field, ObjectType } from 'type-graphql'; import { - Entity, Column, Index, PrimaryColumn, BaseEntity, ViewEntity, ManyToOne, - RelationId, ViewColumn, JoinColumn, } from 'typeorm'; @@ -28,8 +26,8 @@ import { Project } from './project'; ]) @ObjectType() export class ProjectEstimatedMatchingView extends BaseEntity { - @Field(type => Project) - @ManyToOne(type => Project, project => project.projectEstimatedMatchingView) + @Field(_type => Project) + @ManyToOne(_type => Project, project => project.projectEstimatedMatchingView) @JoinColumn({ referencedColumnName: 'id' }) project: Project; diff --git a/src/entities/accountVerification.ts b/src/entities/accountVerification.ts index 3e3acc0dd..f3ff44eb3 100644 --- a/src/entities/accountVerification.ts +++ b/src/entities/accountVerification.ts @@ -5,7 +5,6 @@ import { Entity, BaseEntity, ManyToOne, - ColumnOptions, RelationId, Index, CreateDateColumn, @@ -16,7 +15,7 @@ import { User } from './user'; @Entity() @ObjectType() export class AccountVerification extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; @@ -42,8 +41,8 @@ export class AccountVerification extends BaseEntity { attestation?: string; @Index() - @Field(type => User) - @ManyToOne(type => User, { eager: true }) + @Field(_type => User) + @ManyToOne(_type => User, { eager: true }) user: User; @RelationId( diff --git a/src/entities/anchorContractAddress.ts b/src/entities/anchorContractAddress.ts index 562b1f6ed..68484cc5d 100644 --- a/src/entities/anchorContractAddress.ts +++ b/src/entities/anchorContractAddress.ts @@ -18,7 +18,7 @@ import { User } from './user'; @ObjectType() @Unique(['address', 'networkId', 'project']) export class AnchorContractAddress extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @@ -41,8 +41,8 @@ export class AnchorContractAddress extends BaseEntity { txHash: string; @Index() - @Field(type => Project) - @ManyToOne(type => Project) + @Field(_type => Project) + @ManyToOne(_type => Project) project: Project; @RelationId((relatedAddress: AnchorContractAddress) => relatedAddress.project) @@ -50,8 +50,8 @@ export class AnchorContractAddress extends BaseEntity { projectId: number; @Index() - @Field(type => User, { nullable: true }) - @ManyToOne(type => User, { eager: true, nullable: true }) + @Field(_type => User, { nullable: true }) + @ManyToOne(_type => User, { eager: true, nullable: true }) creator: User; @RelationId( @@ -62,8 +62,8 @@ export class AnchorContractAddress extends BaseEntity { creatorId: number; @Index() - @Field(type => User, { nullable: true }) - @ManyToOne(type => User, { eager: true, nullable: true }) + @Field(_type => User, { nullable: true }) + @ManyToOne(_type => User, { eager: true, nullable: true }) owner: User; @RelationId( diff --git a/src/entities/bankAccount.ts b/src/entities/bankAccount.ts index 58dbd794d..ce1aaa4d6 100644 --- a/src/entities/bankAccount.ts +++ b/src/entities/bankAccount.ts @@ -1,16 +1,10 @@ import { Field, Float, ID, ObjectType } from 'type-graphql'; -import { - BaseEntity, - Column, - Entity, - OneToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @ObjectType() @Entity() export class BankAccount extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @@ -55,7 +49,7 @@ export class BankAccount extends BaseEntity { @ObjectType() @Entity() export class StripeTransaction extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @@ -88,7 +82,7 @@ export class StripeTransaction extends BaseEntity { createdAt: Date; @Column({ type: 'float', nullable: true }) - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) amount: number; @Column({ nullable: true }) diff --git a/src/entities/broadcastNotification.ts b/src/entities/broadcastNotification.ts index e17882a76..1c046943b 100644 --- a/src/entities/broadcastNotification.ts +++ b/src/entities/broadcastNotification.ts @@ -1,10 +1,9 @@ -import { ObjectType, Field, ID } from 'type-graphql'; +import { Field } from 'type-graphql'; import { BaseEntity, Column, CreateDateColumn, Entity, - Index, ManyToOne, PrimaryGeneratedColumn, RelationId, @@ -31,8 +30,8 @@ export default class BroadcastNotification extends BaseEntity { @Column() title: string; - @Field(type => User, { nullable: true }) - @ManyToOne(type => User, { eager: true, nullable: true }) + @Field(_type => User, { nullable: true }) + @ManyToOne(_type => User, { eager: true, nullable: true }) adminUser: User; @RelationId( (broadcastNotification: BroadcastNotification) => diff --git a/src/entities/campaign.ts b/src/entities/campaign.ts index 7e1860e10..744d48d02 100644 --- a/src/entities/campaign.ts +++ b/src/entities/campaign.ts @@ -4,16 +4,10 @@ import { Column, Entity, BaseEntity, - ManyToMany, UpdateDateColumn, CreateDateColumn, - JoinTable, - Index, - ManyToOne, - RelationId, } from 'typeorm'; import { Project } from './project'; -import { User } from './user'; // Copied from projects enums export enum CampaignSortingField { @@ -61,7 +55,7 @@ export enum CampaignType { @Entity() @ObjectType() export class Campaign extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; @@ -73,7 +67,7 @@ export class Campaign extends BaseEntity { @Column('text', { nullable: false }) title: string; - @Field(type => String) + @Field(_type => String) @Column({ type: 'enum', enum: CampaignType, @@ -96,11 +90,11 @@ export class Campaign extends BaseEntity { @Column('text', { nullable: false }) description: string; - @Field(type => [String], { nullable: true }) + @Field(_type => [String], { nullable: true }) @Column('text', { nullable: true, array: true }) hashtags: string[]; - @Field(type => [String], { nullable: true }) + @Field(_type => [String], { nullable: true }) @Column('text', { nullable: true, array: true }) relatedProjectsSlugs: string[]; @@ -119,7 +113,7 @@ export class Campaign extends BaseEntity { // ipfs link videoPreview?: string; - @Field(type => [Project], { nullable: true }) + @Field(_type => [Project], { nullable: true }) relatedProjects: Project[]; @Field({ nullable: true }) @@ -133,7 +127,7 @@ export class Campaign extends BaseEntity { @Column({ nullable: true }) landingLink: string; - @Field(type => [String], { nullable: true }) + @Field(_type => [String], { nullable: true }) @Column({ type: 'enum', enum: CampaignFilterField, @@ -142,7 +136,7 @@ export class Campaign extends BaseEntity { }) filterFields: CampaignFilterField[]; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ type: 'enum', enum: CampaignSortingField, diff --git a/src/entities/category.ts b/src/entities/category.ts index 52873a73d..82e171bc0 100644 --- a/src/entities/category.ts +++ b/src/entities/category.ts @@ -4,7 +4,6 @@ import { Column, Entity, BaseEntity, - Index, ManyToMany, ManyToOne, RelationId, @@ -20,7 +19,7 @@ export const CATEGORY_NAMES = { @Entity() @ObjectType() export class Category extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; @@ -36,7 +35,7 @@ export class Category extends BaseEntity { @Column({ nullable: true }) source: string; - @ManyToMany(type => Project, project => project.categories) + @ManyToMany(_type => Project, project => project.categories) projects: Project[]; @Field(_ => MainCategory, { nullable: true }) diff --git a/src/entities/donation.ts b/src/entities/donation.ts index c99248834..631811783 100644 --- a/src/entities/donation.ts +++ b/src/entities/donation.ts @@ -7,9 +7,6 @@ import { ManyToOne, RelationId, Index, - Unique, - Brackets, - JoinTable, } from 'typeorm'; import { Project } from './project'; import { User } from './user'; @@ -26,6 +23,7 @@ export const DONATION_STATUS = { export const DONATION_ORIGINS = { IDRISS_TWITTER: 'Idriss', DRAFT_DONATION_MATCHING: 'DraftDonationMatching', + SUPER_FLUID: 'SuperFluid', }; export const DONATION_TYPES = { @@ -46,7 +44,7 @@ export enum SortField { @Entity() @ObjectType() export class Donation extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; @@ -68,7 +66,7 @@ export class Donation extends BaseEntity { @Column({ nullable: true }) safeTransactionId?: string; - @Field(type => String) + @Field(_type => String) @Column({ type: 'enum', enum: ChainType, @@ -85,11 +83,11 @@ export class Donation extends BaseEntity { @Column('text', { default: DONATION_STATUS.PENDING }) status: string; - @Field(type => Boolean) + @Field(_type => Boolean) @Column({ type: 'boolean', default: false }) isExternal: boolean; - @Field(type => Int) + @Field(_type => Int) @Column('integer', { nullable: true }) blockNumber?: number; @@ -170,8 +168,8 @@ export class Donation extends BaseEntity { bottomRankInRound?: number; @Index() - @Field(type => Project) - @ManyToOne(type => Project, { eager: true }) + @Field(_type => Project) + @ManyToOne(_type => Project, { eager: true }) project: Project; @RelationId((donation: Donation) => donation.project) @@ -179,8 +177,8 @@ export class Donation extends BaseEntity { projectId: number; @Index() - @Field(type => QfRound, { nullable: true }) - @ManyToOne(type => QfRound, { eager: true }) + @Field(_type => QfRound, { nullable: true }) + @ManyToOne(_type => QfRound, { eager: true }) qfRound: QfRound; @RelationId((donation: Donation) => donation.qfRound) @@ -188,8 +186,8 @@ export class Donation extends BaseEntity { qfRoundId: number; @Index() - @Field(type => QfRound, { nullable: true }) - @ManyToOne(type => QfRound, { eager: true }) + @Field(_type => QfRound, { nullable: true }) + @ManyToOne(_type => QfRound, { eager: true }) distributedFundQfRound: QfRound; @RelationId((donation: Donation) => donation.distributedFundQfRound) @@ -197,78 +195,90 @@ export class Donation extends BaseEntity { distributedFundQfRoundId: number; @Index() - @Field(type => User, { nullable: true }) - @ManyToOne(type => User, { eager: true, nullable: true }) + @Field(_type => User, { nullable: true }) + @ManyToOne(_type => User, { eager: true, nullable: true }) user?: User; + @RelationId((donation: Donation) => donation.user) @Column({ nullable: true }) userId: number; @Index() - @Field(type => RecurringDonation, { nullable: true }) - @ManyToOne(type => RecurringDonation, { eager: true, nullable: true }) + @Field(_type => RecurringDonation, { nullable: true }) + @ManyToOne(_type => RecurringDonation, { eager: true, nullable: true }) recurringDonation?: RecurringDonation; + @RelationId((donation: Donation) => donation.recurringDonation) @Column({ nullable: true }) recurringDonationId: number; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column('text', { nullable: true }) contactEmail?: string | null; - @Field(type => Number, { nullable: true }) + @Field(_type => Number, { nullable: true }) @Column({ nullable: true }) qfRoundUserScore?: number; @Index() - @Field(type => Date) + @Field(_type => Date) @Column() createdAt: Date; - @Field(type => Date, { nullable: true }) + @Field(_type => Date, { nullable: true }) @Column({ nullable: true }) importDate: Date; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) donationType?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) onramperTransactionStatus?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) onramperId?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) referrerWallet?: string; - @Field(type => Date, { nullable: true }) + @Field(_type => Date, { nullable: true }) @Column({ nullable: true }) referralStartTimestamp?: Date; - @Field(type => Boolean, { nullable: false }) + @Field(_type => Boolean, { nullable: false }) @Column({ nullable: false, default: false }) isReferrerGivbackEligible: boolean; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) transakStatus?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) transakTransactionLink?: string; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) @Column({ nullable: true, default: false }) segmentNotified: boolean; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) @Column({ nullable: true, default: false }) isTokenEligibleForGivback: boolean; + @Field({ nullable: true }) + @Column('integer', { nullable: true }) + // To match the superFluid Virtual Period + virtualPeriodStart?: number; + + @Field({ nullable: true }) + @Column('integer', { nullable: true }) + // To match the superFluid Virtual Period + virtualPeriodEnd?: number; + static async findXdaiGivDonationsWithoutPrice() { return this.createQueryBuilder('donation') .where(`donation.currency = 'GIV' AND donation."valueUsd" IS NULL `) diff --git a/src/entities/draftDonation.ts b/src/entities/draftDonation.ts index 29c7b392d..b5cb2e73e 100644 --- a/src/entities/draftDonation.ts +++ b/src/entities/draftDonation.ts @@ -1,4 +1,4 @@ -import { Field, ID, Int, ObjectType } from 'type-graphql'; +import { Field, ID, ObjectType } from 'type-graphql'; import { PrimaryGeneratedColumn, Column, @@ -26,7 +26,7 @@ export const DRAFT_DONATION_STATUS = { }, ) export class DraftDonation extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; @@ -39,7 +39,7 @@ export class DraftDonation extends BaseEntity { // @Column({ nullable: true }) // safeTransactionId?: string; - @Field(type => String) + @Field(_type => String) @Column({ type: 'enum', enum: ChainType, @@ -90,17 +90,17 @@ export class DraftDonation extends BaseEntity { userId: number; @Index() - @Field(type => Date) + @Field(_type => Date) @CreateDateColumn() createdAt: Date; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) referrerId?: string; // Expected call data used only for matching ERC20 transfers // Is calculated and saved once during the matching time, and will be used in next iterations - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) expectedCallData?: string; diff --git a/src/entities/draftRecurringDonation.ts b/src/entities/draftRecurringDonation.ts new file mode 100644 index 000000000..f96140198 --- /dev/null +++ b/src/entities/draftRecurringDonation.ts @@ -0,0 +1,98 @@ +import { Field, ID, ObjectType } from 'type-graphql'; +import { + PrimaryGeneratedColumn, + Column, + Entity, + BaseEntity, + Index, + CreateDateColumn, +} from 'typeorm'; +import { ChainType } from '../types/network'; + +export const DRAFT_RECURRING_DONATION_STATUS = { + PENDING: 'pending', + MATCHED: 'matched', + FAILED: 'failed', +}; + +export const RECURRING_DONATION_ORIGINS = { + DRAFT_RECURRING_DONATION_MATCHING: 'DraftRecurringDonationMatching', +}; + +@Entity() +@ObjectType() +// To mark the draft recurring donation as matched, when the recurringDonation is created in RecurringDonationResolver +export class DraftRecurringDonation extends BaseEntity { + @Field(_type => ID) + @PrimaryGeneratedColumn() + id: number; + + @Field() + @Column({ nullable: false }) + networkId: number; + + @Field() + @Column({ nullable: false }) + flowRate: string; + + @Field(_type => String) + @Column({ + type: 'enum', + enum: ChainType, + default: ChainType.EVM, + }) + chainType: ChainType; + + @Index() + @Field() + @Column({ nullable: false }) + currency: string; + + @Column({ nullable: true, default: false }) + @Field({ nullable: true }) + isBatch: boolean; + + @Column({ nullable: true, default: false }) + @Field({ nullable: true }) + anonymous: boolean; + + @Column({ nullable: true, default: false }) + @Field({ nullable: true }) + // When creating a draft recurring donation, the user can choose to update an existing recurring donation + // This flag is used to determine if the draft recurring donation is for update + isForUpdate: boolean; + + @Field() + @Column({ nullable: true }) + projectId: number; + + @Field() + @Column({ nullable: true }) + @Index({ where: `status = '${DRAFT_RECURRING_DONATION_STATUS.PENDING}'` }) + donorId: number; + + @Field() + @Column({ + type: 'enum', + enum: DRAFT_RECURRING_DONATION_STATUS, + default: DRAFT_RECURRING_DONATION_STATUS.PENDING, + }) + @Index({ where: `status = '${DRAFT_RECURRING_DONATION_STATUS.PENDING}'` }) + status: string; + @Field() + @Column({ nullable: true }) + matchedRecurringDonationId?: number; + + @Field({ nullable: true }) + @Column('text', { nullable: true }) + origin: string; + + @Field({ nullable: true }) + @Column({ nullable: true }) + errorMessage?: string; + + @Index() + @Field(_type => Date) + @CreateDateColumn() + createdAt: Date; +} diff --git a/src/entities/entities.ts b/src/entities/entities.ts index 4a0a5800b..224f41a90 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -1,3 +1,4 @@ +import { DataSourceOptions } from 'typeorm'; import { Organization } from './organization'; import { Category } from './category'; import { Token } from './token'; @@ -27,7 +28,6 @@ import { PowerSnapshotHistory } from './powerSnapshotHistory'; import { PowerBalanceSnapshotHistory } from './powerBalanceSnapshotHistory'; import { PowerBoostingSnapshotHistory } from './powerBoostingSnapshotHistory'; import { LastSnapshotProjectPowerView } from '../views/lastSnapshotProjectPowerView'; -import { DataSourceOptions } from 'typeorm'; import { User } from './user'; import { Project, ProjectUpdate } from './project'; import { Reaction } from './reaction'; @@ -48,6 +48,8 @@ import { Sybil } from './sybil'; import { DraftDonation } from './draftDonation'; import { ProjectFraud } from './projectFraud'; import { ProjectActualMatchingView } from './ProjectActualMatchingView'; +import { ProjectSocialMedia } from './projectSocialMedia'; +import { DraftRecurringDonation } from './draftRecurringDonation'; export const getEntities = (): DataSourceOptions['entities'] => { return [ @@ -74,6 +76,7 @@ export const getEntities = (): DataSourceOptions['entities'] => { ThirdPartyProjectImport, ProjectVerificationForm, ProjectAddress, + ProjectSocialMedia, SocialProfile, MainCategory, PowerBoosting, @@ -112,5 +115,6 @@ export const getEntities = (): DataSourceOptions['entities'] => { AnchorContractAddress, RecurringDonation, + DraftRecurringDonation, ]; }; diff --git a/src/entities/featuredUpdate.ts b/src/entities/featuredUpdate.ts index f5b4b3348..5b815d464 100644 --- a/src/entities/featuredUpdate.ts +++ b/src/entities/featuredUpdate.ts @@ -17,13 +17,13 @@ import { Project, ProjectUpdate } from './project'; @Entity() @ObjectType() export class FeaturedUpdate extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @Index() - @Field(type => Project) - @OneToOne(type => Project) + @Field(_type => Project) + @OneToOne(_type => Project) @JoinColumn() project: Project; @@ -32,8 +32,8 @@ export class FeaturedUpdate extends BaseEntity { projectId: number; @Index() - @Field(type => ProjectUpdate) - @OneToOne(type => ProjectUpdate) + @Field(_type => ProjectUpdate) + @OneToOne(_type => ProjectUpdate) @JoinColumn() projectUpdate: ProjectUpdate; @@ -41,7 +41,7 @@ export class FeaturedUpdate extends BaseEntity { @Column({ nullable: true }) projectUpdateId: number; - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) @Column({ type: 'integer', nullable: true }) position: number; diff --git a/src/entities/instantPowerBalance.ts b/src/entities/instantPowerBalance.ts index be07459e5..b6b5090bb 100644 --- a/src/entities/instantPowerBalance.ts +++ b/src/entities/instantPowerBalance.ts @@ -10,11 +10,11 @@ import { @Entity() @ObjectType() export class InstantPowerBalance extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; - @Field(type => ID) + @Field(_type => ID) @Column() @Index({ unique: true }) userId: number; diff --git a/src/entities/instantPowerFetchState.ts b/src/entities/instantPowerFetchState.ts index e5e637541..31d4e0b5c 100644 --- a/src/entities/instantPowerFetchState.ts +++ b/src/entities/instantPowerFetchState.ts @@ -6,7 +6,7 @@ import { ColumnBigIntTransformer } from '../utils/entities'; @ObjectType() @Check('"id"') export class InstantPowerFetchState extends BaseEntity { - @Field(type => Boolean) + @Field(_type => Boolean) @PrimaryColumn() id: boolean; diff --git a/src/entities/mainCategory.ts b/src/entities/mainCategory.ts index bfb3dcbc5..35bbf6529 100644 --- a/src/entities/mainCategory.ts +++ b/src/entities/mainCategory.ts @@ -11,7 +11,7 @@ import { Category } from './category'; @Entity() @ObjectType() export class MainCategory extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; @@ -36,7 +36,7 @@ export class MainCategory extends BaseEntity { @Column({ default: true }) isActive: boolean; - @Field(type => [Category], { nullable: true }) - @OneToMany(type => Category, category => category.mainCategory) + @Field(_type => [Category], { nullable: true }) + @OneToMany(_type => Category, category => category.mainCategory) categories?: Category[]; } diff --git a/src/entities/organization.ts b/src/entities/organization.ts index 0e34a2b13..2596179a1 100644 --- a/src/entities/organization.ts +++ b/src/entities/organization.ts @@ -14,7 +14,7 @@ import { Token } from './token'; @Entity() @ObjectType() export class Organization extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; @@ -35,12 +35,12 @@ export class Organization extends BaseEntity { @Column('boolean', { nullable: true, default: false }) supportCustomTokens?: boolean; - @Field(type => [Project], { nullable: true }) - @OneToMany(type => Project, project => project.organization) + @Field(_type => [Project], { nullable: true }) + @OneToMany(_type => Project, project => project.organization) projects?: Project[]; - @Field(type => [Token], { nullable: true }) - @ManyToMany(type => Token, token => token.organizations) + @Field(_type => [Token], { nullable: true }) + @ManyToMany(_type => Token, token => token.organizations) @JoinTable() tokens: Token[]; } diff --git a/src/entities/powerBalanceSnapshot.ts b/src/entities/powerBalanceSnapshot.ts index 50d9aa587..84750b46d 100644 --- a/src/entities/powerBalanceSnapshot.ts +++ b/src/entities/powerBalanceSnapshot.ts @@ -1,4 +1,4 @@ -import { Field, ID, Int, ObjectType } from 'type-graphql'; +import { Field, ID, ObjectType } from 'type-graphql'; import { PrimaryGeneratedColumn, Column, @@ -16,11 +16,11 @@ import { PowerSnapshot } from './powerSnapshot'; // To improve the performance of the query, we need to add the following index @Index(['powerSnapshotId', 'userId'], { where: 'balance IS NULL' }) export class PowerBalanceSnapshot extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; - @Field(type => ID) + @Field(_type => ID) @Column() userId: number; @@ -28,7 +28,7 @@ export class PowerBalanceSnapshot extends BaseEntity { @Column('float', { nullable: true }) balance: number; - @Field(type => ID) + @Field(_type => ID) @RelationId( (powerBalanceSnapshot: PowerBalanceSnapshot) => powerBalanceSnapshot.powerSnapshot, @@ -36,7 +36,7 @@ export class PowerBalanceSnapshot extends BaseEntity { @Column() powerSnapshotId: number; - @Field(type => PowerSnapshot, { nullable: false }) - @ManyToOne(type => PowerSnapshot, { nullable: false }) + @Field(_type => PowerSnapshot, { nullable: false }) + @ManyToOne(_type => PowerSnapshot, { nullable: false }) powerSnapshot: PowerSnapshot; } diff --git a/src/entities/powerBalanceSnapshotHistory.ts b/src/entities/powerBalanceSnapshotHistory.ts index 1fc0ef711..4a312aa28 100644 --- a/src/entities/powerBalanceSnapshotHistory.ts +++ b/src/entities/powerBalanceSnapshotHistory.ts @@ -4,11 +4,11 @@ import { Column, Entity, BaseEntity, PrimaryColumn } from 'typeorm'; @Entity() @ObjectType() export class PowerBalanceSnapshotHistory extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryColumn() id: number; - @Field(type => ID) + @Field(_type => ID) @Column() userId: number; @@ -16,7 +16,7 @@ export class PowerBalanceSnapshotHistory extends BaseEntity { @Column('float') balance: number; - @Field(type => ID) + @Field(_type => ID) @Column() powerSnapshotId: number; } diff --git a/src/entities/powerBoosting.ts b/src/entities/powerBoosting.ts index fc6cba852..932916e1c 100644 --- a/src/entities/powerBoosting.ts +++ b/src/entities/powerBoosting.ts @@ -10,21 +10,21 @@ import { UpdateDateColumn, } from 'typeorm'; import { Field, Float, ID, ObjectType } from 'type-graphql'; +import { Max, Min, IsNumber } from 'class-validator'; import { Project } from './project'; import { User } from './user'; -import { Max, Min, IsNumber } from 'class-validator'; import { ColumnNumericTransformer } from '../utils/entities'; @Entity() @ObjectType() @Index(['projectId', 'userId'], { unique: true }) export class PowerBoosting extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; - @Field(type => Project) - @ManyToOne(type => Project, { eager: true }) + @Field(_type => Project) + @ManyToOne(_type => Project, { eager: true }) project: Project; @Index() @@ -32,8 +32,8 @@ export class PowerBoosting extends BaseEntity { @Column({ nullable: false }) projectId: number; - @Field(type => User) - @ManyToOne(type => User, { eager: true }) + @Field(_type => User) + @ManyToOne(_type => User, { eager: true }) user: User; @Index() @@ -41,7 +41,7 @@ export class PowerBoosting extends BaseEntity { @Column({ nullable: false }) userId: number; - @Field(type => Float) + @Field(_type => Float) @Column('numeric', { precision: 5, // 100.00 scale: 2, diff --git a/src/entities/powerBoostingSnapshot.ts b/src/entities/powerBoostingSnapshot.ts index 5733f2104..b033caaf2 100644 --- a/src/entities/powerBoostingSnapshot.ts +++ b/src/entities/powerBoostingSnapshot.ts @@ -1,7 +1,6 @@ import { BaseEntity, Column, - CreateDateColumn, Entity, Index, ManyToOne, @@ -17,11 +16,11 @@ import { PowerSnapshot } from './powerSnapshot'; @ObjectType() @Index(['userId', 'projectId', 'powerSnapshotId'], { unique: true }) export class PowerBoostingSnapshot extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; - @Field(type => ID) + @Field(_type => ID) @RelationId( (powerBoostingSnapshot: PowerBoostingSnapshot) => powerBoostingSnapshot.user, @@ -29,15 +28,15 @@ export class PowerBoostingSnapshot extends BaseEntity { @Column() userId: number; - @Field(type => User, { nullable: false }) - @ManyToOne(type => User, { nullable: false }) + @Field(_type => User, { nullable: false }) + @ManyToOne(_type => User, { nullable: false }) user: User; - @Field(type => ID) + @Field(_type => ID) @Column() projectId: number; - @Field(type => ID) + @Field(_type => ID) @RelationId( (powerBoostingSnapshot: PowerBoostingSnapshot) => powerBoostingSnapshot.powerSnapshot, @@ -45,11 +44,11 @@ export class PowerBoostingSnapshot extends BaseEntity { @Column() powerSnapshotId: number; - @Field(type => PowerSnapshot, { nullable: true }) - @ManyToOne(type => PowerSnapshot, { nullable: false }) + @Field(_type => PowerSnapshot, { nullable: true }) + @ManyToOne(_type => PowerSnapshot, { nullable: false }) powerSnapshot: PowerSnapshot; - @Field(type => Float) + @Field(_type => Float) @Column('numeric', { precision: 5, // 100.00 scale: 2, diff --git a/src/entities/powerBoostingSnapshotHistory.ts b/src/entities/powerBoostingSnapshotHistory.ts index 890a38863..bfb5f2bd7 100644 --- a/src/entities/powerBoostingSnapshotHistory.ts +++ b/src/entities/powerBoostingSnapshotHistory.ts @@ -1,37 +1,27 @@ -import { - BaseEntity, - Column, - Entity, - Index, - ManyToOne, - PrimaryColumn, - RelationId, -} from 'typeorm'; +import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; import { Field, Float, ID, ObjectType } from 'type-graphql'; -import { User } from './user'; import { ColumnNumericTransformer } from '../utils/entities'; -import { PowerSnapshotHistory } from './powerSnapshotHistory'; @Entity() @ObjectType() export class PowerBoostingSnapshotHistory extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryColumn() id: number; - @Field(type => ID) + @Field(_type => ID) @Column() userId: number; - @Field(type => ID) + @Field(_type => ID) @Column() projectId: number; - @Field(type => ID) + @Field(_type => ID) @Column() powerSnapshotId: number; - @Field(type => Float) + @Field(_type => Float) @Column('numeric', { precision: 5, // 100.00 scale: 2, diff --git a/src/entities/powerRound.ts b/src/entities/powerRound.ts index c96dffc6b..4a99876dd 100644 --- a/src/entities/powerRound.ts +++ b/src/entities/powerRound.ts @@ -1,11 +1,11 @@ -import { Field, ID, ObjectType } from 'type-graphql'; +import { Field, ObjectType } from 'type-graphql'; import { Column, Entity, BaseEntity, PrimaryColumn, Check } from 'typeorm'; @Entity() @ObjectType() @Check('"id"') export class PowerRound extends BaseEntity { - @Field(type => Boolean) + @Field(_type => Boolean) @PrimaryColumn() id: boolean; diff --git a/src/entities/powerSnapshot.ts b/src/entities/powerSnapshot.ts index 156df5178..332e9c7ef 100644 --- a/src/entities/powerSnapshot.ts +++ b/src/entities/powerSnapshot.ts @@ -14,11 +14,11 @@ import { ColumnDateTransformer } from '../utils/entities'; @Entity() @ObjectType() export class PowerSnapshot extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; - @Field(type => Date) + @Field(_type => Date) @Column({ type: 'timestamp without time zone', transformer: new ColumnDateTransformer(), @@ -26,7 +26,7 @@ export class PowerSnapshot extends BaseEntity { @Index({ unique: true }) time: Date; - @Field(type => Int) + @Field(_type => Int) @Column('integer', { nullable: true }) @Index({ unique: true }) blockNumber?: number; @@ -35,21 +35,21 @@ export class PowerSnapshot extends BaseEntity { @Column({ type: 'integer', nullable: true }) roundNumber: number; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) @Column({ nullable: true }) @Index() synced?: boolean; - @Field(type => [PowerBoostingSnapshot], { nullable: true }) + @Field(_type => [PowerBoostingSnapshot], { nullable: true }) @OneToMany( - type => PowerBoostingSnapshot, + _type => PowerBoostingSnapshot, powerBoostingSnapshot => powerBoostingSnapshot.powerSnapshot, ) powerBoostingSnapshots?: PowerBoostingSnapshot[]; - @Field(type => [PowerBalanceSnapshot], { nullable: true }) + @Field(_type => [PowerBalanceSnapshot], { nullable: true }) @OneToMany( - type => PowerBalanceSnapshot, + _type => PowerBalanceSnapshot, powerBalanceSnapshot => powerBalanceSnapshot.powerSnapshot, ) powerBalanceSnapshots?: PowerBalanceSnapshot[]; diff --git a/src/entities/powerSnapshotHistory.ts b/src/entities/powerSnapshotHistory.ts index aea5adb7d..4575555b4 100644 --- a/src/entities/powerSnapshotHistory.ts +++ b/src/entities/powerSnapshotHistory.ts @@ -1,29 +1,19 @@ import { Field, ID, Int, ObjectType } from 'type-graphql'; -import { - PrimaryGeneratedColumn, - Column, - Entity, - BaseEntity, - Index, - OneToMany, - PrimaryColumn, -} from 'typeorm'; -import { PowerBalanceSnapshotHistory } from './powerBalanceSnapshotHistory'; -import { PowerBoostingSnapshotHistory } from './powerBoostingSnapshotHistory'; +import { Column, Entity, BaseEntity, Index, PrimaryColumn } from 'typeorm'; @Entity() @ObjectType() export class PowerSnapshotHistory extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryColumn() id: number; - @Field(type => Date) + @Field(_type => Date) @Column() @Index({ unique: true }) time: Date; - @Field(type => Int) + @Field(_type => Int) @Column('integer', { nullable: true }) @Index({ unique: true }) blockNumber?: number; @@ -32,7 +22,7 @@ export class PowerSnapshotHistory extends BaseEntity { @Column({ type: 'integer', nullable: true }) roundNumber: number; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) @Column({ nullable: true }) @Index() synced?: boolean; diff --git a/src/entities/previousRoundRank.ts b/src/entities/previousRoundRank.ts index f88c8e2e4..5402e846c 100644 --- a/src/entities/previousRoundRank.ts +++ b/src/entities/previousRoundRank.ts @@ -12,19 +12,18 @@ import { } from 'typeorm'; import { Field, ID, ObjectType } from 'type-graphql'; import { Project } from './project'; -import { User } from './user'; @Entity() @ObjectType() @Unique(['round', 'project']) export class PreviousRoundRank extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @Index() - @Field(type => Project) - @ManyToOne(type => Project) + @Field(_type => Project) + @ManyToOne(_type => Project) project: Project; @RelationId( diff --git a/src/entities/project.test.ts b/src/entities/project.test.ts index 1015d64d4..76d63b5bd 100644 --- a/src/entities/project.test.ts +++ b/src/entities/project.test.ts @@ -4,9 +4,7 @@ import { createProjectData, saveProjectDirectlyToDb, SEED_DATA, - sleep, } from '../../test/testUtils'; -import { ProjectStatusHistory } from './projectStatusHistory'; import { ProjectStatus } from './projectStatus'; import { ProjectStatusReason } from './projectStatusReason'; import { findOneProjectStatusHistoryByProjectId } from '../repositories/projectSatusHistoryRepository'; @@ -21,7 +19,7 @@ describe('projectUpdate() test cases', projectUpdateTestCases); function projectUpdateTestCases() { it('should update project updatedAt when a new update is added', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); - const update = await ProjectUpdate.create({ + const update = ProjectUpdate.create({ userId: project.adminUserId, projectId: project.id, content: 'content', diff --git a/src/entities/project.ts b/src/entities/project.ts index 04808bd99..50d1f0f7a 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -19,11 +19,11 @@ import { JoinTable, } from 'typeorm'; +import { Int } from 'type-graphql/dist/scalars/aliases'; import { Donation } from './donation'; import { Reaction } from './reaction'; import { User } from './user'; import { ProjectStatus } from './projectStatus'; -import { Int } from 'type-graphql/dist/scalars/aliases'; import { ProjectStatusHistory } from './projectStatusHistory'; import { ProjectStatusReason } from './projectStatusReason'; import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; @@ -54,7 +54,8 @@ import { EstimatedMatching } from '../types/qfTypes'; import { Campaign } from './campaign'; import { ProjectEstimatedMatchingView } from './ProjectEstimatedMatchingView'; import { AnchorContractAddress } from './anchorContractAddress'; -// tslint:disable-next-line:no-var-requires +import { ProjectSocialMedia } from './projectSocialMedia'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const moment = require('moment'); export enum ProjStatus { @@ -79,6 +80,7 @@ export enum SortingField { GIVPower = 'GIVPower', InstantBoosting = 'InstantBoosting', ActiveQfRoundRaisedFunds = 'ActiveQfRoundRaisedFunds', + EstimatedMatching = 'EstimatedMatching', } export enum FilterField { @@ -131,7 +133,7 @@ export enum ReviewStatus { @Entity() @ObjectType() export class Project extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @@ -139,13 +141,13 @@ export class Project extends BaseEntity { @Column() title: string; - @Index() + @Index({ unique: true }) @Field({ nullable: true }) @Column({ nullable: true }) slug?: string; @Index() - @Field(type => [String], { nullable: true }) + @Field(_type => [String], { nullable: true }) @Column('text', { array: true, default: '{}' }) slugHistory?: string[]; @@ -191,8 +193,8 @@ export class Project extends BaseEntity { @Column({ nullable: true }) updatedAt: Date; - @Field(type => Organization) - @ManyToOne(type => Organization) + @Field(_type => Organization) + @ManyToOne(_type => Organization) @JoinTable() organization: Organization; @@ -212,21 +214,21 @@ export class Project extends BaseEntity { @Column({ nullable: true }) impactLocation?: string; - @Field(type => [Category], { nullable: true }) - @ManyToMany(type => Category, category => category.projects, { + @Field(_type => [Category], { nullable: true }) + @ManyToMany(_type => Category, category => category.projects, { nullable: true, }) @JoinTable() categories: Category[]; - @Field(type => [QfRound], { nullable: true }) - @ManyToMany(type => QfRound, qfRound => qfRound.projects, { + @Field(_type => [QfRound], { nullable: true }) + @ManyToMany(_type => QfRound, qfRound => qfRound.projects, { nullable: true, }) @JoinTable() qfRounds: QfRound[]; - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) @Column('float', { nullable: true }) balance: number = 0; @@ -238,52 +240,62 @@ export class Project extends BaseEntity { @Column({ unique: true, nullable: true }) walletAddress?: string; - @Field(type => Boolean) + @Field(_type => Boolean) @Column() verified: boolean; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column('text', { nullable: true }) verificationStatus?: string | null; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) @Column({ default: false }) isImported: boolean; - @Field(type => Boolean) + @Field(_type => Boolean) @Column() giveBacks: boolean; - @Field(type => [Donation], { nullable: true }) - @OneToMany(type => Donation, donation => donation.project) + @Field(_type => [Donation], { nullable: true }) + @OneToMany(_type => Donation, donation => donation.project) donations?: Donation[]; - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) @Column({ nullable: true }) qualityScore: number = 0; - @Field(type => [ProjectContacts], { nullable: true }) + @Field(_type => [ProjectContacts], { nullable: true }) @Column('jsonb', { nullable: true }) contacts: ProjectContacts[]; - @ManyToMany(type => User, user => user.projects) - @Field(type => [User], { nullable: true }) + @ManyToMany(_type => User, user => user.projects) + @Field(_type => [User], { nullable: true }) @JoinTable() users: User[]; @Field(() => [Reaction], { nullable: true }) - @OneToMany(type => Reaction, reaction => reaction.project) + @OneToMany(_type => Reaction, reaction => reaction.project) reactions?: Reaction[]; - @Field(type => [ProjectAddress], { nullable: true }) - @OneToMany(type => ProjectAddress, projectAddress => projectAddress.project, { + @Field(_type => [ProjectAddress], { nullable: true }) + @OneToMany( + _type => ProjectAddress, + projectAddress => projectAddress.project, + { + eager: true, + }, + ) + addresses?: ProjectAddress[]; + + @Field(_type => [ProjectSocialMedia], { nullable: true }) + @OneToMany(_type => ProjectSocialMedia, socialMedia => socialMedia.project, { eager: true, }) - addresses?: ProjectAddress[]; + socialMedia?: ProjectSocialMedia[]; - @Field(type => [AnchorContractAddress], { nullable: true }) + @Field(_type => [AnchorContractAddress], { nullable: true }) @OneToMany( - type => AnchorContractAddress, + _type => AnchorContractAddress, anchorContractAddress => anchorContractAddress.project, { eager: true, @@ -292,15 +304,15 @@ export class Project extends BaseEntity { anchorContracts?: AnchorContractAddress[]; @Index() - @Field(type => ProjectStatus) - @ManyToOne(type => ProjectStatus) + @Field(_type => ProjectStatus) + @ManyToOne(_type => ProjectStatus) status: ProjectStatus; @RelationId((project: Project) => project.status) @Column({ nullable: true }) statusId: number; @Index() - @Field(type => User, { nullable: true }) + @Field(_type => User, { nullable: true }) @ManyToOne(() => User, { eager: true }) adminUser: User; @@ -308,87 +320,87 @@ export class Project extends BaseEntity { @RelationId((project: Project) => project.adminUser) adminUserId: number; - @Field(type => [ProjectStatusHistory], { nullable: true }) + @Field(_type => [ProjectStatusHistory], { nullable: true }) @OneToMany( - type => ProjectStatusHistory, + _type => ProjectStatusHistory, projectStatusHistory => projectStatusHistory.project, ) statusHistory?: ProjectStatusHistory[]; - @Field(type => ProjectVerificationForm, { nullable: true }) + @Field(_type => ProjectVerificationForm, { nullable: true }) @OneToOne( - type => ProjectVerificationForm, + _type => ProjectVerificationForm, projectVerificationForm => projectVerificationForm.project, { nullable: true }, ) projectVerificationForm?: ProjectVerificationForm; - @Field(type => FeaturedUpdate, { nullable: true }) - @OneToOne(type => FeaturedUpdate, featuredUpdate => featuredUpdate.project, { + @Field(_type => FeaturedUpdate, { nullable: true }) + @OneToOne(_type => FeaturedUpdate, featuredUpdate => featuredUpdate.project, { nullable: true, }) featuredUpdate?: FeaturedUpdate; - @Field(type => ProjectPowerView, { nullable: true }) + @Field(_type => ProjectPowerView, { nullable: true }) @OneToOne( - type => ProjectPowerView, + _type => ProjectPowerView, projectPowerView => projectPowerView.project, ) projectPower?: ProjectPowerView; - @Field(type => ProjectFuturePowerView, { nullable: true }) + @Field(_type => ProjectFuturePowerView, { nullable: true }) @OneToOne( - type => ProjectFuturePowerView, + _type => ProjectFuturePowerView, projectFuturePowerView => projectFuturePowerView.project, ) projectFuturePower?: ProjectFuturePowerView; - @Field(type => ProjectInstantPowerView, { nullable: true }) + @Field(_type => ProjectInstantPowerView, { nullable: true }) @OneToOne( - type => ProjectInstantPowerView, + _type => ProjectInstantPowerView, projectInstantPowerView => projectInstantPowerView.project, ) projectInstantPower?: ProjectInstantPowerView; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) verificationFormStatus?: string; - @Field(type => [SocialProfile], { nullable: true }) - @OneToMany(type => SocialProfile, socialProfile => socialProfile.project) + @Field(_type => [SocialProfile], { nullable: true }) + @OneToMany(_type => SocialProfile, socialProfile => socialProfile.project) socialProfiles?: SocialProfile[]; - @Field(type => [ProjectEstimatedMatchingView], { nullable: true }) + @Field(_type => [ProjectEstimatedMatchingView], { nullable: true }) @OneToMany( - type => ProjectEstimatedMatchingView, + _type => ProjectEstimatedMatchingView, projectEstimatedMatchingView => projectEstimatedMatchingView.project, ) projectEstimatedMatchingView?: ProjectEstimatedMatchingView[]; - @Field(type => Float) + @Field(_type => Float) @Column({ type: 'real' }) totalDonations: number; - @Field(type => Float) + @Field(_type => Float) @Column({ type: 'real', default: 0 }) totalTraceDonations: number; - @Field(type => Int, { defaultValue: 0 }) + @Field(_type => Int, { defaultValue: 0 }) @Column({ type: 'integer', default: 0 }) totalReactions: number; - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) @Column({ type: 'integer', nullable: true }) totalProjectUpdates: number; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) @Column({ type: 'boolean', default: null, nullable: true }) listed?: boolean | null; - // @Field(type => Boolean, { nullable: true }) + // @Field(_type => Boolean, { nullable: true }) // @Column({ type: 'boolean', default: false }) // tunnableQf?: boolean; - @Field(type => String) + @Field(_type => String) @Column({ type: 'enum', enum: ReviewStatus, @@ -396,32 +408,33 @@ export class Project extends BaseEntity { }) reviewStatus: ReviewStatus; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) projectUrl?: string; // Virtual attribute to subquery result into - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) prevStatusId?: number; // Virtual attribute for projectUpdate - @Field(type => ProjectUpdate, { nullable: true }) + @Field(_type => ProjectUpdate, { nullable: true }) projectUpdate?: any; - @Field(type => [ProjectUpdate], { nullable: true }) + @Field(_type => [ProjectUpdate], { nullable: true }) + @OneToMany(() => ProjectUpdate, projectUpdate => projectUpdate.project) projectUpdates?: ProjectUpdate[]; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) adminJsBaseUrl: string; // User reaction to the project @Field({ nullable: true }) reaction?: Reaction; - @Field(type => [Campaign], { nullable: true }) + @Field(_type => [Campaign], { nullable: true }) campaigns: Campaign[]; // only projects with status active can be listed automatically - static pendingReviewSince(maximumDaysForListing: Number) { + static pendingReviewSince(maximumDaysForListing: number) { const maxDaysForListing = moment() .subtract(maximumDaysForListing, 'days') .endOf('day'); @@ -472,7 +485,7 @@ export class Project extends BaseEntity { * Custom Query Builders to chain together */ - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) async sumDonationValueUsdForActiveQfRound() { const activeQfRound = this.getActiveQfRound(); return activeQfRound @@ -483,12 +496,12 @@ export class Project extends BaseEntity { : 0; } - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) async sumDonationValueUsd() { return sumDonationValueUsd(this.id); } - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) async countUniqueDonorsForActiveQfRound() { const activeQfRound = this.getActiveQfRound(); return activeQfRound @@ -499,13 +512,13 @@ export class Project extends BaseEntity { : 0; } - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) async countUniqueDonors() { return countUniqueDonors(this.id); } // In your main class - @Field(type => EstimatedMatching, { nullable: true }) + @Field(_type => EstimatedMatching, { nullable: true }) async estimatedMatching(): Promise { const activeQfRound = this.getActiveQfRound(); if (!activeQfRound) { @@ -589,31 +602,31 @@ export class Project extends BaseEntity { @Entity() @ObjectType() export class ProjectUpdate extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; - @Field(type => String) + @Field(_type => String) @Column() title: string; // Virtual attribute for projectUpdate - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) projectTitle?: string; // Virtual attribute for projectUpdate - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) projectSlug?: string; - @Field(type => ID) + @Field(_type => ID) @Column() projectId: number; - @Field(type => ID) + @Field(_type => ID) @Column() userId: number; - @Field(type => String) + @Field(_type => String) @Column() content: string; @@ -621,24 +634,25 @@ export class ProjectUpdate extends BaseEntity { @Column({ nullable: true }) contentSummary?: string; - @Field(type => Date) + @Field(_type => Date) @Column() createdAt: Date; - @Field(type => Boolean) + @Field(_type => Boolean) @Column({ nullable: true }) isMain: boolean; - @Field(type => Int, { defaultValue: 0 }) + @Field(_type => Int, { defaultValue: 0 }) @Column({ type: 'integer', default: 0 }) totalReactions: number; // User reaction to the project update - @Field(type => Reaction, { nullable: true }) + @Field(_type => Reaction, { nullable: true }) reaction?: Reaction; // Project oneToOne as virtual attribute as relation was not set properly - @Field(type => Project, { nullable: true }) + @Field(_type => Project, { nullable: true }) + @ManyToOne(() => Project, project => project.projectUpdates) project?: Project; @Field() @@ -689,9 +703,9 @@ export class ProjectUpdate extends BaseEntity { @Column('text', { nullable: true }) managingFundDescription: string; - @Field(type => FeaturedUpdate, { nullable: true }) + @Field(_type => FeaturedUpdate, { nullable: true }) @OneToOne( - type => FeaturedUpdate, + _type => FeaturedUpdate, featuredUpdate => featuredUpdate.projectUpdate, { nullable: true }, ) diff --git a/src/entities/projectAddress.ts b/src/entities/projectAddress.ts index a57f5e36c..02134207e 100644 --- a/src/entities/projectAddress.ts +++ b/src/entities/projectAddress.ts @@ -19,7 +19,7 @@ import { ChainType } from '../types/network'; @ObjectType() @Unique(['address', 'networkId', 'project']) export class ProjectAddress extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @@ -32,7 +32,7 @@ export class ProjectAddress extends BaseEntity { @Column() networkId: number; - @Field(type => String) + @Field(_type => String) @Column({ type: 'enum', enum: ChainType, @@ -46,8 +46,8 @@ export class ProjectAddress extends BaseEntity { address: string; @Index() - @Field(type => Project) - @ManyToOne(type => Project) + @Field(_type => Project) + @ManyToOne(_type => Project) project: Project; @RelationId((relatedAddress: ProjectAddress) => relatedAddress.project) @@ -55,8 +55,8 @@ export class ProjectAddress extends BaseEntity { projectId: number; @Index() - @Field(type => User, { nullable: true }) - @ManyToOne(type => User, { eager: true, nullable: true }) + @Field(_type => User, { nullable: true }) + @ManyToOne(_type => User, { eager: true, nullable: true }) user: User; @RelationId((relatedAddress: ProjectAddress) => relatedAddress.user) diff --git a/src/entities/projectFraud.ts b/src/entities/projectFraud.ts index ab24bd9a5..9042827a5 100644 --- a/src/entities/projectFraud.ts +++ b/src/entities/projectFraud.ts @@ -15,20 +15,20 @@ import { QfRound } from './qfRound'; @Entity() @Unique(['projectId', 'qfRoundId']) export class ProjectFraud extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; - @Field(type => Project) - @ManyToOne(type => Project, { eager: true }) + @Field(_type => Project) + @ManyToOne(_type => Project, { eager: true }) project: Project; @RelationId((projectFraud: ProjectFraud) => projectFraud.project) @Column() projectId: number; - @Field(type => QfRound) - @ManyToOne(type => QfRound, { eager: true }) + @Field(_type => QfRound) + @ManyToOne(_type => QfRound, { eager: true }) qfRound: QfRound; @RelationId((projectFraud: ProjectFraud) => projectFraud.qfRound) diff --git a/src/entities/projectImage.ts b/src/entities/projectImage.ts index 10f9ab1ec..e344b267b 100644 --- a/src/entities/projectImage.ts +++ b/src/entities/projectImage.ts @@ -12,19 +12,19 @@ import { Project } from './project'; @Entity() @ObjectType() export class ProjectImage extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; - @Field(type => Project) - @ManyToOne(type => Project, { eager: true }) + @Field(_type => Project) + @ManyToOne(_type => Project, { eager: true }) project: Project; @RelationId((projectImage: ProjectImage) => projectImage.project) @Column({ nullable: true }) projectId: number; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) url?: string; } diff --git a/src/entities/projectSocialMedia.ts b/src/entities/projectSocialMedia.ts new file mode 100644 index 000000000..8516a17ab --- /dev/null +++ b/src/entities/projectSocialMedia.ts @@ -0,0 +1,51 @@ +import { + BaseEntity, + Column, + Entity, + Index, + ManyToOne, + PrimaryGeneratedColumn, + RelationId, +} from 'typeorm'; +import { Field, ID, ObjectType } from 'type-graphql'; +import { Project } from './project'; +import { User } from './user'; +import { ProjectSocialMediaType } from '../types/projectSocialMediaType'; + +@Entity() +@ObjectType() +export class ProjectSocialMedia extends BaseEntity { + @Field(_type => ID) + @PrimaryGeneratedColumn() + readonly id: number; + + @Field(_type => String) + @Column({ + type: 'enum', + enum: ProjectSocialMediaType, + }) + type: ProjectSocialMediaType; + + @Index() + @Field() + @Column() + link: string; + + @Index() + @Field(_type => Project) + @ManyToOne(_type => Project) + project: Project; + + @RelationId((relatedAddress: ProjectSocialMedia) => relatedAddress.project) + @Column({ nullable: true }) + projectId: number; + + @Index() + @Field(_type => User, { nullable: true }) + @ManyToOne(_type => User, { eager: true, nullable: true }) + user: User; + + @RelationId((relatedAddress: ProjectSocialMedia) => relatedAddress.user) + @Column({ nullable: true }) + userId: number; +} diff --git a/src/entities/projectStatus.ts b/src/entities/projectStatus.ts index 070903534..f3a63a654 100644 --- a/src/entities/projectStatus.ts +++ b/src/entities/projectStatus.ts @@ -13,7 +13,7 @@ import { ProjectStatusReason } from './projectStatusReason'; @Entity() @ObjectType() export class ProjectStatus extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; @@ -30,20 +30,20 @@ export class ProjectStatus extends BaseEntity { @Column({ nullable: true }) description: string; - @Field(type => [Project], { nullable: true }) - @OneToMany(type => Project, project => project.status) + @Field(_type => [Project], { nullable: true }) + @OneToMany(_type => Project, project => project.status) projects?: Project[]; - @Field(type => [ProjectStatusReason], { nullable: true }) + @Field(_type => [ProjectStatusReason], { nullable: true }) @OneToMany( - type => ProjectStatusReason, + _type => ProjectStatusReason, projectStatusReason => projectStatusReason.status, ) reasons?: ProjectStatusReason[]; - @Field(type => [ProjectStatusReason], { nullable: true }) + @Field(_type => [ProjectStatusReason], { nullable: true }) @OneToMany( - type => ProjectStatusReason, + _type => ProjectStatusReason, projectStatusReason => projectStatusReason.status, ) projectStatusHistories?: ProjectStatusReason[]; diff --git a/src/entities/projectStatusHistory.ts b/src/entities/projectStatusHistory.ts index cecf7c3d6..0f0cdb587 100644 --- a/src/entities/projectStatusHistory.ts +++ b/src/entities/projectStatusHistory.ts @@ -24,12 +24,12 @@ export const HISTORY_DESCRIPTIONS = { @Entity() @ObjectType() export class ProjectStatusHistory extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; - @Field(type => Project) - @ManyToOne(type => Project) + @Field(_type => Project) + @ManyToOne(_type => Project) project: Project; @RelationId( @@ -39,8 +39,8 @@ export class ProjectStatusHistory extends BaseEntity { @Column({ nullable: true }) projectId: number; - @Field(type => ProjectStatus) - @ManyToOne(type => ProjectStatus) + @Field(_type => ProjectStatus) + @ManyToOne(_type => ProjectStatus) status: ProjectStatus; @RelationId( @@ -49,8 +49,8 @@ export class ProjectStatusHistory extends BaseEntity { @Column({ nullable: true }) statusId: number; - @Field(type => ProjectStatus) - @ManyToOne(type => ProjectStatus) + @Field(_type => ProjectStatus) + @ManyToOne(_type => ProjectStatus) prevStatus?: ProjectStatus; @RelationId( @@ -60,8 +60,8 @@ export class ProjectStatusHistory extends BaseEntity { @Column({ nullable: true }) prevStatusId: number; - @Field(type => ProjectStatusReason) - @ManyToOne(type => ProjectStatusReason) + @Field(_type => ProjectStatusReason) + @ManyToOne(_type => ProjectStatusReason) reason?: ProjectStatusReason; @RelationId( @@ -70,8 +70,8 @@ export class ProjectStatusHistory extends BaseEntity { @Column({ nullable: true }) reasonId: number; - @Field(type => User) - @ManyToOne(type => User) + @Field(_type => User) + @ManyToOne(_type => User) user?: User; @RelationId( @@ -84,7 +84,7 @@ export class ProjectStatusHistory extends BaseEntity { @Column({ nullable: true }) description?: string; - @Field(type => Date) + @Field(_type => Date) @Column() createdAt: Date; } diff --git a/src/entities/projectStatusReason.ts b/src/entities/projectStatusReason.ts index 8e6fa3354..376d5031c 100644 --- a/src/entities/projectStatusReason.ts +++ b/src/entities/projectStatusReason.ts @@ -4,18 +4,15 @@ import { Column, Entity, BaseEntity, - OneToMany, - Index, ManyToOne, RelationId, } from 'typeorm'; -import { Project } from './project'; import { ProjectStatus } from './projectStatus'; @Entity() @ObjectType() export class ProjectStatusReason extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; @@ -23,8 +20,8 @@ export class ProjectStatusReason extends BaseEntity { @Column({ nullable: true }) description: string; - @Field(type => ProjectStatus) - @ManyToOne(type => ProjectStatus) + @Field(_type => ProjectStatus) + @ManyToOne(_type => ProjectStatus) status: ProjectStatus; @RelationId( diff --git a/src/entities/projectVerificationForm.ts b/src/entities/projectVerificationForm.ts index 97783fd36..acaf0eebb 100644 --- a/src/entities/projectVerificationForm.ts +++ b/src/entities/projectVerificationForm.ts @@ -59,7 +59,7 @@ export class ProjectRegistry { organizationDescription?: string; @Field({ nullable: true }) organizationName?: string; - @Field(type => [String], { nullable: true }) + @Field(_type => [String], { nullable: true }) attachments?: string[]; } @@ -73,19 +73,19 @@ export class ProjectContacts { @ObjectType() export class Milestones { - @Field(type => String, { nullable: true }) - foundationDate?: String; + @Field(_type => String, { nullable: true }) + foundationDate?: string; @Field({ nullable: true }) mission?: string; @Field({ nullable: true }) achievedMilestones?: string; - @Field(type => [String], { nullable: true }) + @Field(_type => [String], { nullable: true }) achievedMilestonesProofs?: string[]; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) problem?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) plans?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) impact?: string; } @@ -97,7 +97,7 @@ export class FormRelatedAddress { address: string; @Field({ nullable: true }) networkId: number; - @Field(type => ChainType, { defaultValue: ChainType.EVM, nullable: true }) + @Field(_type => ChainType, { defaultValue: ChainType.EVM, nullable: true }) chainType?: ChainType; } @@ -133,13 +133,13 @@ export class ProjectVerificationForm extends BaseEntity { * @see {@link https://github.com/Giveth/giveth-dapps-v2/issues/711#issuecomment-1130001342} */ - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @Index() - @Field(type => Project) - @OneToOne(type => Project) + @Field(_type => Project) + @OneToOne(_type => Project) @JoinColumn() project: Project; @@ -151,8 +151,8 @@ export class ProjectVerificationForm extends BaseEntity { projectId: number; @Index() - @Field(type => User, { nullable: true }) - @ManyToOne(type => User, { eager: true }) + @Field(_type => User, { nullable: true }) + @ManyToOne(_type => User, { eager: true }) reviewer?: User; @RelationId( @@ -163,8 +163,8 @@ export class ProjectVerificationForm extends BaseEntity { reviewerId: number; @Index() - @Field(type => User, { nullable: true }) - @ManyToOne(type => User, { eager: true, nullable: true }) + @Field(_type => User, { nullable: true }) + @ManyToOne(_type => User, { eager: true, nullable: true }) user: User; @RelationId( @@ -174,14 +174,14 @@ export class ProjectVerificationForm extends BaseEntity { @Column({ nullable: true }) userId: number; - @Field(type => [SocialProfile], { nullable: true }) + @Field(_type => [SocialProfile], { nullable: true }) @OneToMany( - type => SocialProfile, + _type => SocialProfile, socialProfile => socialProfile.projectVerificationForm, ) socialProfiles?: SocialProfile[]; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ type: 'enum', enum: PROJECT_VERIFICATION_STATUSES, @@ -200,59 +200,59 @@ export class ProjectVerificationForm extends BaseEntity { verifiedAt: Date; // https://github.com/typeorm/typeorm/issues/4674#issuecomment-618073862 - @Field(type => PersonalInfo, { nullable: true }) + @Field(_type => PersonalInfo, { nullable: true }) @Column('jsonb', { nullable: true }) personalInfo: PersonalInfo; - @Field(type => ProjectRegistry, { nullable: true }) + @Field(_type => ProjectRegistry, { nullable: true }) @Column('jsonb', { nullable: true }) projectRegistry: ProjectRegistry; - @Field(type => [ProjectContacts], { nullable: true }) + @Field(_type => [ProjectContacts], { nullable: true }) @Column('jsonb', { nullable: true }) projectContacts: ProjectContacts[]; - @Field(type => Milestones, { nullable: true }) + @Field(_type => Milestones, { nullable: true }) @Column('jsonb', { nullable: true }) milestones: Milestones; - @Field(type => ManagingFunds, { nullable: true }) + @Field(_type => ManagingFunds, { nullable: true }) @Column('jsonb', { nullable: true }) managingFunds: ManagingFunds; - @Field(type => CommentsSection, { nullable: true }) + @Field(_type => CommentsSection, { nullable: true }) @Column('jsonb', { nullable: true }) commentsSection: CommentsSection; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column('text', { nullable: true }) lastStep: string | null; - @Field(type => Boolean, { nullable: false }) + @Field(_type => Boolean, { nullable: false }) @Column({ default: false }) emailConfirmed: boolean; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column('text', { nullable: true }) email?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column('text', { nullable: true }) emailConfirmationToken: string | null; - @Field(type => Date, { nullable: true }) + @Field(_type => Date, { nullable: true }) @Column('timestamptz', { nullable: true }) emailConfirmationTokenExpiredAt: Date | null; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) @Column({ default: false }) emailConfirmationSent: boolean; - @Field(type => Date, { nullable: true }) + @Field(_type => Date, { nullable: true }) @Column({ type: 'timestamptz', nullable: true }) emailConfirmationSentAt: Date | null; - @Field(type => Date, { nullable: true }) + @Field(_type => Date, { nullable: true }) @Column({ nullable: true }) emailConfirmedAt: Date; diff --git a/src/entities/qfRound.ts b/src/entities/qfRound.ts index f40eff4e3..0fdce1a53 100644 --- a/src/entities/qfRound.ts +++ b/src/entities/qfRound.ts @@ -5,19 +5,16 @@ import { Entity, BaseEntity, ManyToMany, - ManyToOne, - RelationId, UpdateDateColumn, CreateDateColumn, Index, } from 'typeorm'; import { Project } from './project'; -import { MainCategory } from './mainCategory'; @Entity() @ObjectType() export class QfRound extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; @@ -25,6 +22,14 @@ export class QfRound extends BaseEntity { @Column('text', { nullable: true }) name: string; + @Field({ nullable: true }) + @Column('text', { nullable: true }) + title: string; + + @Field({ nullable: true }) + @Column('text', { nullable: true }) + description: string; + @Field() @Index({ unique: true }) @Column('text') @@ -34,31 +39,31 @@ export class QfRound extends BaseEntity { @Column({ nullable: true }) isActive: boolean; - @Field(type => Number) + @Field(_type => Number) @Column() allocatedFund: number; - @Field(type => Number) + @Field(_type => Number) @Column('real', { default: 0.2 }) maximumReward: number; - @Field(type => Number) + @Field(_type => Number) @Column() minimumPassportScore: number; - @Field(type => Number) + @Field(_type => Number) @Column('real', { default: 1 }) minimumValidUsdValue: number; - @Field(type => [Int], { nullable: true }) // Define the new field as an array of integers + @Field(_type => [Int], { nullable: true }) // Define the new field as an array of integers @Column('integer', { array: true, default: [] }) eligibleNetworks: number[]; - @Field(type => Date) + @Field(_type => Date) @Column() beginDate: Date; - @Field(type => Date) + @Field(_type => Date) @Column() endDate: Date; @@ -68,11 +73,11 @@ export class QfRound extends BaseEntity { @CreateDateColumn() createdAt: Date; - @ManyToMany(type => Project, project => project.qfRounds) + @ManyToMany(_type => Project, project => project.qfRounds) projects: Project[]; // only projects with status active can be listed automatically - isEligibleNetwork(donationNetworkId: number): Boolean { + isEligibleNetwork(donationNetworkId: number): boolean { // when not specified, all are valid if (this.eligibleNetworks.length === 0) return true; diff --git a/src/entities/qfRoundHistory.ts b/src/entities/qfRoundHistory.ts index 2e9c57606..1053cc421 100644 --- a/src/entities/qfRoundHistory.ts +++ b/src/entities/qfRoundHistory.ts @@ -25,65 +25,69 @@ import { EstimatedMatching } from '../types/qfTypes'; // Have one record per projectId and qfRoundId @Unique(['projectId', 'qfRoundId']) export class QfRoundHistory extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; - @ManyToOne(type => Project) + @ManyToOne(_type => Project) project: Project; @Index() - @Field(type => ID, { nullable: true }) + @Field(_type => ID, { nullable: true }) @RelationId((qfRoundHistory: QfRoundHistory) => qfRoundHistory.project) @Column() projectId: number; - @ManyToOne(type => QfRound) + @ManyToOne(_type => QfRound) qfRound: QfRound; @Index() - @Field(type => ID, { nullable: true }) + @Field(_type => ID, { nullable: true }) @RelationId((qfRoundHistory: QfRoundHistory) => qfRoundHistory.qfRound) @Column() qfRoundId: number; - @Field(type => Number, { nullable: true }) + @Field(_type => Number, { nullable: true }) @Column({ nullable: true, default: 0 }) uniqueDonors: number; - @Field(type => Number, { nullable: true }) + @Field(_type => Number, { nullable: true }) @Column({ nullable: true, default: 0 }) donationsCount: number; - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) @Column({ type: 'real', nullable: true, default: 0 }) raisedFundInUsd: number; // usd value of matching fund - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) @Column({ type: 'real', nullable: true, default: 0 }) matchingFund: number; - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) @Column({ type: 'real', nullable: true }) matchingFundAmount?: number; - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) @Column({ type: 'real', nullable: true }) matchingFundPriceUsd?: number; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) matchingFundCurrency?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) distributedFundTxHash: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) distributedFundNetwork: string; + @Field(_type => Date, { nullable: true }) + @Column({ nullable: true }) + distributedFundTxDate: Date; + @UpdateDateColumn() updatedAt: Date; @@ -91,7 +95,7 @@ export class QfRoundHistory extends BaseEntity { createdAt: Date; // In your main class - @Field(type => EstimatedMatching, { nullable: true }) + @Field(_type => EstimatedMatching, { nullable: true }) async estimatedMatching(): Promise { const projectDonationsSqrtRootSum = await getProjectDonationsSqrtRootSum( this.projectId, diff --git a/src/entities/reaction.ts b/src/entities/reaction.ts index df6a20b0d..4a9632d91 100644 --- a/src/entities/reaction.ts +++ b/src/entities/reaction.ts @@ -16,35 +16,35 @@ import { User } from './user'; @Index(['userId', 'projectId'], { unique: true }) @Index(['userId', 'projectUpdateId'], { unique: true }) export class Reaction extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; - @ManyToOne(type => ProjectUpdate) + @ManyToOne(_type => ProjectUpdate) projectUpdate: ProjectUpdate; @Index() @RelationId((reaction: Reaction) => reaction.projectUpdate) - @Field(type => ID, { nullable: true }) + @Field(_type => ID, { nullable: true }) @Column({ nullable: true }) projectUpdateId?: number; // We just fill it with join when making query so dont need to Add @Column or @ManyToOne user: User; - @Field(type => ID) + @Field(_type => ID) @Column() userId: number; - @Field(type => String) + @Field(_type => String) @Column() reaction: string; - @ManyToOne(type => Project) + @ManyToOne(_type => Project) project: Project; @Index() - @Field(type => ID, { nullable: true }) + @Field(_type => ID, { nullable: true }) @RelationId((reaction: Reaction) => reaction.project) @Column({ nullable: true }) projectId: number; diff --git a/src/entities/recurringDonation.ts b/src/entities/recurringDonation.ts index eaf2929fc..af58e3c1e 100644 --- a/src/entities/recurringDonation.ts +++ b/src/entities/recurringDonation.ts @@ -5,6 +5,7 @@ import { Entity, Index, ManyToOne, + OneToMany, PrimaryGeneratedColumn, RelationId, Unique, @@ -14,11 +15,14 @@ import { Field, ID, ObjectType } from 'type-graphql'; import { Project } from './project'; import { User } from './user'; import { AnchorContractAddress } from './anchorContractAddress'; +import { Donation } from './donation'; export const RECURRING_DONATION_STATUS = { PENDING: 'pending', VERIFIED: 'verified', + ENDED: 'ended', FAILED: 'failed', + ACTIVE: 'active', }; @Entity() @@ -26,7 +30,7 @@ export const RECURRING_DONATION_STATUS = { @Unique(['txHash', 'networkId', 'project']) // TODO entity is not completed export class RecurringDonation extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @@ -35,13 +39,17 @@ export class RecurringDonation extends BaseEntity { networkId: number; @Field() - @Column({ nullable: false }) - amount: number; + @Column({ nullable: true, default: 0, type: 'real' }) + amountStreamed?: number; + + @Field() + @Column({ nullable: true, default: 0, type: 'real' }) + totalUsdStreamed?: number; - // daily, weekly, monthly, yearly + // per second @Field() @Column({ nullable: false }) - interval: string; + flowRate: string; @Index() @Field() @@ -59,8 +67,8 @@ export class RecurringDonation extends BaseEntity { status: string; @Index() - @Field(type => Project) - @ManyToOne(type => Project) + @Field(_type => Project) + @ManyToOne(_type => Project) project: Project; @RelationId( @@ -73,13 +81,25 @@ export class RecurringDonation extends BaseEntity { @Field({ nullable: true }) finished: boolean; + @Column({ nullable: true, default: false }) + @Field({ nullable: true }) + isArchived: boolean; + + @Column({ nullable: true, default: false }) + @Field({ nullable: true }) + isBatch: boolean; + @Column({ nullable: true, default: false }) @Field({ nullable: true }) anonymous: boolean; + @Field({ nullable: true }) + @Column('text', { nullable: true }) + origin: string; + @Index() - @Field(type => AnchorContractAddress) - @ManyToOne(type => AnchorContractAddress) + @Field(_type => AnchorContractAddress) + @ManyToOne(_type => AnchorContractAddress, { eager: true }) anchorContractAddress: AnchorContractAddress; @RelationId( @@ -90,14 +110,18 @@ export class RecurringDonation extends BaseEntity { anchorContractAddressId: number; @Index() - @Field(type => User, { nullable: true }) - @ManyToOne(type => User, { eager: true, nullable: true }) + @Field(_type => User, { nullable: true }) + @ManyToOne(_type => User, { eager: true, nullable: true }) donor: User; @RelationId((recurringDonation: RecurringDonation) => recurringDonation.donor) @Column({ nullable: true }) donorId: number; + @Field(_type => [Donation], { nullable: true }) + @OneToMany(_type => Donation, donation => donation.recurringDonation) + donations?: Donation[]; + @UpdateDateColumn() @Field() updatedAt: Date; diff --git a/src/entities/referredEvent.ts b/src/entities/referredEvent.ts index 4bbe74a50..45d612848 100644 --- a/src/entities/referredEvent.ts +++ b/src/entities/referredEvent.ts @@ -5,45 +5,42 @@ import { Column, BaseEntity, RelationId, - ManyToOne, - Index, OneToOne, UpdateDateColumn, CreateDateColumn, JoinColumn, } from 'typeorm'; -import { Project, ProjectUpdate } from './project'; import { User } from './user'; @Entity() @ObjectType() export class ReferredEvent extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; - @Field(type => Date, { nullable: true }) + @Field(_type => Date, { nullable: true }) @Column({ nullable: true }) startTime?: Date; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) referrerId?: string; - @Field(type => Boolean, { nullable: false }) + @Field(_type => Boolean, { nullable: false }) @Column({ nullable: false, default: false }) isDonorLinkedToReferrer: boolean; - @Field(type => Boolean, { nullable: false }) + @Field(_type => Boolean, { nullable: false }) @Column({ nullable: false, default: false }) isDonorClickEventSent: boolean; - @Field(type => User, { nullable: true }) - @OneToOne(type => User, { nullable: true }) + @Field(_type => User, { nullable: true }) + @OneToOne(_type => User, { nullable: true }) @JoinColumn() user: User; - @Field(type => ID, { nullable: true }) + @Field(_type => ID, { nullable: true }) @RelationId((referredEvent: ReferredEvent) => referredEvent.user) @Column({ nullable: true }) userId: number; diff --git a/src/entities/socialProfile.ts b/src/entities/socialProfile.ts index adb44c418..97954c525 100644 --- a/src/entities/socialProfile.ts +++ b/src/entities/socialProfile.ts @@ -32,13 +32,13 @@ export class SocialProfile extends BaseEntity { * @see {@link https://github.com/Giveth/giveth-dapps-v2/issues/711#issuecomment-1128435255} */ - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @Index() - @Field(type => Project) - @ManyToOne(type => Project, { eager: true }) + @Field(_type => Project) + @ManyToOne(_type => Project, { eager: true }) project: Project; @RelationId((socialProfile: SocialProfile) => socialProfile.project) @@ -46,15 +46,15 @@ export class SocialProfile extends BaseEntity { projectId: number; @Index() - @Field(type => User, { nullable: true }) - @ManyToOne(type => User, { eager: true, nullable: true }) + @Field(_type => User, { nullable: true }) + @ManyToOne(_type => User, { eager: true, nullable: true }) user: User; @RelationId((socialProfile: SocialProfile) => socialProfile.user) userId: number; @Index() - @Field(type => ProjectVerificationForm) - @ManyToOne(type => ProjectVerificationForm) + @Field(_type => ProjectVerificationForm) + @ManyToOne(_type => ProjectVerificationForm) projectVerificationForm: ProjectVerificationForm; @RelationId( (socialProfile: SocialProfile) => socialProfile.projectVerificationForm, diff --git a/src/entities/sybil.ts b/src/entities/sybil.ts index 89581747f..5dce81090 100644 --- a/src/entities/sybil.ts +++ b/src/entities/sybil.ts @@ -15,20 +15,20 @@ import { QfRound } from './qfRound'; @Entity() @Unique(['userId', 'qfRoundId']) export class Sybil extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; - @Field(type => User) - @ManyToOne(type => User, { eager: true }) + @Field(_type => User) + @ManyToOne(_type => User, { eager: true }) user: User; @RelationId((sybil: Sybil) => sybil.user) @Column() userId: number; - @Field(type => QfRound) - @ManyToOne(type => QfRound, { eager: true }) + @Field(_type => QfRound) + @ManyToOne(_type => QfRound, { eager: true }) qfRound: QfRound; @RelationId((sybil: Sybil) => sybil.qfRound) diff --git a/src/entities/thirdPartyProjectImport.ts b/src/entities/thirdPartyProjectImport.ts index 314fb26b1..bf14ac06a 100644 --- a/src/entities/thirdPartyProjectImport.ts +++ b/src/entities/thirdPartyProjectImport.ts @@ -15,7 +15,7 @@ import { User } from './user'; @ObjectType() export class ThirdPartyProjectImport extends BaseEntity { // required always for entities - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @@ -30,8 +30,8 @@ export class ThirdPartyProjectImport extends BaseEntity { projectName: string; // History of who exported - @Field(type => User) - @ManyToOne(type => User) + @Field(_type => User) + @ManyToOne(_type => User) user?: User; @RelationId( @@ -41,8 +41,8 @@ export class ThirdPartyProjectImport extends BaseEntity { userId: number; // Link to project - @Field(type => Project) - @ManyToOne(type => Project) + @Field(_type => Project) + @ManyToOne(_type => Project) project?: Project; @RelationId( diff --git a/src/entities/token.ts b/src/entities/token.ts index 401e20e13..ceefec96b 100644 --- a/src/entities/token.ts +++ b/src/entities/token.ts @@ -1,4 +1,4 @@ -import { Field, ID, Int, ObjectType } from 'type-graphql'; +import { Field, ID, ObjectType } from 'type-graphql'; import { PrimaryGeneratedColumn, Column, @@ -14,7 +14,7 @@ import { ChainType } from '../types/network'; @ObjectType() @Index(['address', 'networkId'], { unique: true }) export class Token extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() id: number; @@ -40,7 +40,7 @@ export class Token extends BaseEntity { @Column() networkId: number; - @Field(type => String) + @Field(_type => String) @Column({ type: 'enum', enum: ChainType, @@ -57,25 +57,25 @@ export class Token extends BaseEntity { // 1 is the order with most priority, and null means it doesn't have any priority order?: number; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) @Column({ nullable: false, default: false }) isGivbackEligible: boolean; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) @Column({ nullable: true, default: false }) isStableCoin: boolean; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) // If we fill that, we will get price of this token from coingecko coingeckoId: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) // If we fill that, we will get price of this token from cryptocompare cryptoCompareId: string; - @ManyToMany(type => Organization, organization => organization.tokens, { + @ManyToMany(_type => Organization, organization => organization.tokens, { // make it true to show organizations in token page of adminjs panel eager: true, }) diff --git a/src/entities/user.ts b/src/entities/user.ts index 48be2620e..f3ea71cab 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -1,17 +1,15 @@ -import { Field, ID, ObjectType, Int, Float } from 'type-graphql'; +import { Field, Float, ID, Int, ObjectType } from 'type-graphql'; import { - PrimaryGeneratedColumn, - Column, - Entity, - OneToMany, - ManyToMany, BaseEntity, - UpdateDateColumn, + Column, CreateDateColumn, + Entity, JoinTable, + ManyToMany, + OneToMany, OneToOne, - JoinColumn, - RelationId, + PrimaryGeneratedColumn, + UpdateDateColumn, } from 'typeorm'; import { Project, ProjStatus, ReviewStatus } from './project'; import { Donation, DONATION_STATUS } from './donation'; @@ -22,6 +20,11 @@ import { ProjectVerificationForm } from './projectVerificationForm'; import { PowerBoosting } from './powerBoosting'; import { findPowerBoostingsCountByUserId } from '../repositories/powerBoostingRepository'; import { ReferredEvent } from './referredEvent'; +import { + RECURRING_DONATION_STATUS, + RecurringDonation, +} from './recurringDonation'; +import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; export const publicSelectionFields = [ 'user.id', @@ -45,12 +48,18 @@ export enum UserRole { OPERATOR = 'operator', VERIFICATION_FORM_REVIEWER = 'reviewer', CAMPAIGN_MANAGER = 'campaignManager', + QF_MANAGER = 'qfManager', } +export type UserStreamBalanceWarning = + | NOTIFICATIONS_EVENT_NAMES.SUPER_TOKENS_BALANCE_MONTH + | NOTIFICATIONS_EVENT_NAMES.SUPER_TOKENS_BALANCE_WEEK + | NOTIFICATIONS_EVENT_NAMES.SUPER_TOKENS_BALANCE_DEPLETED; + @ObjectType() @Entity() export class User extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @@ -61,56 +70,62 @@ export class User extends BaseEntity { }) role: UserRole; - @Field(type => [AccountVerification], { nullable: true }) + @Field(_type => [AccountVerification], { nullable: true }) @OneToMany( - type => AccountVerification, + _type => AccountVerification, accountVerification => accountVerification.user, ) accountVerifications?: AccountVerification[]; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) email?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) firstName?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) lastName?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) name?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true, unique: true }) walletAddress?: string; + @Column({ + type: 'json', + nullable: true, + }) + streamBalanceWarning?: Record; + @Column({ nullable: true }) password?: string; @Column({ nullable: true }) encryptedPassword?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) avatar?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) url?: string; - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) @Column({ type: 'real', nullable: true, default: null }) passportScore?: number; - @Field(type => Number, { nullable: true }) + @Field(_type => Number, { nullable: true }) @Column({ nullable: true, default: null }) passportStamps?: number; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) location?: string; @@ -123,19 +138,19 @@ export class User extends BaseEntity { @Column('bool', { default: false }) confirmed: boolean; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) chainvineId?: string; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) @Column('bool', { default: false }) wasReferred: boolean; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) @Column('bool', { default: false }) isReferrer: boolean; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) @Column('bool', { default: false }) // After each QF round Lauren and Griff review the donations and pass me a list of sybil addresses // And then we exclude qfRound donation from those addresses when calculating the real matchingFund @@ -147,8 +162,8 @@ export class User extends BaseEntity { }) referredEvent?: ReferredEvent; - @Field(type => [Project]) - @ManyToMany(type => Project, project => project.users) + @Field(_type => [Project]) + @ManyToMany(_type => Project, project => project.users) @JoinTable() projects?: Project[]; @@ -156,30 +171,30 @@ export class User extends BaseEntity { segmentIdentified: boolean; // Admin Reviewing Forms - @Field(type => [ProjectVerificationForm], { nullable: true }) + @Field(_type => [ProjectVerificationForm], { nullable: true }) @OneToMany( - type => ProjectVerificationForm, + _type => ProjectVerificationForm, projectVerificationForm => projectVerificationForm.reviewer, ) projectVerificationForms?: ProjectVerificationForm[]; - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) @Column({ type: 'real', nullable: true, default: 0 }) totalDonated: number; - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) @Column({ type: 'real', nullable: true, default: 0 }) totalReceived: number; - @Field(type => [ProjectStatusHistory], { nullable: true }) + @Field(_type => [ProjectStatusHistory], { nullable: true }) @OneToMany( - type => ProjectStatusHistory, + _type => ProjectStatusHistory, projectStatusHistory => projectStatusHistory.user, ) projectStatusHistories?: ProjectStatusHistory[]; - @Field(type => [PowerBoosting], { nullable: true }) - @OneToMany(type => PowerBoosting, powerBoosting => powerBoosting.user) + @Field(_type => [PowerBoosting], { nullable: true }) + @OneToMany(_type => PowerBoosting, powerBoosting => powerBoosting.user) powerBoostings?: PowerBoosting[]; @UpdateDateColumn() @@ -188,21 +203,37 @@ export class User extends BaseEntity { @CreateDateColumn() createdAt: Date; - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) projectsCount?: number; - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) async donationsCount() { - const query = await Donation.createQueryBuilder('donation') - .where(`donation."userId" = :id`, { id: this.id }) - .andWhere(`status = :status`, { + // Count for non-recurring donations + const nonRecurringDonationsCount = await Donation.createQueryBuilder( + 'donation', + ) + .where(`donation."userId" = :userId`, { userId: this.id }) + .andWhere(`donation.status = :status`, { status: DONATION_STATUS.VERIFIED, - }); + }) + .andWhere(`donation."recurringDonationId" IS NULL`) + .getCount(); + + // Count for recurring donations + const recurringDonationsCount = await RecurringDonation.createQueryBuilder( + 'recurring_donation', + ) + .where(`recurring_donation."donorId" = :donorId`, { donorId: this.id }) + .andWhere(`recurring_donation.status = :status`, { + status: RECURRING_DONATION_STATUS.ACTIVE, + }) + .getCount(); - return query.getCount(); + // Sum of both counts + return nonRecurringDonationsCount + recurringDonationsCount; } - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) async likedProjectsCount() { const likedProjectsCount = await Reaction.createQueryBuilder('reaction') .innerJoinAndSelect('reaction.project', 'project') @@ -216,7 +247,7 @@ export class User extends BaseEntity { return likedProjectsCount; } - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) async boostedProjectsCount() { return findPowerBoostingsCountByUserId(this.id); } diff --git a/src/entities/wallet.ts b/src/entities/wallet.ts index 08497dbd9..58d6e60bb 100644 --- a/src/entities/wallet.ts +++ b/src/entities/wallet.ts @@ -12,7 +12,7 @@ import { User } from './user'; @ObjectType() @Entity() export class Wallet extends BaseEntity { - @Field(type => ID) + @Field(_type => ID) @PrimaryGeneratedColumn() readonly id: number; @@ -20,8 +20,8 @@ export class Wallet extends BaseEntity { @Column('text', { unique: true }) address: string; - @Field(type => User) - @ManyToOne(type => User, { eager: true }) + @Field(_type => User) + @ManyToOne(_type => User, { eager: true }) user: User; @RelationId((donation: Wallet) => donation.user) @Column() diff --git a/src/middleware/apiGivAuthentication.ts b/src/middleware/apiGivAuthentication.ts index 337e62d12..19d83236c 100644 --- a/src/middleware/apiGivAuthentication.ts +++ b/src/middleware/apiGivAuthentication.ts @@ -1,13 +1,6 @@ import { NextFunction, Request, Response } from 'express'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; -import { - createBasicAuthentication, - decodeBasicAuthentication, -} from '../utils/utils'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; +import { decodeBasicAuthentication } from '../utils/utils'; import { logger } from '../utils/logger'; import { ApiGivStandardError, diff --git a/src/middleware/isAuth.ts b/src/middleware/isAuth.ts deleted file mode 100644 index af8f1fa33..000000000 --- a/src/middleware/isAuth.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MiddlewareFn } from 'type-graphql'; - -import { ApolloContext } from '../types/ApolloContext'; - -export const isAuth: MiddlewareFn = async ( - { context }, - next, -) => { - // @ts-ignore - if (!context?.req?.session?.userId) { - throw new Error('not authenticated'); - } - - return next(); -}; diff --git a/src/middleware/pinataUtils.ts b/src/middleware/pinataUtils.ts index 3b2be2e02..8505944a0 100644 --- a/src/middleware/pinataUtils.ts +++ b/src/middleware/pinataUtils.ts @@ -3,9 +3,8 @@ */ import pinataSDK, { PinataPinResponse } from '@pinata/sdk'; - -import ReadableStream = NodeJS.ReadableStream; import config from '../config'; +import ReadableStream = NodeJS.ReadableStream; let _pinata: pinataSDK; @@ -21,8 +20,7 @@ export const getPinata = (): pinataSDK => { export const pinFile = ( file: ReadableStream, - filename: String = 'untitled', - encoding: string, + filename: string = 'untitled', ): Promise => { return getPinata().pinFileToIPFS(file, { pinataMetadata: { name: filename.toString() }, @@ -32,7 +30,6 @@ export const pinFile = ( export const pinFileDataBase64 = ( fileDataBase64: string, filename: string = 'untitled', - encoding: string, ): Promise => { const array = fileDataBase64.split(','); const base64FileData = diff --git a/src/modules.d.ts b/src/modules.d.ts index 5c4866b32..45f122f3c 100644 --- a/src/modules.d.ts +++ b/src/modules.d.ts @@ -13,7 +13,6 @@ declare namespace NodeJS { DROP_DATABASE: string; SEED_PASSWORD: string; APOLLO_KEY: string; - REGISTER_USERNAME_PASSWORD: string; STRIPE_KEY: string; STRIPE_SECRET: string; diff --git a/src/orm.ts b/src/orm.ts index baaf36fa9..d1faa836e 100644 --- a/src/orm.ts +++ b/src/orm.ts @@ -2,7 +2,7 @@ import { DataSource } from 'typeorm'; import config from './config'; import { CronJob } from './entities/CronJob'; import { getEntities } from './entities/entities'; -import { redis, redisConfig } from './redis'; +import { redisConfig } from './redis'; export class AppDataSource { private static datasource: DataSource; diff --git a/src/ormconfig.ts b/src/ormconfig.ts index e1ea2e7ad..bfb4c35d1 100644 --- a/src/ormconfig.ts +++ b/src/ormconfig.ts @@ -1,5 +1,5 @@ -import * as dotenv from 'dotenv'; import * as path from 'path'; +import * as dotenv from 'dotenv'; const configPath = path.resolve( __dirname, @@ -10,7 +10,7 @@ const loadConfigResult = dotenv.config({ }); if (loadConfigResult.error) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('Load process.env error', { path: configPath, error: loadConfigResult.error, diff --git a/src/provider.ts b/src/provider.ts index fdaa15dd7..a3b972971 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -1,8 +1,7 @@ -import config from './config'; import { ethers } from 'ethers'; +import config from './config'; import { i18n, translationErrorMessagesKeys } from './utils/errorMessages'; -const INFURA_API_KEY = config.get('INFURA_API_KEY'); const INFURA_ID = config.get('INFURA_ID'); export const NETWORK_IDS = { @@ -12,7 +11,7 @@ export const NETWORK_IDS = { XDAI: 100, POLYGON: 137, OPTIMISTIC: 10, - OPTIMISM_GOERLI: 420, + OPTIMISM_SEPOLIA: 11155420, BSC: 56, CELO: 42220, CELO_ALFAJORES: 44787, @@ -28,6 +27,101 @@ export const NETWORK_IDS = { SOLANA_DEVNET: 103, }; +export const superTokensToToken = { + ETHx: 'ETH', + USDCx: 'USDC', + DAIx: 'DAI', + OPx: 'OP', + GIVx: 'GIV', +}; + +export const superTokens = [ + { + underlyingToken: { + decimals: 18, + id: '0x2f2c819210191750F2E11F7CfC5664a0eB4fd5e6', + name: 'Giveth', + symbol: 'GIV', + }, + decimals: 18, + id: '0xdfd824f6928b9776c031f7ead948090e2824ce8b', + name: 'fake Super Giveth Token', + symbol: 'GIVx', + }, + { + underlyingToken: { + name: 'Ethereum', + symbol: 'ETH', + decimals: 18, + id: '0x0000000000000000000000000000000000000000', + }, + decimals: 18, + id: '0x0043d7c85c8b96a49a72a92c0b48cdc4720437d7', + name: 'Super ETH', + symbol: 'ETHx', + }, + { + underlyingToken: { + decimals: 18, + id: '0x4200000000000000000000000000000000000042', + name: 'Optimism', + symbol: 'OP', + }, + decimals: 18, + id: '0x1828bff08bd244f7990eddcd9b19cc654b33cdb4', + name: 'Super Optimism', + symbol: 'OPx', + }, + { + underlyingToken: { + name: 'Ethereum', + symbol: 'ETH', + decimals: 18, + id: '0x0000000000000000000000000000000000000000', + }, + decimals: 18, + id: '0x4ac8bd1bdae47beef2d1c6aa62229509b962aa0d', + name: 'Super ETH', + symbol: 'ETHx', + }, + { + underlyingToken: { + decimals: 18, + id: '0x528cdc92eab044e1e39fe43b9514bfdab4412b98', + name: 'Giveth Token', + symbol: 'GIV', + }, + decimals: 18, + id: '0x4cab5b9930210e2edc6a905b9c75d615872a1a7e', + name: 'Super Giveth Token', + symbol: 'GIVx', + }, + { + underlyingToken: { + decimals: 18, + id: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', + name: 'Dai Stablecoin', + symbol: 'DAI', + }, + decimals: 18, + id: '0x7d342726b69c28d942ad8bfe6ac81b972349d524', + name: 'Super Dai Stablecoin', + symbol: 'DAIx', + }, + { + underlyingToken: { + decimals: 6, + id: '0x7f5c764cbc14f9669b88837ca1490cca17c31607', + name: 'USD Coin', + symbol: 'USDC', + }, + decimals: 18, + id: '0x8430f084b939208e2eded1584889c9a66b90562f', + name: 'Super USD Coin', + symbol: 'USDCx', + }, +]; + export const NETWORKS_IDS_TO_NAME = { 1: 'MAIN_NET', 3: 'ROPSTEN', @@ -38,7 +132,7 @@ export const NETWORKS_IDS_TO_NAME = { 42220: 'CELO', 44787: 'CELO_ALFAJORES', 10: 'OPTIMISTIC', - 420: 'OPTIMISM_GOERLI', + 11155420: 'OPTIMISM_SEPOLIA', 61: 'ETC', 63: 'MORDOR_ETC_TESTNET', 42161: 'ARBITRUM_MAINNET', @@ -53,7 +147,7 @@ const NETWORK_NAMES = { GOERLI: 'goerli', POLYGON: 'polygon-mainnet', OPTIMISTIC: 'optimistic-mainnet', - OPTIMISM_GOERLI: 'optimism-goerli-testnet', + OPTIMISM_SEPOLIA: 'optimism-sepolia-testnet', CELO: 'Celo', CELO_ALFAJORES: 'Celo Alfajores', ETC: 'Ethereum Classic', @@ -70,7 +164,7 @@ const NETWORK_NATIVE_TOKENS = { GOERLI: 'ETH', POLYGON: 'MATIC', OPTIMISTIC: 'ETH', - OPTIMISM_GOERLI: 'ETH', + OPTIMISM_SEPOLIA: 'ETH', CELO: 'CELO', CELO_ALFAJORES: 'CELO', ETC: 'ETC', @@ -116,9 +210,9 @@ const networkNativeTokensList = [ nativeToken: NETWORK_NATIVE_TOKENS.OPTIMISTIC, }, { - networkName: NETWORK_NAMES.OPTIMISM_GOERLI, - networkId: NETWORK_IDS.OPTIMISM_GOERLI, - nativeToken: NETWORK_NATIVE_TOKENS.OPTIMISM_GOERLI, + networkName: NETWORK_NAMES.OPTIMISM_SEPOLIA, + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + nativeToken: NETWORK_NATIVE_TOKENS.OPTIMISM_SEPOLIA, }, { networkName: NETWORK_NAMES.CELO, @@ -152,6 +246,16 @@ const networkNativeTokensList = [ }, ]; +export function getNetworkNameById(networkId: number): string { + const networkInfo = networkNativeTokensList.find( + item => item.networkId === networkId, + ); + if (!networkInfo) { + throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_NETWORK_ID)); + } + return networkInfo.networkName; +} + export function getNetworkNativeToken(networkId: number): string { const networkInfo = networkNativeTokensList.find(item => { return item.networkId === networkId; @@ -199,8 +303,8 @@ export function getProvider(networkId: number) { `https://celo-alfajores.infura.io/v3/${INFURA_ID}`; break; - case NETWORK_IDS.OPTIMISM_GOERLI: - url = `https://optimism-goerli.infura.io/v3/${INFURA_ID}`; + case NETWORK_IDS.OPTIMISM_SEPOLIA: + url = `https://optimism-sepolia.infura.io/v3/${INFURA_ID}`; break; case NETWORK_IDS.ARBITRUM_MAINNET: @@ -276,9 +380,9 @@ export function getBlockExplorerApiUrl(networkId: number): string { apiUrl = config.get('OPTIMISTIC_SCAN_API_URL'); apiKey = config.get('OPTIMISTIC_SCAN_API_KEY'); break; - case NETWORK_IDS.OPTIMISM_GOERLI: - apiUrl = config.get('OPTIMISTIC_SCAN_API_URL'); - apiKey = config.get('OPTIMISTIC_SCAN_API_KEY'); + case NETWORK_IDS.OPTIMISM_SEPOLIA: + apiUrl = config.get('OPTIMISTIC_SEPOLIA_SCAN_API_URL'); + apiKey = config.get('OPTIMISTIC_SEPOLIA_SCAN_API_KEY'); break; case NETWORK_IDS.ETC: // ETC network doesn't need API key diff --git a/src/repositories/anchorContractAddressRepository.test.ts b/src/repositories/anchorContractAddressRepository.test.ts index 22a2bbfed..49e6f369c 100644 --- a/src/repositories/anchorContractAddressRepository.test.ts +++ b/src/repositories/anchorContractAddressRepository.test.ts @@ -1,3 +1,4 @@ +import { assert } from 'chai'; import { addNewAnchorAddress } from './anchorContractAddressRepository'; import { createProjectData, @@ -7,7 +8,6 @@ import { saveUserDirectlyToDb, } from '../../test/testUtils'; import { NETWORK_IDS } from '../provider'; -import { assert } from 'chai'; describe('addNewAnchorAddressTestCases', addNewAnchorAddressTestCases); diff --git a/src/repositories/broadcastNotificationRepository.test.ts b/src/repositories/broadcastNotificationRepository.test.ts index 21685fd50..41e8c5304 100644 --- a/src/repositories/broadcastNotificationRepository.test.ts +++ b/src/repositories/broadcastNotificationRepository.test.ts @@ -1,3 +1,4 @@ +import { assert } from 'chai'; import BroadcastNotification, { BROAD_CAST_NOTIFICATION_STATUS, } from '../entities/broadcastNotification'; @@ -5,7 +6,6 @@ import { updateBroadcastNotificationStatus, findBroadcastNotificationById, } from './broadcastNotificationRepository'; -import { assert } from 'chai'; describe( 'updateBroadcastNotificationStatus test cases', diff --git a/src/repositories/campaignRepository.test.ts b/src/repositories/campaignRepository.test.ts index cf73e28f7..3213eb21e 100644 --- a/src/repositories/campaignRepository.test.ts +++ b/src/repositories/campaignRepository.test.ts @@ -1,10 +1,10 @@ +import { assert } from 'chai'; import { Campaign, CampaignType } from '../entities/campaign'; import { findAllActiveCampaigns, findCampaignBySlug, findFeaturedCampaign, } from './campaignRepository'; -import { assert } from 'chai'; import { findProjectById } from './projectRepository'; import { SEED_DATA } from '../../test/testUtils'; import { Project } from '../entities/project'; diff --git a/src/repositories/dbCronRepository.test.ts b/src/repositories/dbCronRepository.test.ts index 1201dcff1..2aab73df9 100644 --- a/src/repositories/dbCronRepository.test.ts +++ b/src/repositories/dbCronRepository.test.ts @@ -1,3 +1,4 @@ +import { assert } from 'chai'; import { dropDbCronExtension, EVERY_MINUTE_CRON_JOB_EXPRESSION, @@ -8,7 +9,6 @@ import { setupPgCronExtension, unSchedulePowerBoostingSnapshot, } from './dbCronRepository'; -import { assert } from 'chai'; import config from '../config'; import { createProjectData, @@ -247,7 +247,6 @@ describe('db cron job test', () => { }); await dropDbCronExtension(); await setupPgCronExtension(); - const EVERY_TEN_SECONDS_CRON_JOB_EXPRESSION = '*/10 * * * * *'; await schedulePowerBoostingSnapshot(EVERY_MINUTE_CRON_JOB_EXPRESSION); @@ -321,7 +320,6 @@ describe('db cron job test', () => { }); await dropDbCronExtension(); await setupPgCronExtension(); - const EVERY_TEN_SECONDS_CRON_JOB_EXPRESSION = '*/10 * * * * *'; await schedulePowerBoostingSnapshot(EVERY_MINUTE_CRON_JOB_EXPRESSION); diff --git a/src/repositories/donationRepository.test.ts b/src/repositories/donationRepository.test.ts index b93cd13a3..cf4f8f963 100644 --- a/src/repositories/donationRepository.test.ts +++ b/src/repositories/donationRepository.test.ts @@ -1,16 +1,16 @@ +import { assert } from 'chai'; +import moment from 'moment'; import { createDonationData, createProjectData, generateRandomEtheriumAddress, generateRandomEvmTxHash, - graphqlUrl, saveDonationDirectlyToDb, saveProjectDirectlyToDb, saveUserDirectlyToDb, SEED_DATA, } from '../../test/testUtils'; import { User, UserRole } from '../entities/user'; -import { assert, expect } from 'chai'; import { countUniqueDonors, countUniqueDonorsForRound, @@ -26,7 +26,6 @@ import { } from './donationRepository'; import { updateOldStableCoinDonationsPrice } from '../services/donationService'; import { Donation, DONATION_STATUS } from '../entities/donation'; -import moment from 'moment'; import { QfRound } from '../entities/qfRound'; import { Project } from '../entities/project'; import { diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index 8a1eb3fcd..6b778fbd3 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -1,8 +1,8 @@ +import { MoreThan } from 'typeorm'; +import moment from 'moment'; import { Project } from '../entities/project'; import { Donation, DONATION_STATUS } from '../entities/donation'; import { ResourcesTotalPerMonthAndYear } from '../resolvers/donationResolver'; -import { Brackets, MoreThan } from 'typeorm'; -import moment from 'moment'; import { AppDataSource } from '../orm'; import { getProjectDonationsSqrtRootSum } from './qfRoundRepository'; import { logger } from '../utils/logger'; @@ -139,6 +139,7 @@ export const donationsTotalAmountPerDateRange = async ( fromDate?: string, toDate?: string, fromOptimismOnly?: boolean, + onlyVerified?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select(`COALESCE(SUM(donation."valueUsd"), 0)`, 'sum') @@ -156,12 +157,18 @@ export const donationsTotalAmountPerDateRange = async ( query.andWhere(`donation."transactionNetworkId" = 10`); } + if (onlyVerified) { + query + .leftJoin('donation.project', 'project') + .andWhere('project.verified = true'); + } + const donationsUsdAmount = await query.getRawOne(); query.cache( `donationsTotalAmountPerDateRange-${fromDate || ''}-${toDate || ''}-${ fromOptimismOnly || 'all' - }`, + }-${onlyVerified || 'all'}`, 300000, ); @@ -172,6 +179,7 @@ export const donationsTotalAmountPerDateRangeByMonth = async ( fromDate?: string, toDate?: string, fromOptimismOnly?: boolean, + onlyVerified?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -192,6 +200,12 @@ export const donationsTotalAmountPerDateRangeByMonth = async ( query.andWhere(`donation."transactionNetworkId" = 10`); } + if (onlyVerified) { + query + .leftJoin('donation.project', 'project') + .andWhere('project.verified = true'); + } + query.groupBy('year, month'); query.orderBy('year', 'ASC'); query.addOrderBy('month', 'ASC'); @@ -199,7 +213,7 @@ export const donationsTotalAmountPerDateRangeByMonth = async ( query.cache( `donationsTotalAmountPerDateRangeByMonth-${fromDate || ''}-${ toDate || '' - }-${fromOptimismOnly || 'all'}`, + }-${fromOptimismOnly || 'all'}-${onlyVerified || 'all'}`, 300000, ); @@ -210,6 +224,7 @@ export const donationsNumberPerDateRange = async ( fromDate?: string, toDate?: string, fromOptimismOnly?: boolean, + onlyVerified?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select(`COALESCE(COUNT(donation.id), 0)`, 'count') @@ -227,12 +242,18 @@ export const donationsNumberPerDateRange = async ( query.andWhere(`donation."transactionNetworkId" = 10`); } + if (onlyVerified) { + query + .leftJoin('donation.project', 'project') + .andWhere('project.verified = true'); + } + const donationsUsdAmount = await query.getRawOne(); query.cache( `donationsTotalNumberPerDateRange-${fromDate || ''}-${toDate || ''}--${ fromOptimismOnly || 'all' - }`, + }-${onlyVerified || 'all'}`, 300000, ); @@ -243,6 +264,7 @@ export const donationsTotalNumberPerDateRangeByMonth = async ( fromDate?: string, toDate?: string, fromOptimismOnly?: boolean, + onlyVerified?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -262,6 +284,12 @@ export const donationsTotalNumberPerDateRangeByMonth = async ( query.andWhere(`donation."transactionNetworkId" = 10`); } + if (onlyVerified) { + query + .leftJoin('donation.project', 'project') + .andWhere('project.verified = true'); + } + query.groupBy('year, month'); query.orderBy('year', 'ASC'); query.addOrderBy('month', 'ASC'); @@ -269,7 +297,7 @@ export const donationsTotalNumberPerDateRangeByMonth = async ( query.cache( `donationsTotalNumberPerDateRangeByMonth-${fromDate || ''}-${ toDate || '' - }-${fromOptimismOnly || 'all'}`, + }-${fromOptimismOnly || 'all'}-${onlyVerified || 'all'}`, 300000, ); @@ -311,6 +339,39 @@ export const donorsCountPerDate = async ( return queryResult.count; }; +export const newDonorsCount = async (fromDate: string, toDate: string) => { + return Donation.createQueryBuilder('donation') + .select('donation.userId') + .addSelect('MIN(donation.createdAt)') + .groupBy('donation.userId') + .having('MIN(donation.createdAt) BETWEEN :fromDate AND :toDate', { + fromDate, + toDate, + }) + .groupBy('donation.userId') + .getRawMany(); +}; + +export const newDonorsDonationTotalUsd = async ( + fromDate: string, + toDate: string, +) => { + const result = await Donation.query( + `SELECT SUM(d."valueUsd") AS total_usd_value_of_first_donations +FROM ( + SELECT "userId", MIN("createdAt") AS firstDonationDate + FROM "donation" + GROUP BY "userId" +) AS first_donations +JOIN "donation" d ON first_donations."userId" = d."userId" AND first_donations.firstDonationDate = d."createdAt" +WHERE d."createdAt" BETWEEN $1 AND $2 + AND d."valueUsd" IS NOT NULL; +`, + [fromDate, toDate], + ); + return result[0]?.total_usd_value_of_first_donations || 0; +}; + export const donorsCountPerDateByMonthAndYear = async ( fromDate?: string, toDate?: string, diff --git a/src/repositories/draftRecurringDonationRepository.ts b/src/repositories/draftRecurringDonationRepository.ts new file mode 100644 index 000000000..41f009b85 --- /dev/null +++ b/src/repositories/draftRecurringDonationRepository.ts @@ -0,0 +1,57 @@ +import { logger } from '../utils/logger'; +import { + DRAFT_RECURRING_DONATION_STATUS, + DraftRecurringDonation, +} from '../entities/draftRecurringDonation'; + +// mark donation status matched based on fromWalletAddress, toWalletAddress, networkId, tokenAddress and amount +export async function markDraftRecurringDonationStatusMatched(params: { + matchedRecurringDonationId: number; + flowRate: string; + projectId: number; + networkId: number; + currency: string; +}): Promise { + try { + const { + networkId, + currency, + matchedRecurringDonationId, + projectId, + flowRate, + } = params; + await DraftRecurringDonation.update( + { + projectId, + flowRate, + networkId, + currency, + status: DRAFT_RECURRING_DONATION_STATUS.PENDING, + }, + { + status: DRAFT_RECURRING_DONATION_STATUS.MATCHED, + matchedRecurringDonationId, + }, + ); + } catch (e) { + logger.error( + `Error in markDraftRecurringDonationStatusMatched - params: ${params} - error: ${e.message}`, + ); + } +} + +export async function deleteExpiredDraftRecurringDonations(hours: number) { + try { + const expiredTime = new Date(Date.now() - hours * 60 * 60 * 1000); + + // donation is expired if it'screated before expiredTime + const result = await DraftRecurringDonation.createQueryBuilder() + .delete() + .where('createdAt < :expiredTime', { expiredTime }) + .execute(); + + logger.debug(`Expired draft donations removed: ${result.affected}`); + } catch (e) { + logger.error(`Error in removing expired draft donations, ${e.message}`); + } +} diff --git a/src/repositories/powerBalanceSnapshotRepository.ts b/src/repositories/powerBalanceSnapshotRepository.ts index 7d5e8ff34..58e70107e 100644 --- a/src/repositories/powerBalanceSnapshotRepository.ts +++ b/src/repositories/powerBalanceSnapshotRepository.ts @@ -1,5 +1,4 @@ import { PowerBalanceSnapshot } from '../entities/powerBalanceSnapshot'; -import { logger } from '../utils/logger'; type PowerBalanceSnapshotParams = Pick< PowerBalanceSnapshot, diff --git a/src/repositories/powerBoostingRepository.test.ts b/src/repositories/powerBoostingRepository.test.ts index 8ff00d941..1fcb39afc 100644 --- a/src/repositories/powerBoostingRepository.test.ts +++ b/src/repositories/powerBoostingRepository.test.ts @@ -1,3 +1,4 @@ +import { assert } from 'chai'; import { assertThrowsAsync, createProjectData, @@ -15,7 +16,6 @@ import { setSingleBoosting, takePowerBoostingSnapshot, } from './powerBoostingRepository'; -import { assert } from 'chai'; import { PowerBoosting } from '../entities/powerBoosting'; import { PowerSnapshot } from '../entities/powerSnapshot'; import { PowerBoostingSnapshot } from '../entities/powerBoostingSnapshot'; @@ -208,7 +208,7 @@ function findPowerBoostingsTestCases() { ); const firstProject = await saveProjectDirectlyToDb(createProjectData()); const secondProject = await saveProjectDirectlyToDb(createProjectData()); - const firstPower = await insertSinglePowerBoosting({ + await insertSinglePowerBoosting({ user: firstUser, project: firstProject, percentage: 1, @@ -932,10 +932,9 @@ function powerBoostingSnapshotTests() { const [snapshot] = await PowerSnapshot.find({ take: 1 }); assert.isDefined(snapshot); - const [powerBoostings, powerBoostingCounts] = - await PowerBoosting.findAndCount({ - select: ['id', 'projectId', 'userId', 'percentage'], - }); + const [powerBoostings] = await PowerBoosting.findAndCount({ + select: ['id', 'projectId', 'userId', 'percentage'], + }); const [powerBoostingSnapshots, powerBoostingSnapshotsCounts] = await PowerBoostingSnapshot.findAndCount({ where: { powerSnapshotId: snapshot?.id }, diff --git a/src/repositories/powerBoostingRepository.ts b/src/repositories/powerBoostingRepository.ts index 2db026762..d2082eb8e 100644 --- a/src/repositories/powerBoostingRepository.ts +++ b/src/repositories/powerBoostingRepository.ts @@ -1,7 +1,7 @@ +import { Brackets } from 'typeorm'; import { PowerBoosting } from '../entities/powerBoosting'; import { Project } from '../entities/project'; import { publicSelectionFields, User } from '../entities/user'; -import { Brackets } from 'typeorm'; import { logger } from '../utils/logger'; import { errorMessages, diff --git a/src/repositories/powerRoundRepository.test.ts b/src/repositories/powerRoundRepository.test.ts index fbda67efb..53136aaa1 100644 --- a/src/repositories/powerRoundRepository.test.ts +++ b/src/repositories/powerRoundRepository.test.ts @@ -1,5 +1,5 @@ -import { assertThrowsAsync } from '../../test/testUtils'; import { assert } from 'chai'; +import { assertThrowsAsync } from '../../test/testUtils'; import { getPowerRound, setPowerRound } from './powerRoundRepository'; import { PowerRound } from '../entities/powerRound'; diff --git a/src/repositories/powerSnapshotRepository.test.ts b/src/repositories/powerSnapshotRepository.test.ts index 7a0be06d7..51360e49b 100644 --- a/src/repositories/powerSnapshotRepository.test.ts +++ b/src/repositories/powerSnapshotRepository.test.ts @@ -1,10 +1,9 @@ +import { assert } from 'chai'; import { PowerSnapshot } from '../entities/powerSnapshot'; import { getPowerBoostingSnapshotWithoutBalance, updatePowerSnapshotSyncedFlag, } from './powerSnapshotRepository'; -import { assert } from 'chai'; -import moment from 'moment'; import { createProjectData, generateRandomEtheriumAddress, diff --git a/src/repositories/previousRoundRankRepository.test.ts b/src/repositories/previousRoundRankRepository.test.ts index ed3ae3cfa..ad3c5c00c 100644 --- a/src/repositories/previousRoundRankRepository.test.ts +++ b/src/repositories/previousRoundRankRepository.test.ts @@ -1,3 +1,4 @@ +import { assert } from 'chai'; import { copyProjectRanksToPreviousRoundRankTable, deleteAllPreviousRoundRanks, @@ -24,7 +25,6 @@ import { } from './projectPowerViewRepository'; import { Project } from '../entities/project'; import { PreviousRoundRank } from '../entities/previousRoundRank'; -import { assert } from 'chai'; import { ProjectPowerView } from '../views/projectPowerView'; import { findProjectById } from './projectRepository'; import { PowerRound } from '../entities/powerRound'; diff --git a/src/repositories/previousRoundRankRepository.ts b/src/repositories/previousRoundRankRepository.ts index d06fbb717..4ea964444 100644 --- a/src/repositories/previousRoundRankRepository.ts +++ b/src/repositories/previousRoundRankRepository.ts @@ -1,6 +1,5 @@ import { PowerSnapshot } from '../entities/powerSnapshot'; import { PreviousRoundRank } from '../entities/previousRoundRank'; -import { ProjectsHaveNewRankingInputParam } from '../adapters/notifications/NotificationAdapterInterface'; export const deleteAllPreviousRoundRanks = async () => { return PreviousRoundRank.query( diff --git a/src/repositories/projectAddressRepository.test.ts b/src/repositories/projectAddressRepository.test.ts index 87cb5040d..5debdec7f 100644 --- a/src/repositories/projectAddressRepository.test.ts +++ b/src/repositories/projectAddressRepository.test.ts @@ -1,3 +1,4 @@ +import { assert } from 'chai'; import { addBulkNewProjectAddress, addNewProjectAddress, @@ -16,7 +17,6 @@ import { saveProjectDirectlyToDb, saveUserDirectlyToDb, } from '../../test/testUtils'; -import { assert } from 'chai'; import { NETWORK_IDS } from '../provider'; import { ProjectStatus } from '../entities/projectStatus'; import { ProjStatus } from '../entities/project'; @@ -211,9 +211,8 @@ function addBulkNewProjectAddressTestCases() { user, }, ]); - const newRelatedAddress = await findRelatedAddressByWalletAddress( - newAddress, - ); + const newRelatedAddress = + await findRelatedAddressByWalletAddress(newAddress); assert.isOk(newRelatedAddress); assert.equal(newRelatedAddress?.address, newAddress); assert.isFalse(newRelatedAddress?.isRecipient); @@ -253,16 +252,14 @@ function addBulkNewProjectAddressTestCases() { user, }, ]); - const newRelatedAddress1 = await findRelatedAddressByWalletAddress( - newAddress1, - ); + const newRelatedAddress1 = + await findRelatedAddressByWalletAddress(newAddress1); assert.isOk(newRelatedAddress1); assert.equal(newRelatedAddress1?.address, newAddress1); assert.equal(newRelatedAddress1?.project?.id, project.id); assert.isFalse(newRelatedAddress1?.isRecipient); - const newRelatedAddress2 = await findRelatedAddressByWalletAddress( - newAddress2, - ); + const newRelatedAddress2 = + await findRelatedAddressByWalletAddress(newAddress2); assert.isOk(newRelatedAddress2); assert.equal(newRelatedAddress2?.address, newAddress2); assert.equal(newRelatedAddress2?.project?.id, project.id); diff --git a/src/repositories/projectPowerViewRepository.ts b/src/repositories/projectPowerViewRepository.ts index 29e93f3fe..dd6c34ae9 100644 --- a/src/repositories/projectPowerViewRepository.ts +++ b/src/repositories/projectPowerViewRepository.ts @@ -1,10 +1,10 @@ import { Not, MoreThan } from 'typeorm'; +import { FindOneOptions } from 'typeorm/find-options/FindOneOptions'; import { ProjectPowerView } from '../views/projectPowerView'; import { ProjectFuturePowerView } from '../views/projectFuturePowerView'; import { logger } from '../utils/logger'; import { updatePowerSnapshotSyncedFlag } from './powerSnapshotRepository'; import { LastSnapshotProjectPowerView } from '../views/lastSnapshotProjectPowerView'; -import { FindOneOptions } from 'typeorm/find-options/FindOneOptions'; import { AppDataSource } from '../orm'; export const getProjectPowers = async ( diff --git a/src/repositories/projectRepository.test.ts b/src/repositories/projectRepository.test.ts index 5975459b3..4bf7b678f 100644 --- a/src/repositories/projectRepository.test.ts +++ b/src/repositories/projectRepository.test.ts @@ -1,3 +1,5 @@ +import { assert } from 'chai'; +import moment from 'moment'; import { findProjectById, findProjectBySlug, @@ -16,11 +18,9 @@ import { saveProjectDirectlyToDb, saveUserDirectlyToDb, } from '../../test/testUtils'; -import { assert } from 'chai'; import { createProjectVerificationForm } from './projectVerificationRepository'; import { PROJECT_VERIFICATION_STATUSES } from '../entities/projectVerificationForm'; import { NETWORK_IDS } from '../provider'; -import moment from 'moment'; import { setPowerRound } from './powerRoundRepository'; import { refreshProjectPowerView } from './projectPowerViewRepository'; import { @@ -467,9 +467,8 @@ function updateDescriptionSummaryTestCases() { }); it('should update description summary on update', async () => { - let project: Project | null = await saveProjectDirectlyToDb( - createProjectData(), - ); + let project: Project | null = + await saveProjectDirectlyToDb(createProjectData()); project.description = SHORT_DESCRIPTION; await project.save(); diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 743cde642..1b340e0d9 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -2,22 +2,19 @@ import { UpdateResult } from 'typeorm'; import { FilterField, Project, - ProjectUpdate, ProjStatus, ReviewStatus, + RevokeSteps, SortingField, } from '../entities/project'; import { ProjectVerificationForm } from '../entities/projectVerificationForm'; import { ProjectAddress } from '../entities/projectAddress'; import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; -import { User, publicSelectionFields } from '../entities/user'; +import { publicSelectionFields } from '../entities/user'; import { ResourcesTotalPerMonthAndYear } from '../resolvers/donationResolver'; import { OrderDirection, ProjectResolver } from '../resolvers/projectResolver'; -import { ChainType } from '../types/network'; -import { - getAppropriateNetworkId, - getDefaultSolanaChainId, -} from '../services/chains'; +import { getAppropriateNetworkId } from '../services/chains'; + export const findProjectById = (projectId: number): Promise => { // return Project.findOne({ id: projectId }); @@ -25,6 +22,7 @@ export const findProjectById = (projectId: number): Promise => { .leftJoinAndSelect('project.status', 'status') .leftJoinAndSelect('project.organization', 'organization') .leftJoinAndSelect('project.addresses', 'addresses') + .leftJoinAndSelect('project.socialMedia', 'socialMedia') .leftJoinAndSelect('project.anchorContracts', 'anchor_contract_address') .leftJoinAndSelect('project.qfRounds', 'qfRounds') .leftJoin('project.adminUser', 'user') @@ -36,7 +34,7 @@ export const findProjectById = (projectId: number): Promise => { }; export const verifiedProjectsAddressesWithOptimism = async (): Promise< - String[] + string[] > => { const recipients = await Project.createQueryBuilder('project') .select('LOWER(addresses.address) AS recipient') @@ -217,6 +215,27 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { .addOrderBy(`project.verified`, OrderDirection.DESC); } break; + case SortingField.EstimatedMatching: + if (activeQfRoundId) { + query + .leftJoin( + 'project.projectEstimatedMatchingView', + 'projectEstimatedMatchingView', + 'projectEstimatedMatchingView.qfRoundId = :qfRoundId', + { qfRoundId: activeQfRoundId }, + ) + .addSelect([ + 'projectEstimatedMatchingView.sqrtRootSum', + 'projectEstimatedMatchingView.qfRoundId', + ]) + .orderBy( + 'projectEstimatedMatchingView.sqrtRootSum', + OrderDirection.DESC, + 'NULLS LAST', + ) + .addOrderBy(`project.verified`, OrderDirection.DESC); + } + break; default: query .orderBy('projectInstantPower.totalPower', OrderDirection.DESC) @@ -227,19 +246,48 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { return query.take(limit).skip(skip); }; -export const projectsWithoutUpdateAfterTimeFrame = async (date: Date) => { - return Project.createQueryBuilder('project') +export const projectsWithoutUpdateAfterTimeFrame = async ( + date: Date, +): Promise => { + const projectsWithLatestUpdateBeforeCutOff = await Project.createQueryBuilder( + 'project', + ) + .leftJoin('project.projectUpdates', 'projectUpdates') + .select('project.id', 'projectId') + .addSelect('MAX(projectUpdates.createdAt)', 'latestUpdate') + .groupBy('project.id') + .having('MAX(projectUpdates.createdAt) < :date', { date }) + .getRawMany(); + + const validProjectIds = projectsWithLatestUpdateBeforeCutOff.map( + item => item.projectId, + ); + + const projects = await Project.createQueryBuilder('project') + .where('project.isImported = false') + .andWhere('project.verified = true') + .andWhere( + '(project.verificationStatus NOT IN (:...statuses) OR project.verificationStatus IS NULL)', + { + statuses: [RevokeSteps.UpForRevoking, RevokeSteps.Revoked], + }, + ) + .andWhereInIds(validProjectIds) .leftJoinAndSelect( 'project.projectVerificationForm', 'projectVerificationForm', ) - .leftJoin('project.adminUser', 'user') - .where('project.isImported = false') - .andWhere('project.verified = true') - .andWhere('project.updatedAt < :badgeRevokingDate', { - badgeRevokingDate: date, - }) + .leftJoinAndSelect('project.adminUser', 'user') + .leftJoinAndSelect('project.projectUpdates', 'projectUpdates') .getMany(); + + projects.forEach(project => { + project.projectUpdates?.sort((a, b) => { + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + }); + }); + + return projects; }; export const findProjectBySlug = (slug: string): Promise => { diff --git a/src/repositories/projectSatusHistoryRepository.ts b/src/repositories/projectSatusHistoryRepository.ts index 0f274ac34..2b26e65a4 100644 --- a/src/repositories/projectSatusHistoryRepository.ts +++ b/src/repositories/projectSatusHistoryRepository.ts @@ -1,5 +1,4 @@ import { ProjectStatusHistory } from '../entities/projectStatusHistory'; -import { SocialProfile } from '../entities/socialProfile'; export const findOneProjectStatusHistoryByProjectId = ( projectId: number, diff --git a/src/repositories/projectSocialMediaRepository.ts b/src/repositories/projectSocialMediaRepository.ts new file mode 100644 index 000000000..45e874dca --- /dev/null +++ b/src/repositories/projectSocialMediaRepository.ts @@ -0,0 +1,33 @@ +import { ProjectSocialMedia } from '../entities/projectSocialMedia'; +import { ProjectSocialMediaType } from '../types/projectSocialMediaType'; + +export const addBulkProjectSocialMedia = async ( + socialMediaArray: { + projectId: number; + userId: number; + type: ProjectSocialMediaType; + link: string; + }[], +): Promise => { + const socialMediaEntities = socialMediaArray.map(socialMediaInput => { + const socialMedia = ProjectSocialMedia.create({ + type: socialMediaInput.type, + link: socialMediaInput.link, + projectId: socialMediaInput.projectId, + userId: socialMediaInput.userId, + }); + return socialMedia.save(); + }); + await Promise.all(socialMediaEntities); +}; + +export const removeProjectSocialMedia = async ( + projectId: number, +): Promise => { + const socialMediaLinks = await ProjectSocialMedia.find({ + where: { projectId }, + }); + if (socialMediaLinks.length > 0) { + await ProjectSocialMedia.remove(socialMediaLinks); + } +}; diff --git a/src/repositories/projectUpdateRepository.test.ts b/src/repositories/projectUpdateRepository.test.ts index 8151c13e9..21a263624 100644 --- a/src/repositories/projectUpdateRepository.test.ts +++ b/src/repositories/projectUpdateRepository.test.ts @@ -1,8 +1,8 @@ +import { assert } from 'chai'; import { createProjectData, saveProjectDirectlyToDb, } from '../../test/testUtils'; -import { assert } from 'chai'; import { ProjectUpdate } from '../entities/project'; import { SUMMARY_LENGTH } from '../constants/summary'; import { getHtmlTextSummary } from '../utils/utils'; diff --git a/src/repositories/projectVerificationRepository.test.ts b/src/repositories/projectVerificationRepository.test.ts index 8861e7cea..cd2a9e719 100644 --- a/src/repositories/projectVerificationRepository.test.ts +++ b/src/repositories/projectVerificationRepository.test.ts @@ -1,10 +1,10 @@ +import { assert } from 'chai'; import { ManagingFunds, Milestones, PROJECT_VERIFICATION_STATUSES, PROJECT_VERIFICATION_STEPS, ProjectContacts, - ProjectVerificationForm, } from '../entities/projectVerificationForm'; import { createProjectData, @@ -28,9 +28,7 @@ import { verifyForm, verifyMultipleForms, } from './projectVerificationRepository'; -import { assert } from 'chai'; import { ChainType } from '../types/network'; -import { ProjectAddress } from '../entities/projectAddress'; describe( 'createProjectVerificationForm test cases', @@ -112,9 +110,7 @@ function updateProjectPersonalInfoOfProjectVerificationTestCases() { personalInfo: projectPersonalInfo, }); - const test = await findProjectVerificationFormById( - projectVerificationForm.id, - ); + await findProjectVerificationFormById(projectVerificationForm.id); assert.equal( updatedProjectVerification?.personalInfo.email, projectPersonalInfo.email, diff --git a/src/repositories/projectVerificationRepository.ts b/src/repositories/projectVerificationRepository.ts index 31632256f..d101ff977 100644 --- a/src/repositories/projectVerificationRepository.ts +++ b/src/repositories/projectVerificationRepository.ts @@ -1,3 +1,4 @@ +import { UpdateResult } from 'typeorm'; import { ManagingFunds, Milestones, @@ -10,18 +11,9 @@ import { } from '../entities/projectVerificationForm'; import { findProjectById } from './projectRepository'; import { findUserById } from './userRepository'; -import { UpdateResult } from 'typeorm'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { User } from '../entities/user'; -import { - getAppropriateNetworkId, - getDefaultSolanaChainId, -} from '../services/chains'; -import { ChainType } from '../types/network'; +import { getAppropriateNetworkId } from '../services/chains'; export const createProjectVerificationForm = async (params: { userId: number; diff --git a/src/repositories/qfRoundHistoryRepository.test.ts b/src/repositories/qfRoundHistoryRepository.test.ts index c4f46bf7d..16e07213d 100644 --- a/src/repositories/qfRoundHistoryRepository.test.ts +++ b/src/repositories/qfRoundHistoryRepository.test.ts @@ -1,3 +1,5 @@ +import { assert, expect } from 'chai'; +import moment from 'moment'; import { createDonationData, createProjectData, @@ -8,9 +10,7 @@ import { saveUserDirectlyToDb, } from '../../test/testUtils'; import { QfRound } from '../entities/qfRound'; -import { assert, expect } from 'chai'; import { Project } from '../entities/project'; -import moment from 'moment'; import { fillQfRoundHistory, diff --git a/src/repositories/qfRoundRepository.test.ts b/src/repositories/qfRoundRepository.test.ts index 60f90b6e8..61bf88402 100644 --- a/src/repositories/qfRoundRepository.test.ts +++ b/src/repositories/qfRoundRepository.test.ts @@ -1,3 +1,5 @@ +import { assert, expect } from 'chai'; +import moment from 'moment'; import { createDonationData, createProjectData, @@ -7,7 +9,6 @@ import { saveUserDirectlyToDb, } from '../../test/testUtils'; import { QfRound } from '../entities/qfRound'; -import { assert, expect } from 'chai'; import { deactivateExpiredQfRounds, findQfRoundById, @@ -17,7 +18,6 @@ import { getQfRoundTotalProjectsDonationsSum, } from './qfRoundRepository'; import { Project } from '../entities/project'; -import moment from 'moment'; import { refreshProjectDonationSummaryView, refreshProjectEstimatedMatchingView, @@ -101,7 +101,7 @@ function getProjectDonationsSqrRootSumTests() { it('should return correct value on multiple donations', async () => { const valuesUsd = [4, 25, 100, 1024]; await Promise.all( - valuesUsd.map(async (valueUsd, index) => { + valuesUsd.map(async valueUsd => { const user = await saveUserDirectlyToDb( generateRandomEtheriumAddress(), ); diff --git a/src/repositories/qfRoundRepository.ts b/src/repositories/qfRoundRepository.ts index 5943bbb1b..45ad7b437 100644 --- a/src/repositories/qfRoundRepository.ts +++ b/src/repositories/qfRoundRepository.ts @@ -1,7 +1,5 @@ import { QfRound } from '../entities/qfRound'; import { AppDataSource } from '../orm'; -import { logger } from '../utils/logger'; -import { Field } from 'type-graphql'; const qfRoundEstimatedMatchingParamsCacheDuration = Number( process.env.QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION || 60000, @@ -31,33 +29,6 @@ export const findQfRoundBySlug = async ( .getOne(); }; -export const relateManyProjectsToQfRound = async (params: { - projectIds: number[]; - qfRoundId: number; - add: boolean; -}) => { - const values = params.projectIds - .map(projectId => `(${projectId}, ${params.qfRoundId})`) - .join(', '); - - let query; - - if (params.add) { - query = ` - INSERT INTO project_qf_rounds_qf_round ("projectId", "qfRoundId") - VALUES ${values} - ON CONFLICT ("projectId", "qfRoundId") DO NOTHING;`; - } else { - const projectIds = params.projectIds.join(','); - query = ` - DELETE FROM project_qf_rounds_qf_round - WHERE "qfRoundId" = ${params.qfRoundId} - AND "projectId" IN (${projectIds});`; - } - - return QfRound.query(query); -}; - export async function getProjectDonationsSqrtRootSum( projectId: number, qfRoundId: number, diff --git a/src/repositories/qfRoundRepository2.ts b/src/repositories/qfRoundRepository2.ts new file mode 100644 index 000000000..5ad213457 --- /dev/null +++ b/src/repositories/qfRoundRepository2.ts @@ -0,0 +1,80 @@ +import { QfRound } from '../entities/qfRound'; +import { getOrttoPersonAttributes } from '../adapters/notifications/NotificationCenterAdapter'; +import { getNotificationAdapter } from '../adapters/adaptersFactory'; +import { AppDataSource } from '../orm'; +import { Project } from '../entities/project'; +import { OrttoPerson } from '../adapters/notifications/NotificationAdapterInterface'; + +// The repository functions that uses Project entity should be here +export const relateManyProjectsToQfRound = async (params: { + projectIds: number[]; + qfRound: QfRound; + add: boolean; +}) => { + const values = params.projectIds + .map(projectId => `(${projectId}, ${params.qfRound.id})`) + .join(', '); + + let query; + let orttoPeople: OrttoPerson[] = []; + const projects = await Promise.all( + params.projectIds.map(id => Project.findOne({ where: { id } })), + ); + + if (params.add) { + query = ` + INSERT INTO project_qf_rounds_qf_round ("projectId", "qfRoundId") + VALUES ${values} + ON CONFLICT ("projectId", "qfRoundId") DO NOTHING;`; + orttoPeople = projects.map(project => + getOrttoPersonAttributes({ + firstName: project?.adminUser?.firstName, + lastName: project?.adminUser?.lastName, + email: project?.adminUser?.email, + userId: project?.adminUser.id?.toString(), + QFProjectOwnerAdded: params.qfRound.name, + }), + ); + } else { + const projectIds = params.projectIds.join(','); + query = ` + DELETE FROM project_qf_rounds_qf_round + WHERE "qfRoundId" = ${params.qfRound.id} + AND "projectId" IN (${projectIds});`; + + const qfRoundProjects = await Project.createQueryBuilder('project') + .leftJoin('project.qfRounds', 'qfRound') + .where('qfRound.id = :qfRoundId', { qfRoundId: params.qfRound.id }) + .leftJoinAndSelect('project.adminUser', 'adminUser') + .getMany(); + + const projectsAdminIds = projects.map(project => project?.adminUser.id); + const projectsUniqueAdminIds = [...new Set(projectsAdminIds)]; + const projectsToRemoveFromOrtto: Project[] = []; + projectsUniqueAdminIds.forEach(id => { + const toRemoveProjects = projects.filter( + project => project?.adminUser.id === id, + ); + const userQFRoundProjectsCount = qfRoundProjects.filter( + project => project.adminUser.id === id, + ).length; + if (toRemoveProjects.length === userQFRoundProjectsCount) { + projectsToRemoveFromOrtto.push(toRemoveProjects[0]!); + } + }); + // We should remove the tag only if user has no other projects in the round + orttoPeople = projectsToRemoveFromOrtto.map(project => + getOrttoPersonAttributes({ + firstName: project?.adminUser?.firstName, + lastName: project?.adminUser?.lastName, + email: project?.adminUser?.email, + userId: project?.adminUser.id?.toString(), + QFProjectOwnerRemoved: params.qfRound.name, + }), + ); + } + if (orttoPeople.length > 0) { + await getNotificationAdapter().updateOrttoPeople(orttoPeople); + } + return AppDataSource.getDataSource().query(query); +}; diff --git a/src/repositories/recurringDonationRepository.test.ts b/src/repositories/recurringDonationRepository.test.ts index 01300685e..e8b49bc0d 100644 --- a/src/repositories/recurringDonationRepository.test.ts +++ b/src/repositories/recurringDonationRepository.test.ts @@ -1,20 +1,148 @@ +import moment from 'moment'; +import { assert } from 'chai'; import { + createDonationData, createProjectData, generateRandomEtheriumAddress, generateRandomEvmTxHash, + saveDonationDirectlyToDb, saveProjectDirectlyToDb, saveUserDirectlyToDb, } from '../../test/testUtils'; import { NETWORK_IDS } from '../provider'; -import { assert } from 'chai'; import { addNewAnchorAddress } from './anchorContractAddressRepository'; -import { createNewRecurringDonation } from './recurringDonationRepository'; +import { + countOfActiveRecurringDonationsByProjectId, + createNewRecurringDonation, + findRecurringDonationById, + findRecurringDonationByProjectIdAndUserIdAndCurrency, + updateRecurringDonationFromTheStreamDonations, +} from './recurringDonationRepository'; +import { getPendingRecurringDonationsIds } from './recurringDonationRepository'; +import { DONATION_STATUS } from '../entities/donation'; +import { RECURRING_DONATION_STATUS } from '../entities/recurringDonation'; describe( 'createNewRecurringDonationTestCases', createNewRecurringDonationTestCases, ); +describe( + 'findRecurringDonationByProjectIdAndUserIdTestCases', + findRecurringDonationByProjectIdAndUserIdTestCases, +); +describe( + 'countOfActiveRecurringDonationsByProjectIdTestCases', + countOfActiveRecurringDonationsByProjectIdTestCases, +); + +describe( + 'getPendingRecurringDonationsIds() test cases', + getPendingRecurringDonationsIdsTestCases, +); +describe( + 'updateRecurringDonationFromTheStreamDonations() test cases', + updateRecurringDonationFromTheStreamDonationsTestCases, +); + +function getPendingRecurringDonationsIdsTestCases() { + it('should return pending donations in last 48 hours', async () => { + const pendingRecurringDonations = await getPendingRecurringDonationsIds(); + + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const creator = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const anchorAddress = generateRandomEtheriumAddress(); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator, + address: anchorAddress, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const currency = 'USD'; + const recurringDonation = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: creator, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + // recurringDonation.status = RECURRING_DONATION_STATUS.PENDING + // await recurringDonation.save() + + const oldDonation = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: creator, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + oldDonation.createdAt = moment() + .subtract({ + hours: + Number(process.env.RECURRING_DONATION_VERIFICAITON_EXPIRATION_HOURS) + + 2, + }) + .toDate(); + await oldDonation.save(); + + const oldDonation2 = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: creator, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + oldDonation2.createdAt = moment() + .subtract({ + hours: + Number(process.env.RECURRING_DONATION_VERIFICAITON_EXPIRATION_HOURS) + + 2, + }) + .toDate(); + await oldDonation2.save(); + + const newPendingDonations = await getPendingRecurringDonationsIds(); + + assert.equal( + newPendingDonations.length, + pendingRecurringDonations.length + 1, + ); + assert.isOk( + newPendingDonations.find( + donation => donation.id === recurringDonation.id, + ), + ); + assert.notOk( + newPendingDonations.find(donation => donation.id === oldDonation.id), + ); + assert.notOk( + newPendingDonations.find(donation => donation.id === oldDonation2.id), + ); + }); +} + function createNewRecurringDonationTestCases() { it('should create recurring donation successfully', async () => { const projectOwner = await saveUserDirectlyToDb( @@ -41,10 +169,11 @@ function createNewRecurringDonationTestCases() { networkId: NETWORK_IDS.OPTIMISTIC, donor: creator, anchorContractAddress, - amount: 100, + flowRate: '100', currency: 'USD', - interval: 'monthly', project, + anonymous: false, + isBatch: false, }); assert.isNotNull(recurringDonation); @@ -55,3 +184,296 @@ function createNewRecurringDonationTestCases() { assert.equal(recurringDonation.donor.id, creator.id); }); } + +function findRecurringDonationByProjectIdAndUserIdTestCases() { + it('should find recurring donation successfully', async () => { + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const creator = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const anchorAddress = generateRandomEtheriumAddress(); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator, + address: anchorAddress, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const currency = 'USD'; + const recurringDonation = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: creator, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + const foundRecurringDonation = + await findRecurringDonationByProjectIdAndUserIdAndCurrency({ + projectId: project.id, + userId: creator.id, + currency, + }); + assert.equal(foundRecurringDonation?.id, recurringDonation.id); + }); +} + +function countOfActiveRecurringDonationsByProjectIdTestCases() { + it('should return count correctly', async () => { + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const creator = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const anchorAddress = generateRandomEtheriumAddress(); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator, + address: anchorAddress, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const currency = 'USD'; + const recurringDonation = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: creator, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + recurringDonation.status = RECURRING_DONATION_STATUS.ACTIVE; + await recurringDonation.save(); + const count = await countOfActiveRecurringDonationsByProjectId(project.id); + assert.equal(count, 1); + }); + it('should return count correctly, when there is more than 1 active recurring donation', async () => { + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const creator = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const anchorAddress = generateRandomEtheriumAddress(); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator, + address: anchorAddress, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const currency = 'USD'; + + const recurringDonation = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: creator, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + recurringDonation.status = RECURRING_DONATION_STATUS.ACTIVE; + await recurringDonation.save(); + + const recurringDonation2 = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: creator, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + recurringDonation2.status = RECURRING_DONATION_STATUS.ACTIVE; + await recurringDonation2.save(); + + const count = await countOfActiveRecurringDonationsByProjectId(project.id); + assert.equal(count, 2); + }); + it('should return count correctly, when there is active and non active donations', async () => { + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const creator = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const anchorAddress = generateRandomEtheriumAddress(); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator, + address: anchorAddress, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const currency = 'USD'; + + const recurringDonation1 = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: creator, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + recurringDonation1.status = RECURRING_DONATION_STATUS.ACTIVE; + await recurringDonation1.save(); + + const recurringDonation2 = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: creator, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + recurringDonation2.status = RECURRING_DONATION_STATUS.PENDING; + await recurringDonation2.save(); + await recurringDonation1.save(); + + const recurringDonation3 = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: creator, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + recurringDonation3.status = RECURRING_DONATION_STATUS.ENDED; + await recurringDonation3.save(); + + const recurringDonation4 = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: creator, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + recurringDonation4.status = RECURRING_DONATION_STATUS.FAILED; + await recurringDonation4.save(); + + const count = await countOfActiveRecurringDonationsByProjectId(project.id); + assert.equal(count, 1); + }); +} + +function updateRecurringDonationFromTheStreamDonationsTestCases() { + it('should fill amountStreamed, totalUsdStreamed correctly', async () => { + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const anchorAddress = generateRandomEtheriumAddress(); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator: donor, + address: anchorAddress, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const currency = 'USDT'; + const recurringDonation = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: donor, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + const d1 = await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: DONATION_STATUS.VERIFIED, + amount: 13, + valueUsd: 15, + }, + donor.id, + project.id, + ); + d1.recurringDonation = recurringDonation; + await d1.save(); + + const d2 = await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: DONATION_STATUS.VERIFIED, + amount: 12, + valueUsd: 14, + }, + donor.id, + project.id, + ); + d2.recurringDonation = recurringDonation; + await d2.save(); + + await updateRecurringDonationFromTheStreamDonations(recurringDonation.id); + const updatedRecurringDonation = await findRecurringDonationById( + recurringDonation.id, + ); + + assert.equal( + updatedRecurringDonation?.totalUsdStreamed, + d1.valueUsd + d2.valueUsd, + ); + assert.equal( + updatedRecurringDonation?.amountStreamed, + d1.amount + d2.amount, + ); + }); +} diff --git a/src/repositories/recurringDonationRepository.ts b/src/repositories/recurringDonationRepository.ts index 1f6dd58d1..0635dba4b 100644 --- a/src/repositories/recurringDonationRepository.ts +++ b/src/repositories/recurringDonationRepository.ts @@ -1,8 +1,13 @@ +import moment from 'moment'; +import { MoreThan } from 'typeorm'; import { Project } from '../entities/project'; import { User } from '../entities/user'; -import { RecurringDonation } from '../entities/recurringDonation'; +import { + RECURRING_DONATION_STATUS, + RecurringDonation, +} from '../entities/recurringDonation'; import { AnchorContractAddress } from '../entities/anchorContractAddress'; -import { Donation } from '../entities/donation'; +import { logger } from '../utils/logger'; export const createNewRecurringDonation = async (params: { project: Project; @@ -10,9 +15,10 @@ export const createNewRecurringDonation = async (params: { anchorContractAddress: AnchorContractAddress; networkId: number; txHash: string; - interval: string; - amount: number; + flowRate: string; currency: string; + anonymous: boolean; + isBatch: boolean; }): Promise => { const recurringDonation = await RecurringDonation.create({ project: params.project, @@ -21,11 +27,54 @@ export const createNewRecurringDonation = async (params: { networkId: params.networkId, txHash: params.txHash, currency: params.currency, - interval: params.interval, - amount: params.amount, + flowRate: params.flowRate, + anonymous: params.anonymous, + isBatch: params.isBatch, }); return recurringDonation.save(); }; +export const updateRecurringDonation = async (params: { + recurringDonation: RecurringDonation; + txHash?: string; + flowRate?: string; + anonymous?: boolean; + isArchived?: boolean; + status?: string; +}): Promise => { + const { recurringDonation, txHash, anonymous, flowRate, status, isArchived } = + params; + if (txHash && flowRate) { + recurringDonation.txHash = txHash; + recurringDonation.flowRate = flowRate; + recurringDonation.finished = false; + recurringDonation.isArchived = false; + recurringDonation.status = RECURRING_DONATION_STATUS.PENDING; + } + + if (anonymous) { + recurringDonation.anonymous = anonymous; + } + + if ( + recurringDonation.status === RECURRING_DONATION_STATUS.ACTIVE && + status === RECURRING_DONATION_STATUS.ENDED + ) { + recurringDonation.status = status; + recurringDonation.finished = true; + } + + if ( + recurringDonation.status === RECURRING_DONATION_STATUS.ENDED && + isArchived + ) { + recurringDonation.isArchived = true; + } else if (isArchived === false) { + // isArchived can be undefined, so we need to check if it's false + recurringDonation.isArchived = false; + } + + return recurringDonation.save(); +}; // TODO Need to write test cases for this function export const findActiveRecurringDonations = async (): Promise< @@ -39,13 +88,95 @@ export const findActiveRecurringDonations = async (): Promise< }); }; +export const updateRecurringDonationFromTheStreamDonations = async ( + recurringDonationId: number, +) => { + try { + await RecurringDonation.query( + ` + UPDATE "recurring_donation" + SET "totalUsdStreamed" = ( + SELECT COALESCE(SUM(d."valueUsd"), 0) + FROM donation as d + WHERE d."recurringDonationId" = $1 + ), + "amountStreamed" = ( + SELECT COALESCE(SUM(d."amount"), 0) + FROM donation as d + WHERE d."recurringDonationId" = $1 + ) + WHERE "id" = $1 + `, + [recurringDonationId], + ); + } catch (e) { + logger.error('updateRecurringDonationFromTheStreamDonations() error', e); + } +}; + export const findRecurringDonationById = async ( - donationId: number, + id: number, ): Promise => { - return RecurringDonation.createQueryBuilder('recurringDonation') - .where(`recurringDonation.id = :donationId`, { - donationId, - }) + return await RecurringDonation.createQueryBuilder('recurringDonation') + .innerJoinAndSelect( + `recurringDonation.anchorContractAddress`, + 'anchorContractAddress', + ) + .leftJoinAndSelect(`recurringDonation.donations`, 'donations') .leftJoinAndSelect('recurringDonation.project', 'project') + .leftJoinAndSelect(`recurringDonation.donor`, 'donor') + .where(`recurringDonation.id = :id`, { id }) .getOne(); }; + +export const countOfActiveRecurringDonationsByProjectId = async ( + projectId: number, +): Promise => { + return await RecurringDonation.createQueryBuilder('recurringDonation') + .where(`recurringDonation.projectId = :projectId`, { projectId }) + .andWhere(`recurringDonation.status = :status`, { + status: RECURRING_DONATION_STATUS.ACTIVE, + }) + .getCount(); +}; + +export const findRecurringDonationByProjectIdAndUserIdAndCurrency = + async (params: { + projectId: number; + userId: number; + currency: string; + }): Promise => { + return RecurringDonation.createQueryBuilder('recurringDonation') + .where(`recurringDonation.projectId = :projectId`, { + projectId: params.projectId, + }) + .andWhere(`recurringDonation.donorId = :userId`, { + userId: params.userId, + }) + .andWhere(`recurringDonation.currency = :currency`, { + currency: params.currency, + }) + .leftJoinAndSelect('recurringDonation.project', 'project') + .leftJoinAndSelect('recurringDonation.donor', 'donor') + .getOne(); + }; + +export const getPendingRecurringDonationsIds = (): Promise< + { id: number }[] +> => { + const date = moment() + .subtract({ + hours: + Number(process.env.RECURRING_DONATION_VERIFICAITON_EXPIRATION_HOURS) || + 72, + }) + .toDate(); + logger.debug('getPendingRecurringDonationsIds -> expirationDate', date); + return RecurringDonation.find({ + where: { + status: RECURRING_DONATION_STATUS.PENDING, + createdAt: MoreThan(date), + }, + select: ['id'], + }); +}; diff --git a/src/repositories/socialProfileRepository.test.ts b/src/repositories/socialProfileRepository.test.ts index 217406bad..f5628c96f 100644 --- a/src/repositories/socialProfileRepository.test.ts +++ b/src/repositories/socialProfileRepository.test.ts @@ -1,7 +1,7 @@ +import { assert } from 'chai'; import { createProjectData, generateRandomEtheriumAddress, - generateTestAccessToken, saveProjectDirectlyToDb, saveProjectVerificationFormDirectlyToDb, saveUserDirectlyToDb, @@ -13,7 +13,6 @@ import { removeSocialProfileById, } from './socialProfileRepository'; import { SOCIAL_NETWORKS } from '../entities/socialProfile'; -import { assert } from 'chai'; describe( 'removeSocialProfileById test cases', diff --git a/src/repositories/socialProfileRepository.ts b/src/repositories/socialProfileRepository.ts index 40a1eeec2..5ed6d2798 100644 --- a/src/repositories/socialProfileRepository.ts +++ b/src/repositories/socialProfileRepository.ts @@ -74,7 +74,7 @@ export const isSocialNetworkAddedToVerificationForm = async (params: { socialNetworkId: string; socialNetwork: string; projectVerificationFormId: number; -}): Promise => { +}): Promise => { const { socialNetworkId, socialNetwork, projectVerificationFormId } = params; const socialProfilesCount = await SocialProfile.createQueryBuilder( 'social_profile', diff --git a/src/repositories/userRepository.test.ts b/src/repositories/userRepository.test.ts index a42083d84..508099a95 100644 --- a/src/repositories/userRepository.test.ts +++ b/src/repositories/userRepository.test.ts @@ -1,3 +1,4 @@ +import { assert } from 'chai'; import { createDonationData, createProjectData, @@ -17,7 +18,6 @@ import { findUsersWhoLikedProjectExcludeProjectOwner, findUsersWhoSupportProject, } from './userRepository'; -import { assert } from 'chai'; import { Reaction } from '../entities/reaction'; import { insertSinglePowerBoosting } from './powerBoostingRepository'; @@ -432,7 +432,7 @@ function findUserByIdTestCases() { }); it('should not find user when userId is undefined', async () => { - // @ts-ignore + // @ts-expect-error it's a test const foundUser = await findUserById(undefined); assert.isNull(foundUser); }); diff --git a/src/resolvers/anchorContractAddressResolver.test.ts b/src/resolvers/anchorContractAddressResolver.test.ts index d94e0ae48..a53a96818 100644 --- a/src/resolvers/anchorContractAddressResolver.test.ts +++ b/src/resolvers/anchorContractAddressResolver.test.ts @@ -1,3 +1,5 @@ +import { assert } from 'chai'; +import axios from 'axios'; import { NETWORK_IDS } from '../provider'; import { createProjectData, @@ -8,11 +10,14 @@ import { saveProjectDirectlyToDb, saveUserDirectlyToDb, } from '../../test/testUtils'; -import { assert } from 'chai'; -import axios from 'axios'; import { createAnchorContractAddressQuery } from '../../test/graphqlQueries'; -import { errorMessages } from '../utils/errorMessages'; +import { + errorMessages, + translationErrorMessagesKeys, +} from '../utils/errorMessages'; import { addNewAnchorAddress } from '../repositories/anchorContractAddressRepository'; +import { AnchorContractAddress } from '../entities/anchorContractAddress'; +import { findUserByWalletAddress } from '../repositories/userRepository'; describe( 'addAnchorContractAddress test cases', @@ -20,27 +25,82 @@ describe( ); function addAnchorContractAddressTestCases() { - it('should create anchorContractAddress successfully', async () => { + //TODO for writing success test cases we shoul be able to set id for the project + // but we can't set id for the project because it is auto generated + + it('should get invalid projectId error', async () => { + // https://sepolia-optimism.etherscan.io/tx/0x7af6b35466b651a43dab0d06e066c421416e5b340c62e5a54124b3eac346297a const projectOwner = await saveUserDirectlyToDb( generateRandomEtheriumAddress(), ); + const project = await saveProjectDirectlyToDb( - createProjectData(), + { + ...createProjectData(), + }, projectOwner, ); - const contractCreator = await saveUserDirectlyToDb( + + const fromWalletAddress = '0x871cd6353b803ceceb090bb827ecb2f361db81ab'; + const contractCreator = + (await findUserByWalletAddress(fromWalletAddress)) || + (await saveUserDirectlyToDb(fromWalletAddress)); + + const accessToken = await generateTestAccessToken(contractCreator.id); + const contractAddress = '0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3'; + const result = await axios.post( + graphqlUrl, + { + query: createAnchorContractAddressQuery, + variables: { + projectId: project.id, + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + address: contractAddress, + txHash: + '0x7af6b35466b651a43dab0d06e066c421416e5b340c62e5a54124b3eac346297a', + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isNull(result.data.data.addAnchorContractAddress); + assert.equal( + result.data.errors[0].message, + translationErrorMessagesKeys.INVALID_PROJECT_ID, + ); + + await AnchorContractAddress.delete({ id: result.data.data.id }); + }); + it('should get invalid projectId error', async () => { + // https://sepolia-optimism.etherscan.io/tx/0x545f669af1370b8f5e6457008e6ce2a78f8b8ab5d486104bff52c094024298c2 + const projectOwner = await saveUserDirectlyToDb( generateRandomEtheriumAddress(), ); + const project = await saveProjectDirectlyToDb( + { + ...createProjectData(), + }, + projectOwner, + ); + + const fromWalletAddress = '0x8f48094a12c8f99d616ae8f3305d5ec73cbaa6b6'; + const contractCreator = + (await findUserByWalletAddress(fromWalletAddress)) || + (await saveUserDirectlyToDb(fromWalletAddress)); + const accessToken = await generateTestAccessToken(contractCreator.id); - const contractAddress = generateRandomEtheriumAddress(); + const contractAddress = '0x1190f5ac0f509d8f3f4b662bf17437d37d64527c'; const result = await axios.post( graphqlUrl, { query: createAnchorContractAddressQuery, variables: { projectId: project.id, - networkId: NETWORK_IDS.OPTIMISTIC, + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, address: contractAddress, txHash: generateRandomEvmTxHash(), }, @@ -51,7 +111,13 @@ function addAnchorContractAddressTestCases() { }, }, ); - assert.isTrue(result.data.data.addAnchorContractAddress.isActive); + assert.isNull(result.data.data.addAnchorContractAddress); + assert.equal( + result.data.errors[0].message, + translationErrorMessagesKeys.TX_NOT_FOUND, + ); + + await AnchorContractAddress.delete({ id: result.data.data.id }); }); it('should return unAuthorized error when not sending JWT', async () => { diff --git a/src/resolvers/anchorContractAddressResolver.ts b/src/resolvers/anchorContractAddressResolver.ts index c133d66d1..b45fd4c2b 100644 --- a/src/resolvers/anchorContractAddressResolver.ts +++ b/src/resolvers/anchorContractAddressResolver.ts @@ -1,24 +1,23 @@ -import { Arg, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'; +import path from 'path'; +import { promises as fs } from 'fs'; +import { ethers } from 'ethers'; +import { Arg, Ctx, Int, Mutation, Resolver } from 'type-graphql'; -import { QfRoundHistory } from '../entities/qfRoundHistory'; -import { getQfRoundHistory } from '../repositories/qfRoundHistoryRepository'; import { AnchorContractAddress } from '../entities/anchorContractAddress'; import { findProjectById } from '../repositories/projectRepository'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { addNewAnchorAddress, findActiveAnchorAddress, } from '../repositories/anchorContractAddressRepository'; import { ApolloContext } from '../types/ApolloContext'; import { findUserById } from '../repositories/userRepository'; +import { getProvider } from '../provider'; +import { logger } from '../utils/logger'; -@Resolver(of => AnchorContractAddress) +@Resolver(_of => AnchorContractAddress) export class AnchorContractAddressResolver { - @Mutation(returns => AnchorContractAddress, { nullable: true }) + @Mutation(_returns => AnchorContractAddress, { nullable: true }) async addAnchorContractAddress( @Ctx() ctx: ApolloContext, @Arg('projectId', () => Int) projectId: number, @@ -61,7 +60,49 @@ export class AnchorContractAddressResolver { ); } - // Validate anchor address, the owner of contract must be the project owner + const web3Provider = getProvider(networkId); + // tx sample // https://sepolia-optimism.etherscan.io/tx/0x7af6b35466b651a43dab0d06e066c421416e5b340c62e5a54124b3eac346297a + const networkData = await web3Provider.getTransaction(txHash); + + if (!networkData) { + logger.debug( + 'Transaction not found in the network. maybe its not mined yet', + { + txHash, + networkId, + }, + ); + throw new Error(i18n.__(translationErrorMessagesKeys.TX_NOT_FOUND)); + } + + // Load the ABI from file + const abiPath = path.join(__dirname, '../abi/anchorContractAbi.json'); + const abi = JSON.parse(await fs.readFile(abiPath, 'utf-8')); + + const iface = new ethers.utils.Interface(abi); + const decodedData = iface.parseTransaction({ data: networkData.data }); + const txProjectId = decodedData.args[1]; + if (Number(txProjectId) !== projectId) { + logger.debug('txProjectId odoes not match the project id', { + txProjectId, + projectId, + }); + throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_PROJECT_ID)); + } + const profileOwnerWalletAddress = decodedData.args[3]; + if ( + profileOwnerWalletAddress.toLowerCase() !== + project?.adminUser?.walletAddress?.toLowerCase() + ) { + logger.debug( + 'profile owner of tx payload does not match the project owner', + { + profileOwnerWalletAddress, + projectOwner: project.adminUser.walletAddress, + }, + ); + throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_PROJECT_ID)); + } return addNewAnchorAddress({ project, diff --git a/src/resolvers/campaignResolver.test.ts b/src/resolvers/campaignResolver.test.ts index bd6f2daf1..98d4ffd93 100644 --- a/src/resolvers/campaignResolver.test.ts +++ b/src/resolvers/campaignResolver.test.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { assert } from 'chai'; import { createProjectData, graphqlUrl, @@ -6,7 +7,6 @@ import { SEED_DATA, } from '../../test/testUtils'; import { fetchCampaignBySlug, getCampaigns } from '../../test/graphqlQueries'; -import { assert } from 'chai'; import { Campaign, CampaignFilterField, diff --git a/src/resolvers/campaignResolver.ts b/src/resolvers/campaignResolver.ts index 094f727d4..1a0f32429 100644 --- a/src/resolvers/campaignResolver.ts +++ b/src/resolvers/campaignResolver.ts @@ -1,12 +1,4 @@ -import { - Arg, - Ctx, - Field, - Int, - Query, - registerEnumType, - Resolver, -} from 'type-graphql'; +import { Arg, Ctx, Int, Query, registerEnumType, Resolver } from 'type-graphql'; import { Campaign, CampaignFilterField, @@ -31,12 +23,12 @@ registerEnumType(CampaignFilterField, { description: 'Same filter fields like projects', }); -@Resolver(of => Campaign) +@Resolver(_of => Campaign) export class CampaignResolver { - @Query(returns => [Campaign], { nullable: true }) + @Query(_returns => [Campaign], { nullable: true }) async campaigns( @Ctx() { req: { user }, projectsFiltersThreadPool }: ApolloContext, - @Arg('connectedWalletUserId', type => Int, { nullable: true }) + @Arg('connectedWalletUserId', _type => Int, { nullable: true }) connectedWalletUserId?: number, ) { const userId = connectedWalletUserId || user?.userId; @@ -48,11 +40,11 @@ export class CampaignResolver { ); } - @Query(returns => Campaign, { nullable: true }) + @Query(_returns => Campaign, { nullable: true }) async findCampaignBySlug( @Ctx() { req: { user }, projectsFiltersThreadPool }: ApolloContext, - @Arg('connectedWalletUserId', type => Int, { nullable: true }) + @Arg('connectedWalletUserId', _type => Int, { nullable: true }) connectedWalletUserId?: number, // If user dont send slug, we return first featured campaign @Arg('slug', { nullable: true }) slug?: string, diff --git a/src/resolvers/categoryResolver.test.ts b/src/resolvers/categoryResolver.test.ts index de309dff5..cc07575b7 100644 --- a/src/resolvers/categoryResolver.test.ts +++ b/src/resolvers/categoryResolver.test.ts @@ -1,10 +1,10 @@ import { assert } from 'chai'; +import axios from 'axios'; import { graphqlUrl, saveCategoryDirectlyToDb, saveMainCategoryDirectlyToDb, } from '../../test/testUtils'; -import axios from 'axios'; import { getCategoryData, getMainCategoriesData, diff --git a/src/resolvers/categoryResolver.ts b/src/resolvers/categoryResolver.ts index 13fe485a7..2fd1d6611 100644 --- a/src/resolvers/categoryResolver.ts +++ b/src/resolvers/categoryResolver.ts @@ -6,7 +6,7 @@ import { Category } from '../entities/category'; import { MainCategory } from '../entities/mainCategory'; import { AppDataSource } from '../orm'; -@Resolver(of => User) +@Resolver(_of => User) export class CategoryResolver { constructor( private readonly categoryRepository: Repository, @@ -18,7 +18,7 @@ export class CategoryResolver { AppDataSource.getDataSource().getRepository(MainCategory); } - @Query(returns => [Category], { nullable: true }) + @Query(_returns => [Category], { nullable: true }) async categories() { return this.categoryRepository .createQueryBuilder('category') @@ -29,7 +29,7 @@ export class CategoryResolver { }) .getMany(); } - @Query(returns => [MainCategory], { nullable: true }) + @Query(_returns => [MainCategory], { nullable: true }) async mainCategories() { return MainCategory.createQueryBuilder('mainCategory') .innerJoinAndSelect( diff --git a/src/resolvers/chainvineResolver.test.ts b/src/resolvers/chainvineResolver.test.ts index b9e43c021..6c000fc29 100644 --- a/src/resolvers/chainvineResolver.test.ts +++ b/src/resolvers/chainvineResolver.test.ts @@ -48,7 +48,6 @@ function registerClickOnChainvineTestCases() { referrerUser.chainvineId = generateHexNumber(10); await referrerUser.save(); - const accessToken = await generateTestAccessToken(user.id); const result = await axios.post(graphqlUrl, { query: registerClickOnChainvineQuery, variables: { diff --git a/src/resolvers/chainvineResolver.ts b/src/resolvers/chainvineResolver.ts index 1b7c1be9e..ac75e8bbf 100644 --- a/src/resolvers/chainvineResolver.ts +++ b/src/resolvers/chainvineResolver.ts @@ -1,19 +1,6 @@ -import { - Arg, - Ctx, - Field, - Int, - Mutation, - Query, - registerEnumType, - Resolver, -} from 'type-graphql'; +import { Arg, Ctx, Mutation, Resolver } from 'type-graphql'; import { ApolloContext } from '../types/ApolloContext'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { User } from '../entities/user'; import { findUserById, @@ -22,9 +9,9 @@ import { import { getChainvineAdapter } from '../adapters/adaptersFactory'; import { firstOrCreateReferredEventByUserId } from '../repositories/referredEventRepository'; -@Resolver(of => User) +@Resolver(_of => User) export class ChainvineResolver { - @Mutation(returns => User, { nullable: true }) + @Mutation(_returns => User, { nullable: true }) async registerOnChainvine( @Ctx() { req: { user } }: ApolloContext, ): Promise { @@ -56,7 +43,7 @@ export class ChainvineResolver { } } - @Mutation(returns => User, { nullable: true }) + @Mutation(_returns => User, { nullable: true }) async registerClickEvent( @Arg('referrerId', { nullable: false }) referrerId: string, @Arg('walletAddress', { nullable: false }) walletAddress: string, diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 3b5d0a40e..d827f4b7b 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -1,4 +1,5 @@ import { assert } from 'chai'; +import axios from 'axios'; import { generateTestAccessToken, graphqlUrl, @@ -15,7 +16,6 @@ import { generateRandomSolanaAddress, generateRandomSolanaTxHash, } from '../../test/testUtils'; -import axios from 'axios'; import { errorMessages } from '../utils/errorMessages'; import { Donation, DONATION_STATUS } from '../entities/donation'; import { @@ -26,13 +26,14 @@ import { donationsToWallets, donationsFromWallets, createDonationMutation, - updateDonationStatusMutation, fetchTotalDonationsUsdAmount, fetchTotalDonors, fetchTotalDonationsPerCategoryPerDate, fetchRecentDonations, fetchTotalDonationsNumberPerDateRange, doesDonatedToProjectInQfRoundQuery, + fetchNewDonorsCount, + fetchNewDonorsDonationTotalUsd, } from '../../test/graphqlQueries'; import { NETWORK_IDS } from '../provider'; import { User } from '../entities/user'; @@ -49,11 +50,9 @@ import { PowerBalanceSnapshot } from '../entities/powerBalanceSnapshot'; import { PowerBoostingSnapshot } from '../entities/powerBoostingSnapshot'; import { AppDataSource } from '../orm'; import { generateRandomString } from '../utils/utils'; -import { ChainvineMockAdapter } from '../adapters/chainvine/chainvineMockAdapter'; import { getChainvineAdapter } from '../adapters/adaptersFactory'; import { firstOrCreateReferredEventByUserId } from '../repositories/referredEventRepository'; import { QfRound } from '../entities/qfRound'; -import { findProjectById } from '../repositories/projectRepository'; import { addOrUpdatePowerSnapshotBalances } from '../repositories/powerBalanceSnapshotRepository'; import { findPowerSnapshots } from '../repositories/powerSnapshotRepository'; import { ChainType } from '../types/network'; @@ -62,8 +61,11 @@ import { DRAFT_DONATION_STATUS, DraftDonation, } from '../entities/draftDonation'; +import { addNewAnchorAddress } from '../repositories/anchorContractAddressRepository'; +import { createNewRecurringDonation } from '../repositories/recurringDonationRepository'; +import { RECURRING_DONATION_STATUS } from '../entities/recurringDonation'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const moment = require('moment'); // TODO Write test cases @@ -77,6 +79,10 @@ describe('donationsToWallets() test cases', donationsToWalletsTestCases); describe('donationsFromWallets() test cases', donationsFromWalletsTestCases); describe('totalDonationsUsdAmount() test cases', donationsUsdAmountTestCases); describe('totalDonorsCountPerDate() test cases', donorsCountPerDateTestCases); +describe( + 'newDonorsCountAndTotalDonationPerDateTestCases() test cases', + newDonorsCountAndTotalDonationPerDateTestCases, +); describe( 'doesDonatedToProjectInQfRound() test cases', doesDonatedToProjectInQfRoundTestCases, @@ -112,17 +118,81 @@ function totalDonationsPerCategoryPerDateTestCases() { donationsResponse.data.data.totalDonationsPerCategory.find( d => d.title === 'food', ); + + const donationToVerified = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(30, 'days').toDate(), + valueUsd: 20, + }), + SEED_DATA.SECOND_USER.id, + SEED_DATA.FIRST_PROJECT.id, + ); + // Donation to non-verified project + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(30, 'days').toDate(), + valueUsd: 10, + }), + SEED_DATA.SECOND_USER.id, + SEED_DATA.NON_VERIFIED_PROJECT.id, + ); + const totalDonationsToVerified = await axios.post(graphqlUrl, { + query: fetchTotalDonationsPerCategoryPerDate, + variables: { + fromDate: moment().add(29, 'days').toDate(), + toDate: moment().add(31, 'days').toDate(), + onlyVerified: true, + }, + }); + const foodTotal = + totalDonationsToVerified.data.data.totalDonationsPerCategory.find( + d => d.title === 'food', + ); + assert.equal( foodDonationsResponseTotal.totalUsd, foodDonationsTotalUsd[0].sum, ); + assert.equal(foodTotal.totalUsd, donationToVerified.valueUsd); }); } function totalDonationsNumberPerDateTestCases() { it('should return donations count per time range', async () => { + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(22, 'days').toDate(), + valueUsd: 20, + }), + SEED_DATA.SECOND_USER.id, + SEED_DATA.FIRST_PROJECT.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(22, 'days').toDate(), + valueUsd: 30, + }), + SEED_DATA.SECOND_USER.id, + SEED_DATA.NON_VERIFIED_PROJECT.id, + ); const donationsResponse = await axios.post(graphqlUrl, { query: fetchTotalDonationsNumberPerDateRange, + variables: { + fromDate: moment().add(21, 'days').toDate().toISOString().split('T')[0], + toDate: moment().add(23, 'days').toDate().toISOString().split('T')[0], + }, + }); + const donationsResponseToVerified = await axios.post(graphqlUrl, { + query: fetchTotalDonationsNumberPerDateRange, + variables: { + fromDate: moment().add(21, 'days').toDate().toISOString().split('T')[0], + toDate: moment().add(23, 'days').toDate().toISOString().split('T')[0], + onlyVerified: true, + }, }); assert.isNumber( donationsResponse.data.data.totalDonationsNumberPerDate.total, @@ -131,14 +201,22 @@ function totalDonationsNumberPerDateTestCases() { donationsResponse.data.data.totalDonationsNumberPerDate .totalPerMonthAndYear.length > 0, ); + assert.equal( + donationsResponse.data.data.totalDonationsNumberPerDate.total, + 2, + ); + assert.equal( + donationsResponseToVerified.data.data.totalDonationsNumberPerDate.total, + 1, + ); }); } function donorsCountPerDateTestCases() { - it('should return not return data if the date is not yyyy-mm-dd', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); + it('should not return data if the date is not yyyy-mm-dd', async () => { + await saveProjectDirectlyToDb(createProjectData()); const walletAddress = generateRandomEtheriumAddress(); - const user = await saveUserDirectlyToDb(walletAddress); + await saveUserDirectlyToDb(walletAddress); const donationsResponse = await axios.post(graphqlUrl, { query: fetchTotalDonors, variables: { @@ -157,8 +235,8 @@ function donorsCountPerDateTestCases() { const project = await saveProjectDirectlyToDb(createProjectData()); const walletAddress = generateRandomEtheriumAddress(); const user = await saveUserDirectlyToDb(walletAddress); - // should count as 1 as its the same user - const donation = await saveDonationDirectlyToDb( + // should count as 1 as it's the same user + await saveDonationDirectlyToDb( createDonationData({ status: DONATION_STATUS.VERIFIED, createdAt: moment().add(50, 'days').toDate(), @@ -167,7 +245,7 @@ function donorsCountPerDateTestCases() { user.id, project.id, ); - const donation2 = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( createDonationData({ status: DONATION_STATUS.VERIFIED, createdAt: moment().add(50, 'days').toDate(), @@ -178,7 +256,7 @@ function donorsCountPerDateTestCases() { ); // anonymous donations count as separate - const anonymousDonation1 = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( createDonationData({ status: DONATION_STATUS.VERIFIED, createdAt: moment().add(50, 'days').toDate(), @@ -188,7 +266,7 @@ function donorsCountPerDateTestCases() { undefined, project.id, ); - const anonymousDonation2 = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( createDonationData({ status: DONATION_STATUS.VERIFIED, createdAt: moment().add(50, 'days').toDate(), @@ -221,6 +299,62 @@ function donorsCountPerDateTestCases() { }); } +function newDonorsCountAndTotalDonationPerDateTestCases() { + it('should return new donors count and their total donation per time range', async () => { + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(40, 'days').toDate(), + valueUsd: 30, + }), + user.id, + 1, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(40, 'days').toDate(), + valueUsd: 25, + }), + user.id, + 1, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(40, 'days').toDate(), + valueUsd: 20, + }), + DONATION_SEED_DATA.FIRST_DONATION.userId, + 1, + ); + + const newDonors = await axios.post(graphqlUrl, { + query: fetchNewDonorsCount, + variables: { + fromDate: moment().add(40, 'days').toDate().toISOString().split('T')[0], + toDate: moment().add(41, 'days').toDate().toISOString().split('T')[0], + }, + }); + const donationUsd = await axios.post(graphqlUrl, { + query: fetchNewDonorsDonationTotalUsd, + variables: { + fromDate: moment().add(40, 'days').toDate().toISOString().split('T')[0], + toDate: moment().add(41, 'days').toDate().toISOString().split('T')[0], + }, + }); + const totalNewDonors = newDonors.data.data.newDonorsCountPerDate.total; + const totalDonationUsd = + donationUsd.data.data.newDonorsDonationTotalUsdPerDate.total; + assert.isOk(newDonors.data.data.newDonorsCountPerDate); + assert.isOk(donationUsd.data.data.newDonorsDonationTotalUsdPerDate); + assert.equal(totalNewDonors, 1); + assert.equal(totalDonationUsd, 30); + }); +} + function doesDonatedToProjectInQfRoundTestCases() { it('should return true when there is verified donation', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); @@ -426,17 +560,23 @@ function doesDonatedToProjectInQfRoundTestCases() { function donationsUsdAmountTestCases() { it('should return total usd amount for donations made in a time range', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); - const walletAddress = generateRandomEtheriumAddress(); - const user = await saveUserDirectlyToDb(walletAddress); - const donation = await saveDonationDirectlyToDb( + const donationToNonVerified = await saveDonationDirectlyToDb( createDonationData({ status: DONATION_STATUS.VERIFIED, createdAt: moment().add(100, 'days').toDate(), valueUsd: 20, }), - user.id, - project.id, + SEED_DATA.SECOND_USER.id, + SEED_DATA.NON_VERIFIED_PROJECT.id, + ); + const donationToVerified = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(99, 'days').toDate(), + valueUsd: 10, + }), + SEED_DATA.SECOND_USER.id, + SEED_DATA.FIRST_PROJECT.id, ); const donationsResponse = await axios.post(graphqlUrl, { @@ -447,10 +587,20 @@ function donationsUsdAmountTestCases() { }, }); - assert.isOk(donationsResponse); + const donationsResponseToVerified = await axios.post(graphqlUrl, { + query: fetchTotalDonationsUsdAmount, + variables: { + fromDate: moment().add(99, 'days').toDate().toISOString().split('T')[0], + toDate: moment().add(101, 'days').toDate().toISOString().split('T')[0], + onlyVerified: true, + }, + }); + + assert.isOk(donationsResponse.data.data); + assert.isOk(donationsResponseToVerified.data.data); assert.equal( donationsResponse.data.data.donationsTotalUsdPerDate.total, - donation.valueUsd, + donationToNonVerified.valueUsd + donationToVerified.valueUsd, ); const total = donationsResponse.data.data.donationsTotalUsdPerDate.totalPerMonthAndYear.reduce( @@ -461,6 +611,10 @@ function donationsUsdAmountTestCases() { donationsResponse.data.data.donationsTotalUsdPerDate.total, total, ); + assert.equal( + donationsResponseToVerified.data.data.donationsTotalUsdPerDate.total, + donationToVerified.valueUsd, + ); }); } @@ -782,7 +936,7 @@ function createDonationTestCases() { firstName: 'first name', }).save(); - const user2 = await User.create({ + await User.create({ walletAddress: referrerWalletAddress, loginType: 'wallet', firstName: 'first name', @@ -940,7 +1094,7 @@ function createDonationTestCases() { firstName: 'first name', }).save(); - const user2 = await User.create({ + await User.create({ walletAddress: referrerWalletAddress, loginType: 'wallet', firstName: 'first name', @@ -1037,7 +1191,7 @@ function createDonationTestCases() { firstName: 'first name', }).save(); - const user2 = await User.create({ + await User.create({ walletAddress: referrerWalletAddress, loginType: 'wallet', firstName: 'first name', @@ -1104,7 +1258,7 @@ function createDonationTestCases() { firstName: 'first name', }).save(); - const user2 = await User.create({ + await User.create({ walletAddress: referrerWalletAddress, loginType: 'wallet', firstName: 'first name', @@ -2040,7 +2194,7 @@ function createDonationTestCases() { assert.isFalse(donation?.segmentNotified); }); it('should throw exception when send invalid projectId', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); + await saveProjectDirectlyToDb(createProjectData()); const user = await User.create({ walletAddress: generateRandomEtheriumAddress(), loginType: 'wallet', @@ -2379,7 +2533,7 @@ function createDonationTestCases() { ); assert.equal( saveDonationResponse.data.errors[0].message, - '"transactionNetworkId" must be one of [1, 3, 5, 100, 137, 10, 420, 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, 101, 102, 103]', ); }); it('should not throw exception when currency is not valid when currency is USDC.e', async () => { @@ -2712,13 +2866,13 @@ function donationsByProjectIdTestCases() { firstName: 'first name', }).save(); - const user2 = await User.create({ + await User.create({ walletAddress: referrerWalletAddress, loginType: 'wallet', firstName: 'first name', }).save(); - const donation1 = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( createDonationData({ status: DONATION_STATUS.VERIFIED, qfRoundId: qfRound.id, @@ -2727,7 +2881,7 @@ function donationsByProjectIdTestCases() { project.id, ); - const donation2 = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( createDonationData({ status: DONATION_STATUS.VERIFIED, qfRoundId: qfRound.id, @@ -2751,7 +2905,7 @@ function donationsByProjectIdTestCases() { project.qfRounds = [qfRound2]; await project.save(); - const donation3 = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( createDonationData({ status: DONATION_STATUS.VERIFIED, qfRoundId: qfRound2.id, @@ -2760,7 +2914,7 @@ function donationsByProjectIdTestCases() { project.id, ); - const donation4 = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( createDonationData({ status: DONATION_STATUS.VERIFIED, qfRoundId: qfRound2.id, @@ -3239,6 +3393,73 @@ function donationsByProjectIdTestCases() { donations.find(donation => Number(donation.id) === pendingDonation.id), ); }); + it('should return recurringDonationsCount and totalCount correctly', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + { ...createDonationData(), status: DONATION_STATUS.VERIFIED }, + user.id, + project.id, + ); + + 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, + }); + recurringDonation.status = RECURRING_DONATION_STATUS.ACTIVE; + await recurringDonation.save(); + + const recurringDonation2 = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: user, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + }); + recurringDonation2.status = RECURRING_DONATION_STATUS.ACTIVE; + await recurringDonation2.save(); + + const result = await axios.post( + graphqlUrl, + { + query: fetchDonationsByProjectIdQuery, + variables: { + projectId: project.id, + }, + }, + {}, + ); + + assert.equal(result.data.data.donationsByProjectId.totalCount, 1); + assert.equal( + result.data.data.donationsByProjectId.recurringDonationsCount, + 2, + ); + }); } function donationsByUserIdTestCases() { @@ -4022,409 +4243,410 @@ function donationsToWalletsTestCases() { }); } -function updateDonationStatusTestCases() { - it('should update donation status to verified after calling without sending status', async () => { - // https://blockscout.com/xdai/mainnet/tx/0xaaf96af4d0634dafcac1b6eca627b77ceb157aad1037033761ed3a4220ebb2b5 - const transactionInfo = { - txHash: - '0xaaf96af4d0634dafcac1b6eca627b77ceb157aad1037033761ed3a4220ebb2b5', - networkId: NETWORK_IDS.XDAI, - amount: 1, - fromAddress: '0x00d18ca9782be1caef611017c2fbc1a39779a57c', - toAddress: '0x90b31c07fb0310b4b0d88368169dad8fe0cbb6da', - currency: 'XDAI', - timestamp: 1647483910, - }; - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 1, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - assert.equal(donation.status, DONATION_STATUS.PENDING); - const accessToken = await generateTestAccessToken(user.id); - const result = await axios.post( - graphqlUrl, - { - query: updateDonationStatusMutation, - variables: { - donationId: donation.id, - }, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - assert.equal( - result.data.data.updateDonationStatus.status, - DONATION_STATUS.VERIFIED, - ); - }); - it('should update donation status to failed after calling without sending status ', async () => { - // https://blockscout.com/xdai/mainnet/tx/0x6c2550e21d57d2c9c7e1cb22c0c4d6581575c77f9be2ef35995466e61c730a08 - const transactionInfo = { - txHash: - '0x6c2550e21d57d2c9c7e1cb22c0c4d6581575c77f9be2ef35995466e61c730a08', - networkId: NETWORK_IDS.XDAI, - amount: 1, - fromAddress: generateRandomEtheriumAddress(), - toAddress: '0x42a7d872dec08d309f4b93d05e5b9de183765858', - currency: 'GIV', - timestamp: 1647069070, - }; - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 1, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - assert.equal(donation.status, DONATION_STATUS.PENDING); - const accessToken = await generateTestAccessToken(user.id); - const result = await axios.post( - graphqlUrl, - { - query: updateDonationStatusMutation, - variables: { - donationId: donation.id, - }, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - assert.equal( - result.data.data.updateDonationStatus.status, - DONATION_STATUS.FAILED, - ); - assert.equal( - result.data.data.updateDonationStatus.verifyErrorMessage, - errorMessages.TRANSACTION_FROM_ADDRESS_IS_DIFFERENT_FROM_SENT_FROM_ADDRESS, - ); - }); - // ROPSTEN CHAIN DECOMMISSIONED use goerli - // TODO: Rewrite this test with goerli. - // it('should update donation status to failed when tx is failed on network ', async () => { - // // https://ropsten.etherscan.io/tx/0x66a7902f3dad318e8d075454e26ee829e9832db0b20922cfd9d916fb792ff724 - // const transactionInfo = { - // txHash: - // '0x66a7902f3dad318e8d075454e26ee829e9832db0b20922cfd9d916fb792ff724', - // currency: 'DAI', - // networkId: NETWORK_IDS.ROPSTEN, - // fromAddress: '0x839395e20bbB182fa440d08F850E6c7A8f6F0780', - // toAddress: '0x5ac583feb2b1f288c0a51d6cdca2e8c814bfe93b', - // amount: 0.04, - // timestamp: 1607360947, - // }; - // const project = await saveProjectDirectlyToDb({ - // ...createProjectData(), - // walletAddress: transactionInfo.toAddress, - // }); - // const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - // const donation = await saveDonationDirectlyToDb( - // { - // amount: transactionInfo.amount, - // transactionNetworkId: transactionInfo.networkId, - // transactionId: transactionInfo.txHash, - // currency: transactionInfo.currency, - // fromWalletAddress: transactionInfo.fromAddress, - // toWalletAddress: transactionInfo.toAddress, - // valueUsd: 1, - // anonymous: false, - // createdAt: new Date(transactionInfo.timestamp), - // status: DONATION_STATUS.PENDING, - // }, - // user.id, - // project.id, - // ); - // assert.equal(donation.status, DONATION_STATUS.PENDING); - // const accessToken = await generateTestAccessToken(user.id); - // const result = await axios.post( - // graphqlUrl, - // { - // query: updateDonationStatusMutation, - // variables: { - // donationId: donation.id, - // status: DONATION_STATUS.FAILED, - // }, - // }, - // { - // headers: { - // Authorization: `Bearer ${accessToken}`, - // }, - // }, - // ); - // assert.equal( - // result.data.data.updateDonationStatus.status, - // DONATION_STATUS.FAILED, - // ); - // assert.equal( - // result.data.data.updateDonationStatus.verifyErrorMessage, - // errorMessages.TRANSACTION_STATUS_IS_FAILED_IN_NETWORK, - // ); - // }); - it('should donation status remain pending after calling without sending status (we assume its not mined so far)', async () => { - const transactionInfo = { - txHash: generateRandomEvmTxHash(), - networkId: NETWORK_IDS.XDAI, - amount: 1, - fromAddress: generateRandomEtheriumAddress(), - toAddress: generateRandomEtheriumAddress(), - currency: 'GIV', - timestamp: 1647069070, - }; - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 1, - nonce: 99999999, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - assert.equal(donation.status, DONATION_STATUS.PENDING); - const accessToken = await generateTestAccessToken(user.id); - const result = await axios.post( - graphqlUrl, - { - query: updateDonationStatusMutation, - variables: { - donationId: donation.id, - }, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - assert.equal( - result.data.data.updateDonationStatus.status, - DONATION_STATUS.PENDING, - ); - }); - - it('should update donation status to verified ', async () => { - // https://etherscan.io/tx/0xe42fd848528dcb06f56fd3b553807354b4bf0ff591454e1cc54070684d519df5 - const transactionInfo = { - txHash: - '0xe42fd848528dcb06f56fd3b553807354b4bf0ff591454e1cc54070684d519df5', - networkId: NETWORK_IDS.MAIN_NET, - amount: 500, - fromAddress: '0x5d28fe1e9f895464aab52287d85ebff32b351674', - toAddress: '0x0eed1566f46b0421d53d2143a3957bb22016ef4b', - currency: 'GIV', - timestamp: 1646704855, - }; - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 1, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - assert.equal(donation.status, DONATION_STATUS.PENDING); - const accessToken = await generateTestAccessToken(user.id); - const result = await axios.post( - graphqlUrl, - { - query: updateDonationStatusMutation, - variables: { - donationId: donation.id, - - // We send faild but because it checks with network first, it ignores sent status - status: DONATION_STATUS.FAILED, - }, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - assert.equal( - result.data.data.updateDonationStatus.status, - DONATION_STATUS.VERIFIED, - ); - }); - it('should update donation status to failed', async () => { - // https://blockscout.com/xdai/mainnet/tx/0x013c3371c1de181439ac51067fd2e417b71b9d462c13417252e2153f80af630f - const transactionInfo = { - txHash: - '0x013c3371c1de181439ac51067fd2e417b71b9d462c13417252e2153f80af630f', - networkId: NETWORK_IDS.XDAI, - amount: 2800, - fromAddress: '0x5d28fe1e9f895464aab52287d85ebff32b351674', - toAddress: generateRandomEtheriumAddress(), - currency: 'GIV', - timestamp: 1646725075, - }; - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - valueUsd: 1, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - assert.equal(donation.status, DONATION_STATUS.PENDING); - const accessToken = await generateTestAccessToken(user.id); - const result = await axios.post( - graphqlUrl, - { - query: updateDonationStatusMutation, - variables: { - donationId: donation.id, - status: DONATION_STATUS.FAILED, - }, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - assert.equal( - result.data.data.updateDonationStatus.status, - DONATION_STATUS.FAILED, - ); - assert.equal( - result.data.data.updateDonationStatus.verifyErrorMessage, - errorMessages.TRANSACTION_TO_ADDRESS_IS_DIFFERENT_FROM_SENT_TO_ADDRESS, - ); - }); - it('should update donation status to failed, tx is not mined and donor says it failed', async () => { - const transactionInfo = { - txHash: generateRandomEvmTxHash(), - networkId: NETWORK_IDS.XDAI, - amount: 1, - fromAddress: generateRandomEtheriumAddress(), - toAddress: generateRandomEtheriumAddress(), - currency: 'GIV', - timestamp: 1647069070, - }; - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - nonce: 999999, - valueUsd: 1, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - assert.equal(donation.status, DONATION_STATUS.PENDING); - const accessToken = await generateTestAccessToken(user.id); - const result = await axios.post( - graphqlUrl, - { - query: updateDonationStatusMutation, - variables: { - donationId: donation.id, - status: DONATION_STATUS.FAILED, - }, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - assert.equal( - result.data.data.updateDonationStatus.status, - DONATION_STATUS.FAILED, - ); - assert.equal( - result.data.data.updateDonationStatus.verifyErrorMessage, - errorMessages.DONOR_REPORTED_IT_AS_FAILED, - ); - }); -} +// +// function updateDonationStatusTestCases() { +// it('should update donation status to verified after calling without sending status', async () => { +// // https://blockscout.com/xdai/mainnet/tx/0xaaf96af4d0634dafcac1b6eca627b77ceb157aad1037033761ed3a4220ebb2b5 +// const transactionInfo = { +// txHash: +// '0xaaf96af4d0634dafcac1b6eca627b77ceb157aad1037033761ed3a4220ebb2b5', +// networkId: NETWORK_IDS.XDAI, +// amount: 1, +// fromAddress: '0x00d18ca9782be1caef611017c2fbc1a39779a57c', +// toAddress: '0x90b31c07fb0310b4b0d88368169dad8fe0cbb6da', +// currency: 'XDAI', +// timestamp: 1647483910, +// }; +// const project = await saveProjectDirectlyToDb({ +// ...createProjectData(), +// walletAddress: transactionInfo.toAddress, +// }); +// const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); +// const donation = await saveDonationDirectlyToDb( +// { +// amount: transactionInfo.amount, +// transactionNetworkId: transactionInfo.networkId, +// transactionId: transactionInfo.txHash, +// currency: transactionInfo.currency, +// fromWalletAddress: transactionInfo.fromAddress, +// toWalletAddress: transactionInfo.toAddress, +// valueUsd: 1, +// anonymous: false, +// createdAt: new Date(transactionInfo.timestamp), +// status: DONATION_STATUS.PENDING, +// }, +// user.id, +// project.id, +// ); +// assert.equal(donation.status, DONATION_STATUS.PENDING); +// const accessToken = await generateTestAccessToken(user.id); +// const result = await axios.post( +// graphqlUrl, +// { +// query: updateDonationStatusMutation, +// variables: { +// donationId: donation.id, +// }, +// }, +// { +// headers: { +// Authorization: `Bearer ${accessToken}`, +// }, +// }, +// ); +// assert.equal( +// result.data.data.updateDonationStatus.status, +// DONATION_STATUS.VERIFIED, +// ); +// }); +// it('should update donation status to failed after calling without sending status ', async () => { +// // https://blockscout.com/xdai/mainnet/tx/0x6c2550e21d57d2c9c7e1cb22c0c4d6581575c77f9be2ef35995466e61c730a08 +// const transactionInfo = { +// txHash: +// '0x6c2550e21d57d2c9c7e1cb22c0c4d6581575c77f9be2ef35995466e61c730a08', +// networkId: NETWORK_IDS.XDAI, +// amount: 1, +// fromAddress: generateRandomEtheriumAddress(), +// toAddress: '0x42a7d872dec08d309f4b93d05e5b9de183765858', +// currency: 'GIV', +// timestamp: 1647069070, +// }; +// const project = await saveProjectDirectlyToDb({ +// ...createProjectData(), +// walletAddress: transactionInfo.toAddress, +// }); +// const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); +// const donation = await saveDonationDirectlyToDb( +// { +// amount: transactionInfo.amount, +// transactionNetworkId: transactionInfo.networkId, +// transactionId: transactionInfo.txHash, +// currency: transactionInfo.currency, +// fromWalletAddress: transactionInfo.fromAddress, +// toWalletAddress: transactionInfo.toAddress, +// valueUsd: 1, +// anonymous: false, +// createdAt: new Date(transactionInfo.timestamp), +// status: DONATION_STATUS.PENDING, +// }, +// user.id, +// project.id, +// ); +// assert.equal(donation.status, DONATION_STATUS.PENDING); +// const accessToken = await generateTestAccessToken(user.id); +// const result = await axios.post( +// graphqlUrl, +// { +// query: updateDonationStatusMutation, +// variables: { +// donationId: donation.id, +// }, +// }, +// { +// headers: { +// Authorization: `Bearer ${accessToken}`, +// }, +// }, +// ); +// assert.equal( +// result.data.data.updateDonationStatus.status, +// DONATION_STATUS.FAILED, +// ); +// assert.equal( +// result.data.data.updateDonationStatus.verifyErrorMessage, +// errorMessages.TRANSACTION_FROM_ADDRESS_IS_DIFFERENT_FROM_SENT_FROM_ADDRESS, +// ); +// }); +// // ROPSTEN CHAIN DECOMMISSIONED use goerli +// // TODO: Rewrite this test with goerli. +// // it('should update donation status to failed when tx is failed on network ', async () => { +// // // https://ropsten.etherscan.io/tx/0x66a7902f3dad318e8d075454e26ee829e9832db0b20922cfd9d916fb792ff724 +// // const transactionInfo = { +// // txHash: +// // '0x66a7902f3dad318e8d075454e26ee829e9832db0b20922cfd9d916fb792ff724', +// // currency: 'DAI', +// // networkId: NETWORK_IDS.ROPSTEN, +// // fromAddress: '0x839395e20bbB182fa440d08F850E6c7A8f6F0780', +// // toAddress: '0x5ac583feb2b1f288c0a51d6cdca2e8c814bfe93b', +// // amount: 0.04, +// // timestamp: 1607360947, +// // }; +// // const project = await saveProjectDirectlyToDb({ +// // ...createProjectData(), +// // walletAddress: transactionInfo.toAddress, +// // }); +// // const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); +// // const donation = await saveDonationDirectlyToDb( +// // { +// // amount: transactionInfo.amount, +// // transactionNetworkId: transactionInfo.networkId, +// // transactionId: transactionInfo.txHash, +// // currency: transactionInfo.currency, +// // fromWalletAddress: transactionInfo.fromAddress, +// // toWalletAddress: transactionInfo.toAddress, +// // valueUsd: 1, +// // anonymous: false, +// // createdAt: new Date(transactionInfo.timestamp), +// // status: DONATION_STATUS.PENDING, +// // }, +// // user.id, +// // project.id, +// // ); +// // assert.equal(donation.status, DONATION_STATUS.PENDING); +// // const accessToken = await generateTestAccessToken(user.id); +// // const result = await axios.post( +// // graphqlUrl, +// // { +// // query: updateDonationStatusMutation, +// // variables: { +// // donationId: donation.id, +// // status: DONATION_STATUS.FAILED, +// // }, +// // }, +// // { +// // headers: { +// // Authorization: `Bearer ${accessToken}`, +// // }, +// // }, +// // ); +// // assert.equal( +// // result.data.data.updateDonationStatus.status, +// // DONATION_STATUS.FAILED, +// // ); +// // assert.equal( +// // result.data.data.updateDonationStatus.verifyErrorMessage, +// // errorMessages.TRANSACTION_STATUS_IS_FAILED_IN_NETWORK, +// // ); +// // }); +// it('should donation status remain pending after calling without sending status (we assume its not mined so far)', async () => { +// const transactionInfo = { +// txHash: generateRandomEvmTxHash(), +// networkId: NETWORK_IDS.XDAI, +// amount: 1, +// fromAddress: generateRandomEtheriumAddress(), +// toAddress: generateRandomEtheriumAddress(), +// currency: 'GIV', +// timestamp: 1647069070, +// }; +// const project = await saveProjectDirectlyToDb({ +// ...createProjectData(), +// walletAddress: transactionInfo.toAddress, +// }); +// const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); +// const donation = await saveDonationDirectlyToDb( +// { +// amount: transactionInfo.amount, +// transactionNetworkId: transactionInfo.networkId, +// transactionId: transactionInfo.txHash, +// currency: transactionInfo.currency, +// fromWalletAddress: transactionInfo.fromAddress, +// toWalletAddress: transactionInfo.toAddress, +// valueUsd: 1, +// nonce: 99999999, +// anonymous: false, +// createdAt: new Date(transactionInfo.timestamp), +// status: DONATION_STATUS.PENDING, +// }, +// user.id, +// project.id, +// ); +// assert.equal(donation.status, DONATION_STATUS.PENDING); +// const accessToken = await generateTestAccessToken(user.id); +// const result = await axios.post( +// graphqlUrl, +// { +// query: updateDonationStatusMutation, +// variables: { +// donationId: donation.id, +// }, +// }, +// { +// headers: { +// Authorization: `Bearer ${accessToken}`, +// }, +// }, +// ); +// assert.equal( +// result.data.data.updateDonationStatus.status, +// DONATION_STATUS.PENDING, +// ); +// }); +// +// it('should update donation status to verified ', async () => { +// // https://etherscan.io/tx/0xe42fd848528dcb06f56fd3b553807354b4bf0ff591454e1cc54070684d519df5 +// const transactionInfo = { +// txHash: +// '0xe42fd848528dcb06f56fd3b553807354b4bf0ff591454e1cc54070684d519df5', +// networkId: NETWORK_IDS.MAIN_NET, +// amount: 500, +// fromAddress: '0x5d28fe1e9f895464aab52287d85ebff32b351674', +// toAddress: '0x0eed1566f46b0421d53d2143a3957bb22016ef4b', +// currency: 'GIV', +// timestamp: 1646704855, +// }; +// const project = await saveProjectDirectlyToDb({ +// ...createProjectData(), +// walletAddress: transactionInfo.toAddress, +// }); +// const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); +// const donation = await saveDonationDirectlyToDb( +// { +// amount: transactionInfo.amount, +// transactionNetworkId: transactionInfo.networkId, +// transactionId: transactionInfo.txHash, +// currency: transactionInfo.currency, +// fromWalletAddress: transactionInfo.fromAddress, +// toWalletAddress: transactionInfo.toAddress, +// valueUsd: 1, +// anonymous: false, +// createdAt: new Date(transactionInfo.timestamp), +// status: DONATION_STATUS.PENDING, +// }, +// user.id, +// project.id, +// ); +// assert.equal(donation.status, DONATION_STATUS.PENDING); +// const accessToken = await generateTestAccessToken(user.id); +// const result = await axios.post( +// graphqlUrl, +// { +// query: updateDonationStatusMutation, +// variables: { +// donationId: donation.id, +// +// // We send faild but because it checks with network first, it ignores sent status +// status: DONATION_STATUS.FAILED, +// }, +// }, +// { +// headers: { +// Authorization: `Bearer ${accessToken}`, +// }, +// }, +// ); +// assert.equal( +// result.data.data.updateDonationStatus.status, +// DONATION_STATUS.VERIFIED, +// ); +// }); +// it('should update donation status to failed', async () => { +// // https://blockscout.com/xdai/mainnet/tx/0x013c3371c1de181439ac51067fd2e417b71b9d462c13417252e2153f80af630f +// const transactionInfo = { +// txHash: +// '0x013c3371c1de181439ac51067fd2e417b71b9d462c13417252e2153f80af630f', +// networkId: NETWORK_IDS.XDAI, +// amount: 2800, +// fromAddress: '0x5d28fe1e9f895464aab52287d85ebff32b351674', +// toAddress: generateRandomEtheriumAddress(), +// currency: 'GIV', +// timestamp: 1646725075, +// }; +// const project = await saveProjectDirectlyToDb({ +// ...createProjectData(), +// walletAddress: transactionInfo.toAddress, +// }); +// const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); +// const donation = await saveDonationDirectlyToDb( +// { +// amount: transactionInfo.amount, +// transactionNetworkId: transactionInfo.networkId, +// transactionId: transactionInfo.txHash, +// currency: transactionInfo.currency, +// fromWalletAddress: transactionInfo.fromAddress, +// toWalletAddress: transactionInfo.toAddress, +// valueUsd: 1, +// anonymous: false, +// createdAt: new Date(transactionInfo.timestamp), +// status: DONATION_STATUS.PENDING, +// }, +// user.id, +// project.id, +// ); +// assert.equal(donation.status, DONATION_STATUS.PENDING); +// const accessToken = await generateTestAccessToken(user.id); +// const result = await axios.post( +// graphqlUrl, +// { +// query: updateDonationStatusMutation, +// variables: { +// donationId: donation.id, +// status: DONATION_STATUS.FAILED, +// }, +// }, +// { +// headers: { +// Authorization: `Bearer ${accessToken}`, +// }, +// }, +// ); +// assert.equal( +// result.data.data.updateDonationStatus.status, +// DONATION_STATUS.FAILED, +// ); +// assert.equal( +// result.data.data.updateDonationStatus.verifyErrorMessage, +// errorMessages.TRANSACTION_TO_ADDRESS_IS_DIFFERENT_FROM_SENT_TO_ADDRESS, +// ); +// }); +// it('should update donation status to failed, tx is not mined and donor says it failed', async () => { +// const transactionInfo = { +// txHash: generateRandomEvmTxHash(), +// networkId: NETWORK_IDS.XDAI, +// amount: 1, +// fromAddress: generateRandomEtheriumAddress(), +// toAddress: generateRandomEtheriumAddress(), +// currency: 'GIV', +// timestamp: 1647069070, +// }; +// const project = await saveProjectDirectlyToDb({ +// ...createProjectData(), +// walletAddress: transactionInfo.toAddress, +// }); +// const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); +// const donation = await saveDonationDirectlyToDb( +// { +// amount: transactionInfo.amount, +// transactionNetworkId: transactionInfo.networkId, +// transactionId: transactionInfo.txHash, +// currency: transactionInfo.currency, +// fromWalletAddress: transactionInfo.fromAddress, +// toWalletAddress: transactionInfo.toAddress, +// nonce: 999999, +// valueUsd: 1, +// anonymous: false, +// createdAt: new Date(transactionInfo.timestamp), +// status: DONATION_STATUS.PENDING, +// }, +// user.id, +// project.id, +// ); +// assert.equal(donation.status, DONATION_STATUS.PENDING); +// const accessToken = await generateTestAccessToken(user.id); +// const result = await axios.post( +// graphqlUrl, +// { +// query: updateDonationStatusMutation, +// variables: { +// donationId: donation.id, +// status: DONATION_STATUS.FAILED, +// }, +// }, +// { +// headers: { +// Authorization: `Bearer ${accessToken}`, +// }, +// }, +// ); +// assert.equal( +// result.data.data.updateDonationStatus.status, +// DONATION_STATUS.FAILED, +// ); +// assert.equal( +// result.data.data.updateDonationStatus.verifyErrorMessage, +// errorMessages.DONOR_REPORTED_IT_AS_FAILED, +// ); +// }); +// } async function recentDonationsTestCases() { // Clear all other donations @@ -4440,7 +4662,7 @@ async function recentDonationsTestCases() { const user = await saveUserDirectlyToDb(walletAddress); const user2 = await saveUserDirectlyToDb(walletAddress2); const user3 = await saveUserDirectlyToDb(walletAddress3); - const donation1 = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( createDonationData({ status: DONATION_STATUS.VERIFIED, createdAt: new Date(1000), diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 2451306d2..fe23bf45e 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -14,11 +14,11 @@ import { } from 'type-graphql'; import { Service } from 'typedi'; import { Max, Min } from 'class-validator'; +import { Brackets, In, Repository } from 'typeorm'; import { Donation, DONATION_STATUS, SortField } from '../entities/donation'; import { ApolloContext } from '../types/ApolloContext'; import { Project, ProjStatus } from '../entities/project'; import { Token } from '../entities/token'; -import { Brackets, In, Repository } from 'typeorm'; import { publicSelectionFields, User } from '../entities/user'; import SentryLogger from '../sentryLogger'; import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; @@ -50,6 +50,8 @@ import { findDonationById, getRecentDonations, isVerifiedDonationExistsInQfRound, + newDonorsCount, + newDonorsDonationTotalUsd, } from '../repositories/donationRepository'; import { sleep } from '../utils/utils'; import { findProjectRecipientAddressByNetworkId } from '../repositories/projectAddressRepository'; @@ -60,49 +62,50 @@ import { getChainvineReferralInfoForDonation } from '../services/chainvineReferr import { relatedActiveQfRoundForProject } from '../services/qfRoundService'; import { detectAddressChainType } from '../utils/networks'; import { ChainType } from '../types/network'; -import { - getAppropriateNetworkId, - getDefaultSolanaChainId, -} from '../services/chains'; +import { getAppropriateNetworkId } from '../services/chains'; import { markDraftDonationStatusMatched } from '../repositories/draftDonationRepository'; import { DRAFT_DONATION_STATUS, DraftDonation, } from '../entities/draftDonation'; +import { countOfActiveRecurringDonationsByProjectId } from '../repositories/recurringDonationRepository'; const draftDonationEnabled = process.env.ENABLE_DRAFT_DONATION === 'true'; @ObjectType() class PaginateDonations { - @Field(type => [Donation], { nullable: true }) + @Field(_type => [Donation], { nullable: true }) donations: Donation[]; - @Field(type => Number, { nullable: true }) + @Field(_type => Number, { nullable: true }) totalCount: number; - @Field(type => Number, { nullable: true }) + @Field(_type => Number, { nullable: true }) + recurringDonationsCount: number; + + @Field(_type => Number, { nullable: true }) totalUsdBalance: number; - @Field(type => Number, { nullable: true }) + @Field(_type => Number, { nullable: true }) totalEthBalance: number; } // As general as posible types to reuse it @ObjectType() export class ResourcesTotalPerMonthAndYear { - @Field(type => Number, { nullable: true }) - total?: Number; + @Field(_type => Number, { nullable: true }) + total?: number; - @Field(type => String, { nullable: true }) - date?: String; + @Field(_type => String, { nullable: true }) + date?: string; } @ObjectType() export class ResourcePerDateRange { - @Field(type => Number, { nullable: true }) - total?: Number; + @Field(_type => Number, { nullable: true }) + total?: number; - @Field(type => [ResourcesTotalPerMonthAndYear], { nullable: true }) + @Field(_type => [ResourcesTotalPerMonthAndYear], { nullable: true }) totalPerMonthAndYear?: ResourcesTotalPerMonthAndYear[]; } @@ -128,26 +131,26 @@ registerEnumType(SortDirection, { @InputType() class SortBy { - @Field(type => SortField) + @Field(_type => SortField) field: SortField; - @Field(type => SortDirection) + @Field(_type => SortDirection) direction: SortDirection; } @Service() @ArgsType() class UserDonationsArgs { - @Field(type => Int, { defaultValue: 0 }) + @Field(_type => Int, { defaultValue: 0 }) @Min(0) skip: number; - @Field(type => Int, { defaultValue: 10 }) + @Field(_type => Int, { defaultValue: 10 }) @Min(0) @Max(50) take: number; - @Field(type => SortBy, { + @Field(_type => SortBy, { defaultValue: { field: SortField.CreationDate, direction: SortDirection.DESC, @@ -155,49 +158,49 @@ class UserDonationsArgs { }) orderBy: SortBy; - @Field(type => Int, { nullable: false }) + @Field(_type => Int, { nullable: false }) userId: number; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) status: string; } @ObjectType() class UserDonations { - @Field(type => [Donation]) + @Field(_type => [Donation]) donations: Donation[]; - @Field(type => Int) + @Field(_type => Int) totalCount: number; } @ObjectType() class MainCategoryDonations { - @Field(type => Int) + @Field(_type => Int) id: number; - @Field(type => String) + @Field(_type => String) title: string; - @Field(type => String) + @Field(_type => String) slug: string; - @Field(type => Number) + @Field(_type => Number) totalUsd: number; } @ObjectType() class DonationCurrencyStats { - @Field(type => String, { nullable: true }) - currency?: String; + @Field(_type => String, { nullable: true }) + currency?: string; - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) uniqueDonorCount?: number; - @Field(type => Number, { nullable: true }) - currencyPercentage?: Number; + @Field(_type => Number, { nullable: true }) + currencyPercentage?: number; } -@Resolver(of => User) +@Resolver(_of => User) export class DonationResolver { private readonly donationRepository: Repository; constructor() { @@ -205,7 +208,7 @@ export class DonationResolver { AppDataSource.getDataSource().getRepository(Donation); } - @Query(returns => [DonationCurrencyStats]) + @Query(_returns => [DonationCurrencyStats]) async getDonationStats(): Promise { const query = ` SELECT @@ -221,7 +224,7 @@ export class DonationResolver { return result; } - @Query(returns => [Donation], { nullable: true }) + @Query(_returns => [Donation], { nullable: true }) async donations( // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss @Arg('fromDate', { nullable: true }) fromDate?: string, @@ -255,11 +258,12 @@ export class DonationResolver { } } - @Query(returns => [MainCategoryDonations], { nullable: true }) + @Query(_returns => [MainCategoryDonations], { nullable: true }) async totalDonationsPerCategory( @Arg('fromDate', { nullable: true }) fromDate?: string, @Arg('toDate', { nullable: true }) toDate?: string, @Arg('fromOptimismOnly', { nullable: true }) fromOptimismOnly?: boolean, + @Arg('onlyVerified', { nullable: true }) onlyVerified?: boolean, ): Promise { try { validateWithJoiSchema( @@ -296,20 +300,24 @@ export class DonationResolver { } } - const result = await query.getRawMany(); - return result; + if (onlyVerified) { + query.andWhere('projects.verified = true'); + } + + return await query.getRawMany(); } catch (e) { - logger.error('donations query error', e); + logger.error('totalDonationsPerCategory query error', e); throw e; } } - @Query(returns => ResourcePerDateRange, { nullable: true }) + @Query(_returns => ResourcePerDateRange, { nullable: true }) async donationsTotalUsdPerDate( // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss @Arg('fromDate', { nullable: true }) fromDate?: string, @Arg('toDate', { nullable: true }) toDate?: string, @Arg('fromOptimismOnly', { nullable: true }) fromOptimismOnly?: boolean, + @Arg('onlyVerified', { nullable: true }) onlyVerified?: boolean, ): Promise { try { validateWithJoiSchema( @@ -320,12 +328,14 @@ export class DonationResolver { fromDate, toDate, fromOptimismOnly, + onlyVerified, ); const totalPerMonthAndYear = await donationsTotalAmountPerDateRangeByMonth( fromDate, toDate, fromOptimismOnly, + onlyVerified, ); return { @@ -338,12 +348,13 @@ export class DonationResolver { } } - @Query(returns => ResourcePerDateRange, { nullable: true }) + @Query(_returns => ResourcePerDateRange, { nullable: true }) async totalDonationsNumberPerDate( // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss @Arg('fromDate', { nullable: true }) fromDate?: string, @Arg('toDate', { nullable: true }) toDate?: string, @Arg('fromOptimismOnly', { nullable: true }) fromOptimismOnly?: boolean, + @Arg('onlyVerified', { nullable: true }) onlyVerified?: boolean, ): Promise { try { validateWithJoiSchema( @@ -354,12 +365,14 @@ export class DonationResolver { fromDate, toDate, fromOptimismOnly, + onlyVerified, ); const totalPerMonthAndYear = await donationsTotalNumberPerDateRangeByMonth( fromDate, toDate, fromOptimismOnly, + onlyVerified, ); return { @@ -377,14 +390,14 @@ export class DonationResolver { * @param take * @return last donations' id, valueUd, createdAt, user.walletAddress and project.slug */ - @Query(returns => [Donation], { nullable: true }) + @Query(_returns => [Donation], { nullable: true }) async recentDonations( - @Arg('take', type => Int, { nullable: true }) take: number = 30, + @Arg('take', _type => Int, { nullable: true }) take: number = 30, ): Promise { return getRecentDonations(take); } - @Query(returns => ResourcePerDateRange, { nullable: true }) + @Query(_returns => ResourcePerDateRange, { nullable: true }) async totalDonorsCountPerDate( // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss @Arg('fromDate', { nullable: true }) fromDate?: string, @@ -416,11 +429,51 @@ export class DonationResolver { } } + @Query(_returns => ResourcePerDateRange, { nullable: true }) + async newDonorsCountPerDate( + // fromDate and toDate should be in this format (YYYY-MM-DD)T(HH:mm:ss)Z + @Arg('fromDate') fromDate: string, + @Arg('toDate') toDate: string, + ): Promise<{ total: number }> { + try { + validateWithJoiSchema( + { fromDate, toDate }, + resourcePerDateReportValidator, + ); + const newDonors = await newDonorsCount(fromDate, toDate); + return { + total: newDonors?.length || 0, + }; + } catch (e) { + logger.error('newDonorsCountPerDate query error', e); + throw e; + } + } + + @Query(_returns => ResourcePerDateRange, { nullable: true }) + async newDonorsDonationTotalUsdPerDate( + // fromDate and toDate should be in this format (YYYY-MM-DD)T(HH:mm:ss)Z + @Arg('fromDate') fromDate: string, + @Arg('toDate') toDate: string, + ): Promise<{ total: number }> { + try { + validateWithJoiSchema( + { fromDate, toDate }, + resourcePerDateReportValidator, + ); + const total = await newDonorsDonationTotalUsd(fromDate, toDate); + return { total }; + } catch (e) { + logger.error('newDonorsDonationTotalUsdPerDate query error', e); + throw e; + } + } + // TODO I think we can delete this resolver - @Query(returns => [Donation], { nullable: true }) + @Query(_returns => [Donation], { nullable: true }) async donationsFromWallets( - @Ctx() ctx: ApolloContext, - @Arg('fromWalletAddresses', type => [String]) + @Ctx() _ctx: ApolloContext, + @Arg('fromWalletAddresses', _type => [String]) fromWalletAddresses: string[], ) { const fromWalletAddressesArray: string[] = fromWalletAddresses.map(o => @@ -436,10 +489,10 @@ export class DonationResolver { } // TODO I think we can delete this resolver - @Query(returns => [Donation], { nullable: true }) + @Query(_returns => [Donation], { nullable: true }) async donationsToWallets( - @Ctx() ctx: ApolloContext, - @Arg('toWalletAddresses', type => [String]) toWalletAddresses: string[], + @Ctx() _ctx: ApolloContext, + @Arg('toWalletAddresses', _type => [String]) toWalletAddresses: string[], ) { const toWalletAddressesArray: string[] = toWalletAddresses.map(o => o.toLowerCase(), @@ -454,19 +507,19 @@ export class DonationResolver { .getMany(); } - @Query(returns => PaginateDonations, { nullable: true }) + @Query(_returns => PaginateDonations, { nullable: true }) async donationsByProjectId( - @Ctx() ctx: ApolloContext, - @Arg('take', type => Int, { defaultValue: 10 }) take: number, - @Arg('skip', type => Int, { defaultValue: 0 }) skip: number, - @Arg('traceable', type => Boolean, { defaultValue: false }) + @Ctx() _ctx: ApolloContext, + @Arg('take', _type => Int, { defaultValue: 10 }) take: number, + @Arg('skip', _type => Int, { defaultValue: 0 }) skip: number, + @Arg('traceable', _type => Boolean, { defaultValue: false }) traceable: boolean, - @Arg('qfRoundId', type => Int, { defaultValue: null, nullable: true }) + @Arg('qfRoundId', _type => Int, { defaultValue: null, nullable: true }) qfRoundId: number, - @Arg('projectId', type => Int, { nullable: false }) projectId: number, - @Arg('status', type => String, { nullable: true }) status: string, - @Arg('searchTerm', type => String, { nullable: true }) searchTerm: string, - @Arg('orderBy', type => SortBy, { + @Arg('projectId', _type => Int, { nullable: false }) projectId: number, + @Arg('status', _type => String, { nullable: true }) status: string, + @Arg('searchTerm', _type => String, { nullable: true }) searchTerm: string, + @Arg('orderBy', _type => SortBy, { defaultValue: { field: SortField.CreationDate, direction: SortDirection.DESC, @@ -488,7 +541,9 @@ export class DonationResolver { .leftJoin('donation.user', 'user') .leftJoinAndSelect('donation.qfRound', 'qfRound') .addSelect(publicSelectionFields) - .where(`donation.projectId = ${projectId}`) + .where( + `donation.projectId = ${projectId} AND donation.recurringDonationId IS NULL`, + ) .orderBy( `donation.${orderBy.field}`, orderBy.direction, @@ -534,6 +589,9 @@ export class DonationResolver { ); } + const recurringDonationsCount = + await countOfActiveRecurringDonationsByProjectId(projectId); + const [donations, donationsCount] = await query .take(take) .skip(skip) @@ -542,11 +600,12 @@ export class DonationResolver { donations, totalCount: donationsCount, totalUsdBalance: project.totalDonations, + recurringDonationsCount, }; } // TODO I think we can delete this resolver - @Query(returns => [Donation], { nullable: true }) + @Query(_returns => [Donation], { nullable: true }) async donationsByDonor(@Ctx() ctx: ApolloContext) { if (!ctx.req.user) throw new Error( @@ -555,13 +614,14 @@ export class DonationResolver { return this.donationRepository .createQueryBuilder('donation') .where({ userId: ctx.req.user.userId }) + .andWhere(`donation.recurringDonationId IS NULL`) .leftJoin('donation.user', 'user') .addSelect(publicSelectionFields) .leftJoinAndSelect('donation.project', 'project') .getMany(); } - @Query(returns => UserDonations, { nullable: true }) + @Query(_returns => UserDonations, { nullable: true }) async donationsByUserId( @Args() { take, skip, orderBy, userId, status }: UserDonationsArgs, @Ctx() ctx: ApolloContext, @@ -573,6 +633,7 @@ export class DonationResolver { .leftJoinAndSelect('donation.user', 'user') .leftJoinAndSelect('donation.qfRound', 'qfRound') .where(`donation.userId = ${userId}`) + .andWhere(`donation.recurringDonationId IS NULL`) .orderBy( `donation.${orderBy.field}`, orderBy.direction, @@ -598,7 +659,7 @@ export class DonationResolver { }; } - @Mutation(returns => Number) + @Mutation(_returns => Number) async createDonation( @Arg('amount') amount: number, @Arg('transactionId', { nullable: true }) transactionId: string, @@ -613,7 +674,7 @@ export class DonationResolver { @Arg('referrerId', { nullable: true }) referrerId?: string, @Arg('safeTransactionId', { nullable: true }) safeTransactionId?: string, @Arg('draftDonationId', { nullable: true }) draftDonationId?: number, - ): Promise { + ): Promise { const logData = { amount, transactionId, @@ -693,7 +754,7 @@ export class DonationResolver { symbol: token, }, }); - const isCustomToken = !Boolean(tokenInDb); + const isCustomToken = !tokenInDb; let isTokenEligibleForGivback = false; if (isCustomToken && !project.organization.supportCustomTokens) { throw new Error(i18n.__(translationErrorMessagesKeys.TOKEN_NOT_FOUND)); @@ -777,9 +838,8 @@ export class DonationResolver { logger.error('get chainvine wallet address error', e); } } - const activeQfRoundForProject = await relatedActiveQfRoundForProject( - projectId, - ); + const activeQfRoundForProject = + await relatedActiveQfRoundForProject(projectId); if ( activeQfRoundForProject && activeQfRoundForProject.isEligibleNetwork(networkId) @@ -806,7 +866,7 @@ export class DonationResolver { case NETWORK_IDS.GOERLI: priceChainId = NETWORK_IDS.MAIN_NET; break; - case NETWORK_IDS.OPTIMISM_GOERLI: + case NETWORK_IDS.OPTIMISM_SEPOLIA: priceChainId = NETWORK_IDS.OPTIMISTIC; break; case NETWORK_IDS.MORDOR_ETC_TESTNET: @@ -820,10 +880,8 @@ export class DonationResolver { await updateDonationPricesAndValues( donation, project, - tokenInDb, - token, + tokenInDb!, priceChainId, - amount, ); if (chainType === ChainType.EVM) { @@ -848,7 +906,7 @@ export class DonationResolver { } } - @Mutation(returns => Donation) + @Mutation(_returns => Donation) async updateDonationStatus( @Arg('donationId') donationId: number, @Arg('status', { nullable: true }) status: string, @@ -927,7 +985,7 @@ export class DonationResolver { @Arg('projectId', _ => Int) projectId: number, @Arg('qfRoundId', _ => Int) qfRoundId: number, @Arg('userId', _ => Int) userId: number, - ): Promise { + ): Promise { return isVerifiedDonationExistsInQfRound({ projectId, qfRoundId, diff --git a/src/resolvers/draftDonationResolver.test.ts b/src/resolvers/draftDonationResolver.test.ts index 486cf390b..81abe98b1 100644 --- a/src/resolvers/draftDonationResolver.test.ts +++ b/src/resolvers/draftDonationResolver.test.ts @@ -1,4 +1,5 @@ import { assert, expect } from 'chai'; +import axios from 'axios'; import { generateTestAccessToken, graphqlUrl, @@ -6,9 +7,12 @@ import { createProjectData, generateRandomEvmTxHash, generateRandomEtheriumAddress, + saveRecurringDonationDirectlyToDb, } from '../../test/testUtils'; -import axios from 'axios'; -import { createDraftDonationMutation } from '../../test/graphqlQueries'; +import { + createDraftDonationMutation, + createDraftRecurringDonationMutation, +} from '../../test/graphqlQueries'; import { NETWORK_IDS } from '../provider'; import { User } from '../entities/user'; import { generateRandomString } from '../utils/utils'; @@ -17,13 +21,18 @@ import { DRAFT_DONATION_STATUS, DraftDonation, } from '../entities/draftDonation'; +import { + DRAFT_RECURRING_DONATION_STATUS, + DraftRecurringDonation, +} from '../entities/draftRecurringDonation'; -// tslint:disable-next-line:no-var-requires -const moment = require('moment'); - -describe('createDonation() test cases', createDonationTestCases); +describe('createDraftDonation() test cases', createDraftDonationTestCases); +describe( + 'createDraftRecurringDonation() test cases', + createDraftRecurringDonationTestCases, +); -function createDonationTestCases() { +function createDraftDonationTestCases() { let project; let referrerId; let user; @@ -167,3 +176,181 @@ function createDonationTestCases() { ); }); } + +function createDraftRecurringDonationTestCases() { + let project; + let user; + let accessToken; + let donationData; + + beforeEach(async () => { + project = await saveProjectDirectlyToDb(createProjectData()); + + user = await User.create({ + walletAddress: generateRandomEtheriumAddress(), + loginType: 'wallet', + firstName: 'first name', + }).save(); + + accessToken = await generateTestAccessToken(user.id); + donationData = { + projectId: project.id, + networkId: NETWORK_IDS.XDAI, + flowRate: '100', + currency: 'GIV', + toAddress: project.walletAddress, + }; + }); + it('create simple draft recurring donation', async () => { + const saveDonationResponse = await axios.post( + graphqlUrl, + { + query: createDraftRecurringDonationMutation, + variables: donationData, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isOk(saveDonationResponse.data.data.createDraftRecurringDonation); + const draftRecurringDonation = await DraftRecurringDonation.findOne({ + where: { + id: saveDonationResponse.data.data.createDraftRecurringDonation, + }, + }); + + expect(draftRecurringDonation).deep.contain({ + networkId: donationData.networkId, + chainType: ChainType.EVM, + status: DRAFT_RECURRING_DONATION_STATUS.PENDING, + currency: 'GIV', + anonymous: false, + isBatch: false, + flowRate: donationData.flowRate, + projectId: project.id, + donorId: user.id, + }); + }); + it('create simple draft donation when isForUpdate:true but recurringDonation doesnt exist', async () => { + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: user.id, + projectId: project.id, + networkId: NETWORK_IDS.XDAI, + currency: 'GIV', + }, + }); + const saveDonationResponse = await axios.post( + graphqlUrl, + { + query: createDraftRecurringDonationMutation, + variables: { + ...donationData, + isForUpdate: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isOk(saveDonationResponse.data.data.createDraftRecurringDonation); + const draftRecurringDonation = await DraftRecurringDonation.findOne({ + where: { + id: saveDonationResponse.data.data.createDraftRecurringDonation, + }, + }); + + expect(draftRecurringDonation).deep.contain({ + networkId: donationData.networkId, + chainType: ChainType.EVM, + status: DRAFT_RECURRING_DONATION_STATUS.PENDING, + currency: 'GIV', + anonymous: false, + isBatch: false, + flowRate: donationData.flowRate, + projectId: project.id, + donorId: user.id, + }); + }); + + it.skip('should return the same draft recurring donation id if the same donation is created twice', async () => { + const saveDonationResponse = await axios.post( + graphqlUrl, + { + query: createDraftRecurringDonationMutation, + variables: donationData, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isOk(saveDonationResponse.data.data.createDraftRecurringDonation); + + const saveDonationResponse2 = await axios.post( + graphqlUrl, + { + query: createDraftRecurringDonationMutation, + variables: donationData, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isOk(saveDonationResponse2.data.data.createDraftRecurringDonation); + expect( + saveDonationResponse2.data.data.createDraftRecurringDonation, + ).to.be.equal(saveDonationResponse.data.data.createDraftRecurringDonation); + }); + + it('should create a new draft recurring donation if the first one is matched', async () => { + const saveDonationResponse = await axios.post( + graphqlUrl, + { + query: createDraftRecurringDonationMutation, + variables: donationData, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isOk(saveDonationResponse.data.data.createDraftRecurringDonation); + + const draftDonation = await DraftRecurringDonation.findOne({ + where: { + id: saveDonationResponse.data.data.createDraftRecurringDonation, + }, + }); + + draftDonation!.status = DRAFT_RECURRING_DONATION_STATUS.MATCHED; + await draftDonation!.save(); + + const saveDonationResponse2 = await axios.post( + graphqlUrl, + { + query: createDraftRecurringDonationMutation, + variables: donationData, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isOk(saveDonationResponse2.data.data.createDraftRecurringDonation); + expect( + saveDonationResponse2.data.data.createDraftRecurringDonation, + ).to.be.not.equal( + saveDonationResponse.data.data.createDraftRecurringDonation, + ); + }); +} diff --git a/src/resolvers/draftDonationResolver.ts b/src/resolvers/draftDonationResolver.ts index 752b34600..b211010a5 100644 --- a/src/resolvers/draftDonationResolver.ts +++ b/src/resolvers/draftDonationResolver.ts @@ -1,20 +1,16 @@ import { Arg, Ctx, Mutation, Resolver } from 'type-graphql'; -import { ApolloContext } from '../types/ApolloContext'; -import { ProjStatus } from '../entities/project'; -import { Token } from '../entities/token'; import { Repository } from 'typeorm'; +import { ApolloContext } from '../types/ApolloContext'; import { User } from '../entities/user'; import SentryLogger from '../sentryLogger'; import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; -import { isTokenAcceptableForProject } from '../services/donationService'; import { createDraftDonationQueryValidator, + createDraftRecurringDonationQueryValidator, validateWithJoiSchema, } from '../utils/validators/graphqlQueryValidators'; import { logger } from '../utils/logger'; import { findUserById } from '../repositories/userRepository'; -import { findProjectRecipientAddressByNetworkId } from '../repositories/projectAddressRepository'; -import { findProjectById } from '../repositories/projectRepository'; import { AppDataSource } from '../orm'; import { detectAddressChainType } from '../utils/networks'; import { ChainType } from '../types/network'; @@ -23,18 +19,27 @@ import { DRAFT_DONATION_STATUS, DraftDonation, } from '../entities/draftDonation'; +import { DraftRecurringDonation } from '../entities/draftRecurringDonation'; +import { + findRecurringDonationById, + findRecurringDonationByProjectIdAndUserIdAndCurrency, +} from '../repositories/recurringDonationRepository'; +import { RecurringDonation } from '../entities/recurringDonation'; const draftDonationEnabled = process.env.ENABLE_DRAFT_DONATION === 'true'; +const draftRecurringDonationEnabled = + process.env.ENABLE_DRAFT_RECURRING_DONATION === 'true'; -@Resolver(of => User) +@Resolver(_of => User) export class DraftDonationResolver { private readonly donationRepository: Repository; + constructor() { this.donationRepository = AppDataSource.getDataSource().getRepository(DraftDonation); } - @Mutation(returns => Number) + @Mutation(_returns => Number) async createDraftDonation( @Arg('amount') amount: number, @Arg('networkId') networkId: number, @@ -46,7 +51,7 @@ export class DraftDonationResolver { @Ctx() ctx: ApolloContext, @Arg('referrerId', { nullable: true }) referrerId?: string, @Arg('safeTransactionId', { nullable: true }) safeTransactionId?: string, - ): Promise { + ): Promise { const logData = { amount, networkId, @@ -156,4 +161,146 @@ export class DraftDonationResolver { throw e; } } + + @Mutation(_returns => Number) + async createDraftRecurringDonation( + @Arg('networkId') networkId: number, + @Arg('flowRate') flowRate: string, + @Arg('currency') currency: string, + @Arg('isBatch', { nullable: true, defaultValue: false }) isBatch: boolean, + @Arg('anonymous', { nullable: true, defaultValue: false }) + anonymous: boolean, + @Arg('projectId') projectId: number, + @Ctx() ctx: ApolloContext, + @Arg('recurringDonationId', { nullable: true }) + recurringDonationId?: number, + @Arg('isForUpdate', { nullable: true, defaultValue: false }) + isForUpdate?: boolean, + ): Promise { + const logData = { + flowRate, + networkId, + currency, + anonymous, + isBatch, + isForUpdate, + recurringDonationId, + projectId, + userId: ctx?.req?.user?.userId, + }; + logger.debug( + 'createDraftRecurringDonation() resolver has been called with this data', + logData, + ); + if (!draftRecurringDonationEnabled) { + throw new Error( + i18n.__(translationErrorMessagesKeys.DRAFT_RECURRING_DONATION_DISABLED), + ); + } + try { + const userId = ctx?.req?.user?.userId; + const donorUser = await findUserById(userId); + if (!donorUser) { + throw new Error(i18n.__(translationErrorMessagesKeys.UN_AUTHORIZED)); + } + const chainType = detectAddressChainType(donorUser.walletAddress!); + const _networkId = getAppropriateNetworkId({ + networkId, + chainType, + }); + + const validaDataInput = { + flowRate, + networkId: _networkId, + anonymous, + currency, + isBatch, + projectId, + chainType, + isForUpdate, + recurringDonationId, + }; + try { + validateWithJoiSchema( + validaDataInput, + createDraftRecurringDonationQueryValidator, + ); + } catch (e) { + logger.error( + 'Error on validating createDraftRecurringDonation input', + validaDataInput, + ); + throw e; // Rethrow the original error + } + let recurringDonation: RecurringDonation | null; + if (recurringDonationId && isForUpdate) { + recurringDonation = + await findRecurringDonationById(recurringDonationId); + if (!recurringDonation || recurringDonation.donorId !== donorUser.id) { + throw new Error( + i18n.__(translationErrorMessagesKeys.RECURRING_DONATION_NOT_FOUND), + ); + } + } else if (isForUpdate) { + recurringDonation = + await findRecurringDonationByProjectIdAndUserIdAndCurrency({ + projectId, + userId: donorUser.id, + currency, + }); + if (!recurringDonation || recurringDonation.donorId !== donorUser.id) { + throw new Error( + i18n.__(translationErrorMessagesKeys.RECURRING_DONATION_NOT_FOUND), + ); + } + } + if (chainType !== ChainType.EVM) { + throw new Error(i18n.__(translationErrorMessagesKeys.EVM_SUPPORT_ONLY)); + } + + const draftRecurringDonationId = + await DraftRecurringDonation.createQueryBuilder( + 'draftRecurringDonation', + ) + .insert() + .values({ + networkId: _networkId, + currency, + flowRate, + donorId: donorUser.id, + isBatch, + projectId, + isForUpdate, + anonymous: Boolean(anonymous), + chainType: chainType as ChainType, + matchedRecurringDonationId: recurringDonationId, + }) + .orIgnore() + .returning('id') + .execute(); + + if (draftRecurringDonationId.raw.length === 0) { + // TODO unreached code, because we dont have any unique index on this table, so we need to think about it + const existingDraftDonation = await DraftRecurringDonation.findOne({ + where: { + networkId: _networkId, + currency, + projectId, + donorId: donorUser.id, + flowRate, + }, + select: ['id'], + }); + return existingDraftDonation!.id; + } + return draftRecurringDonationId.raw[0].id; + } catch (e) { + SentryLogger.captureException(e); + logger.error('createDraftRecurringDonation() error', { + error: e, + inputData: logData, + }); + throw e; + } + } } diff --git a/src/resolvers/givPowerTestingResolver.ts b/src/resolvers/givPowerTestingResolver.ts index 58ccde0d8..3e4424180 100644 --- a/src/resolvers/givPowerTestingResolver.ts +++ b/src/resolvers/givPowerTestingResolver.ts @@ -32,64 +32,64 @@ const enableGivPower = process.env.ENABLE_GIV_POWER_TESTING as string; @ObjectType() class PowerBalances { - @Field(type => [PowerBalanceSnapshot], { nullable: true }) + @Field(_type => [PowerBalanceSnapshot], { nullable: true }) powerBalances?: PowerBalanceSnapshot[] | undefined; - @Field(type => Int) + @Field(_type => Int) count: number; } @ObjectType() class PowerSnapshots { - @Field(type => [PowerSnapshot], { nullable: true }) + @Field(_type => [PowerSnapshot], { nullable: true }) powerSnapshots?: PowerSnapshot[]; - @Field(type => Int) + @Field(_type => Int) count: number; } @ObjectType() class FuturePowers { - @Field(type => [ProjectFuturePowerView], { nullable: true }) + @Field(_type => [ProjectFuturePowerView], { nullable: true }) futurePowers?: ProjectFuturePowerView[]; - @Field(type => Int) + @Field(_type => Int) count: number; } @ObjectType() class ProjectsPowers { - @Field(type => [ProjectPowerView], { nullable: true }) + @Field(_type => [ProjectPowerView], { nullable: true }) projectsPowers?: ProjectPowerView[]; - @Field(type => Int) + @Field(_type => Int) count: number; } @ObjectType() class UserProjectBoostings { - @Field(type => [UserProjectPowerView], { nullable: true }) + @Field(_type => [UserProjectPowerView], { nullable: true }) userProjectBoostings?: UserProjectPowerView[]; - @Field(type => Int) + @Field(_type => Int) count: number; } @ObjectType() class UserPowerBoostings { - @Field(type => [PowerBoosting], { nullable: true }) + @Field(_type => [PowerBoosting], { nullable: true }) powerBoostings?: PowerBoosting[]; - @Field(type => Int) + @Field(_type => Int) count: number; } @ObjectType() class UserPowerBoostingsSnapshots { - @Field(type => [PowerBoostingSnapshot], { nullable: true }) + @Field(_type => [PowerBoostingSnapshot], { nullable: true }) userPowerBoostingsSnapshot?: PowerBoostingSnapshot[]; - @Field(type => Int) + @Field(_type => Int) count: number; } @@ -98,14 +98,14 @@ class UserPowerBoostingsSnapshots { export class GivPowerTestingResolver { // Returns powerBalances by userIds or powerSnapshotIds or round, or any combination of those 3 // Further filtering as required - @Query(returns => PowerBalances) + @Query(_returns => PowerBalances) async powerBalances( - @Arg('userIds', type => [Number], { defaultValue: [] }) userIds?: number[], - @Arg('powerSnapshotIds', type => [Number], { defaultValue: [] }) + @Arg('userIds', _type => [Number], { defaultValue: [] }) userIds?: number[], + @Arg('powerSnapshotIds', _type => [Number], { defaultValue: [] }) powerSnapshotIds?: number[], - @Arg('take', type => Number, { defaultValue: 100 }) take?: number, - @Arg('skip', type => Number, { defaultValue: 0 }) skip?: number, - @Arg('round', type => Number, { nullable: true }) round?: number, + @Arg('take', _type => Number, { defaultValue: 100 }) take?: number, + @Arg('skip', _type => Number, { defaultValue: 0 }) skip?: number, + @Arg('round', _type => Number, { nullable: true }) round?: number, ): Promise { if (enableGivPower !== 'true') { return { @@ -129,12 +129,12 @@ export class GivPowerTestingResolver { // This is so Testing team know what snapshots where taken and in which rounds // So they can use other endpoints - @Query(returns => PowerSnapshots) + @Query(_returns => PowerSnapshots) async powerSnapshots( - @Arg('take', type => Number, { defaultValue: 100 }) take?: number, - @Arg('skip', type => Number, { defaultValue: 0 }) skip?: number, - @Arg('round', type => Number, { nullable: true }) round?: number, - @Arg('powerSnapshotId', type => Number, { nullable: true }) + @Arg('take', _type => Number, { defaultValue: 100 }) take?: number, + @Arg('skip', _type => Number, { defaultValue: 0 }) skip?: number, + @Arg('round', _type => Number, { nullable: true }) round?: number, + @Arg('powerSnapshotId', _type => Number, { nullable: true }) powerSnapshotId?: number, ): Promise { if (enableGivPower !== 'true') { @@ -156,12 +156,12 @@ export class GivPowerTestingResolver { }; } - @Query(returns => UserPowerBoostings) + @Query(_returns => UserPowerBoostings) async userProjectPowerBoostings( - @Arg('take', type => Number, { defaultValue: 100 }) take?: number, - @Arg('skip', type => Number, { defaultValue: 0 }) skip?: number, - @Arg('projectId', type => Number, { nullable: true }) projectId?: number, - @Arg('userId', type => Number, { nullable: true }) userId?: number, + @Arg('take', _type => Number, { defaultValue: 100 }) take?: number, + @Arg('skip', _type => Number, { defaultValue: 0 }) skip?: number, + @Arg('projectId', _type => Number, { nullable: true }) projectId?: number, + @Arg('userId', _type => Number, { nullable: true }) userId?: number, ): Promise { if (enableGivPower !== 'true') { return { @@ -183,15 +183,15 @@ export class GivPowerTestingResolver { }; } - @Query(returns => UserPowerBoostingsSnapshots) + @Query(_returns => UserPowerBoostingsSnapshots) async userProjectPowerBoostingsSnapshots( - @Arg('take', type => Number, { defaultValue: 100 }) take?: number, - @Arg('skip', type => Number, { defaultValue: 0 }) skip?: number, - @Arg('projectId', type => Number, { nullable: true }) projectId?: number, - @Arg('userId', type => Number, { nullable: true }) userId?: number, - @Arg('powerSnapshotId', type => Number, { nullable: true }) + @Arg('take', _type => Number, { defaultValue: 100 }) take?: number, + @Arg('skip', _type => Number, { defaultValue: 0 }) skip?: number, + @Arg('projectId', _type => Number, { nullable: true }) projectId?: number, + @Arg('userId', _type => Number, { nullable: true }) userId?: number, + @Arg('powerSnapshotId', _type => Number, { nullable: true }) powerSnapshotId?: number, - @Arg('round', type => Number, { nullable: true }) round?: number, + @Arg('round', _type => Number, { nullable: true }) round?: number, ): Promise { if (enableGivPower !== 'true') { return { @@ -217,18 +217,18 @@ export class GivPowerTestingResolver { } // Know the current round running - @Query(returns => PowerRound) + @Query(_returns => PowerRound) async currentPowerRound(): Promise { return getPowerRound(); } - @Query(returns => FuturePowers) + @Query(_returns => FuturePowers) async projectsFuturePowers( - @Arg('projectIds', type => [Number], { defaultValue: [] }) + @Arg('projectIds', _type => [Number], { defaultValue: [] }) projectIds?: number[], - @Arg('round', type => Number, { nullable: true }) round?: number, - @Arg('take', type => Number, { defaultValue: 100 }) take?: number, - @Arg('skip', type => Number, { defaultValue: 0 }) skip?: number, + @Arg('round', _type => Number, { nullable: true }) round?: number, + @Arg('take', _type => Number, { defaultValue: 100 }) take?: number, + @Arg('skip', _type => Number, { defaultValue: 0 }) skip?: number, ): Promise { if (enableGivPower !== 'true') { return { @@ -250,13 +250,13 @@ export class GivPowerTestingResolver { }; } - @Query(returns => ProjectsPowers) + @Query(_returns => ProjectsPowers) async projectsPowers( - @Arg('projectIds', type => [Number], { defaultValue: [] }) + @Arg('projectIds', _type => [Number], { defaultValue: [] }) projectIds?: number[], - @Arg('round', type => Number, { nullable: true }) round?: number, - @Arg('take', type => Number, { defaultValue: 100 }) take?: number, - @Arg('skip', type => Number, { defaultValue: 0 }) skip?: number, + @Arg('round', _type => Number, { nullable: true }) round?: number, + @Arg('take', _type => Number, { defaultValue: 100 }) take?: number, + @Arg('skip', _type => Number, { defaultValue: 0 }) skip?: number, ): Promise { if (enableGivPower !== 'true') { return { @@ -278,7 +278,7 @@ export class GivPowerTestingResolver { }; } - @Query(returns => UserProjectBoostings) + @Query(_returns => UserProjectBoostings) async userProjectBoostings( @Args() { take, skip, projectId, userId, orderBy, round }: UserProjectPowerArgs, diff --git a/src/resolvers/instantPowerResolver.test.ts b/src/resolvers/instantPowerResolver.test.ts index 6f65a9b0a..dd17d325c 100644 --- a/src/resolvers/instantPowerResolver.test.ts +++ b/src/resolvers/instantPowerResolver.test.ts @@ -1,4 +1,5 @@ -import { assert, expect } from 'chai'; +import { expect } from 'chai'; +import axios from 'axios'; import { refreshProjectUserInstantPowerView, saveOrUpdateInstantPowerBalances, @@ -13,7 +14,6 @@ import { } from '../../test/testUtils'; import { PowerBoosting } from '../entities/powerBoosting'; import { insertSinglePowerBoosting } from './../repositories/powerBoostingRepository'; -import axios from 'axios'; import { getProjectUserInstantPowerQuery } from '../../test/graphqlQueries'; import { User } from '../entities/user'; import { Project } from '../entities/project'; diff --git a/src/resolvers/instantPowerResolver.ts b/src/resolvers/instantPowerResolver.ts index 4a5992bd1..1cb826de6 100644 --- a/src/resolvers/instantPowerResolver.ts +++ b/src/resolvers/instantPowerResolver.ts @@ -16,8 +16,8 @@ export class ProjectUserInstantPowerViewResolver { @Query(() => PaginatedProjectUserInstantPowerView, { nullable: true }) async getProjectUserInstantPower( @Arg('projectId', () => Int, { nullable: false }) projectId: number, - @Arg('take', type => Int, { nullable: true }) take?: number, - @Arg('skip', type => Int, { defaultValue: 0 }) skip?: number, + @Arg('take', _type => Int, { nullable: true }) take?: number, + @Arg('skip', _type => Int, { defaultValue: 0 }) skip?: number, ): Promise { const [projectUserInstantPowers, total] = await getProjectUserInstantPowerView(projectId, take, skip); diff --git a/src/resolvers/powerBoostingResolver.test.ts b/src/resolvers/powerBoostingResolver.test.ts index 0dd731f17..85e8041d0 100644 --- a/src/resolvers/powerBoostingResolver.test.ts +++ b/src/resolvers/powerBoostingResolver.test.ts @@ -1,3 +1,5 @@ +import axios, { AxiosResponse } from 'axios'; +import { assert } from 'chai'; import { createProjectData, generateRandomEtheriumAddress, @@ -7,14 +9,12 @@ import { saveUserDirectlyToDb, SEED_DATA, } from '../../test/testUtils'; -import axios, { AxiosResponse } from 'axios'; import { getBottomPowerRankQuery, getPowerBoostingsQuery, setMultiplePowerBoostingMutation, setSinglePowerBoostingMutation, } from '../../test/graphqlQueries'; -import { assert } from 'chai'; import { errorMessages } from '../utils/errorMessages'; import { PowerBoosting } from '../entities/powerBoosting'; import { @@ -769,9 +769,7 @@ function getPowerBoostingTestCases() { }); }); it('should get list of power with 1000 boostings filter by projectId', async () => { - const firstUser = await saveUserDirectlyToDb( - generateRandomEtheriumAddress(), - ); + await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb(createProjectData()); for (let i = 0; i < 1000; i++) { diff --git a/src/resolvers/powerBoostingResolver.ts b/src/resolvers/powerBoostingResolver.ts index d59d946f5..3feb19638 100644 --- a/src/resolvers/powerBoostingResolver.ts +++ b/src/resolvers/powerBoostingResolver.ts @@ -13,20 +13,16 @@ import { registerEnumType, Resolver, } from 'type-graphql'; +import { Max, Min } from 'class-validator'; +import { Service } from 'typedi'; import { ApolloContext } from '../types/ApolloContext'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { PowerBoosting } from '../entities/powerBoosting'; import { setMultipleBoosting, setSingleBoosting, findPowerBoostings, } from '../repositories/powerBoostingRepository'; -import { Max, Min } from 'class-validator'; -import { Service } from 'typedi'; import { logger } from '../utils/logger'; import { getBottomRank } from '../repositories/projectPowerViewRepository'; import { getNotificationAdapter } from '../adapters/adaptersFactory'; @@ -54,13 +50,13 @@ registerEnumType(PowerBoostingOrderDirection, { @InputType() class PowerBoostingOrderBy { - @Field(type => PowerBoostingOrderField, { + @Field(_type => PowerBoostingOrderField, { nullable: true, defaultValue: PowerBoostingOrderField.UpdatedAt, }) field: PowerBoostingOrderField; - @Field(type => PowerBoostingOrderDirection, { + @Field(_type => PowerBoostingOrderDirection, { nullable: true, defaultValue: PowerBoostingOrderDirection.DESC, }) @@ -70,16 +66,16 @@ class PowerBoostingOrderBy { @Service() @ArgsType() export class GetPowerBoostingArgs { - @Field(type => Int, { defaultValue: 0 }) + @Field(_type => Int, { defaultValue: 0 }) @Min(0) skip: number; - @Field(type => Int, { defaultValue: 1000 }) + @Field(_type => Int, { defaultValue: 1000 }) @Min(0) @Max(1000) take: number; - @Field(type => PowerBoostingOrderBy, { + @Field(_type => PowerBoostingOrderBy, { nullable: true, defaultValue: { field: PowerBoostingOrderField.UpdatedAt, @@ -88,28 +84,28 @@ export class GetPowerBoostingArgs { }) orderBy: PowerBoostingOrderBy; - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) projectId?: number; - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) userId?: number; } @ObjectType() class GivPowers { - @Field(type => [PowerBoosting]) + @Field(_type => [PowerBoosting]) powerBoostings: PowerBoosting[]; - @Field(type => Int) + @Field(_type => Int) totalCount: number; } -@Resolver(of => PowerBoosting) +@Resolver(_of => PowerBoosting) export class PowerBoostingResolver { - @Mutation(returns => [PowerBoosting]) + @Mutation(_returns => [PowerBoosting]) async setMultiplePowerBoosting( - @Arg('projectIds', type => [Int]) projectIds: number[], - @Arg('percentages', type => [Float]) percentages: number[], + @Arg('projectIds', _type => [Int]) projectIds: number[], + @Arg('percentages', _type => [Float]) percentages: number[], @Ctx() { req: { user } }: ApolloContext, ): Promise { const userId = user?.userId; @@ -133,10 +129,10 @@ export class PowerBoostingResolver { return result; } - @Mutation(returns => [PowerBoosting]) + @Mutation(_returns => [PowerBoosting]) async setSinglePowerBoosting( - @Arg('projectId', type => Int) projectId: number, - @Arg('percentage', type => Float) percentage: number, + @Arg('projectId', _type => Int) projectId: number, + @Arg('percentage', _type => Float) percentage: number, @Ctx() { req: { user } }: ApolloContext, ): Promise { const userId = user?.userId; @@ -159,7 +155,7 @@ export class PowerBoostingResolver { return result; } - @Query(returns => GivPowers) + @Query(_returns => GivPowers) async getPowerBoosting( @Args() { take, skip, projectId, userId, orderBy }: GetPowerBoostingArgs, @@ -184,8 +180,8 @@ export class PowerBoostingResolver { }; } - @Query(returns => Number) - async getTopPowerRank(): Promise { + @Query(_returns => Number) + async getTopPowerRank(): Promise { return getBottomRank(); } } diff --git a/src/resolvers/projectPowerResolver.test.ts b/src/resolvers/projectPowerResolver.test.ts index d71646ebb..bd7045f72 100644 --- a/src/resolvers/projectPowerResolver.test.ts +++ b/src/resolvers/projectPowerResolver.test.ts @@ -1,3 +1,5 @@ +import axios from 'axios'; +import { assert } from 'chai'; import { createProjectData, generateRandomEtheriumAddress, @@ -5,9 +7,7 @@ import { saveProjectDirectlyToDb, saveUserDirectlyToDb, } from '../../test/testUtils'; -import axios from 'axios'; import { getPowerAmountRankQuery } from '../../test/graphqlQueries'; -import { assert } from 'chai'; import { PowerBalanceSnapshot } from '../entities/powerBalanceSnapshot'; import { PowerBoostingSnapshot } from '../entities/powerBoostingSnapshot'; import { diff --git a/src/resolvers/projectPowerResolver.ts b/src/resolvers/projectPowerResolver.ts index 141f8c118..ed1724e6a 100644 --- a/src/resolvers/projectPowerResolver.ts +++ b/src/resolvers/projectPowerResolver.ts @@ -2,12 +2,12 @@ import { Arg, Query, Resolver, Int } from 'type-graphql'; import { getPowerAmountRank } from '../repositories/projectPowerViewRepository'; import { ProjectPowerView } from '../views/projectPowerView'; -@Resolver(of => ProjectPowerView) +@Resolver(_of => ProjectPowerView) export class ProjectPowerResolver { - @Query(returns => Int) + @Query(_returns => Int) async powerAmountRank( @Arg('powerAmount', { nullable: false }) powerAmount: number, - @Arg('projectId', type => Int, { nullable: true }) projectId: number, + @Arg('projectId', _type => Int, { nullable: true }) projectId: number, ): Promise { return await getPowerAmountRank(powerAmount, projectId); } diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts index 529b30d6e..5d1ae0678 100644 --- a/src/resolvers/projectResolver.allProject.test.ts +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -1,5 +1,7 @@ import { assert } from 'chai'; import 'mocha'; +import axios from 'axios'; +import moment from 'moment'; import { createDonationData, createProjectData, @@ -12,7 +14,6 @@ import { saveUserDirectlyToDb, SEED_DATA, } from '../../test/testUtils'; -import axios from 'axios'; import { fetchMultiFilterAllProjectsQuery } from '../../test/graphqlQueries'; import { Project, ReviewStatus, SortingField } from '../entities/project'; import { User } from '../entities/user'; @@ -27,14 +28,12 @@ import { refreshProjectPowerView } from '../repositories/projectPowerViewReposit import { PowerBalanceSnapshot } from '../entities/powerBalanceSnapshot'; import { PowerBoostingSnapshot } from '../entities/powerBoostingSnapshot'; import { ProjectAddress } from '../entities/projectAddress'; -import moment from 'moment'; import { PowerBoosting } from '../entities/powerBoosting'; import { AppDataSource } from '../orm'; // We are using cache so redis needs to be cleared for tests with same filters import { redis } from '../redis'; import { Campaign, CampaignType } from '../entities/campaign'; import { generateRandomString, getHtmlTextSummary } from '../utils/utils'; -import { ArgumentValidationError } from 'type-graphql'; import { InstantPowerBalance } from '../entities/instantPowerBalance'; import { saveOrUpdateInstantPowerBalances } from '../repositories/instantBoostingRepository'; import { updateInstantBoosting } from '../services/instantBoostingServices'; @@ -48,10 +47,6 @@ import { addOrUpdatePowerSnapshotBalances } from '../repositories/powerBalanceSn import { findPowerSnapshots } from '../repositories/powerSnapshotRepository'; import { ChainType } from '../types/network'; -const ARGUMENT_VALIDATION_ERROR_MESSAGE = new ArgumentValidationError([ - { property: '' }, -]).message; - // search and filters describe('all projects test cases --->', allProjectsTestCases); @@ -126,16 +121,19 @@ function allProjectsTestCases() { result = await axios.post(graphqlUrl, { query: fetchMultiFilterAllProjectsQuery, variables: { - limit, searchTerm: SEED_DATA.FIRST_PROJECT.title, connectedWalletUserId: USER_DATA.id, }, }); projects = result.data.data.allProjects.projects; - assert.equal(projects.length, limit); + // Find the project with the exact title + const selectedProject = projects.find( + ({ title }) => title === SEED_DATA.FIRST_PROJECT.title, + ); + assert.isAtLeast(projects.length, limit); assert.equal( - projects[0]?.reaction?.id, + selectedProject?.reaction?.id, REACTION_SEED_DATA.FIRST_LIKED_PROJECT_REACTION.id, ); projects.forEach(project => { @@ -178,7 +176,7 @@ function allProjectsTestCases() { title: String(new Date().getTime()), slug: String(new Date().getTime()), }); - const secondProject = await saveProjectDirectlyToDb({ + await saveProjectDirectlyToDb({ ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), @@ -462,9 +460,7 @@ function allProjectsTestCases() { ...createProjectData(), verified: false, }); // Not boosted -Not verified project - const project5 = await saveProjectDirectlyToDb(createProjectData()); // Not boosted project - - const roundNumber = project3.id * 10; + await saveProjectDirectlyToDb(createProjectData()); // Not boosted project await Promise.all( [ @@ -559,11 +555,11 @@ function allProjectsTestCases() { const project1 = await saveProjectDirectlyToDb(createProjectData()); const project2 = await saveProjectDirectlyToDb(createProjectData()); const project3 = await saveProjectDirectlyToDb(createProjectData()); - const project4 = await saveProjectDirectlyToDb({ + await saveProjectDirectlyToDb({ ...createProjectData(), verified: false, }); // Not boosted -Not verified project - const project5 = await saveProjectDirectlyToDb(createProjectData()); // Not boosted project + await saveProjectDirectlyToDb(createProjectData()); // Not boosted project const roundNumber = project3.id * 10; @@ -1253,7 +1249,7 @@ function allProjectsTestCases() { address => address.isRecipient === true && (address.networkId === NETWORK_IDS.OPTIMISTIC || - address.networkId === NETWORK_IDS.OPTIMISM_GOERLI) && + address.networkId === NETWORK_IDS.OPTIMISM_SEPOLIA) && address.chainType === ChainType.EVM, ), ); @@ -1469,7 +1465,7 @@ function allProjectsTestCases() { // Delete all project addresses await ProjectAddress.delete({ chainType: ChainType.SOLANA }); - const project = await saveProjectDirectlyToDb({ + await saveProjectDirectlyToDb({ ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index 72e697b4c..5138d9554 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -1,18 +1,19 @@ import { assert, expect } from 'chai'; import 'mocha'; +import axios from 'axios'; +import moment from 'moment'; +import { ArgumentValidationError } from 'type-graphql'; import { createProjectData, generateRandomEtheriumAddress, generateRandomSolanaAddress, generateTestAccessToken, graphqlUrl, - PROJECT_UPDATE_SEED_DATA, saveFeaturedProjectDirectlyToDb, saveProjectDirectlyToDb, saveUserDirectlyToDb, SEED_DATA, } from '../../test/testUtils'; -import axios from 'axios'; import { activateProjectQuery, addProjectUpdateQuery, @@ -25,6 +26,7 @@ import { fetchFeaturedProjectUpdate, fetchLatestProjectUpdates, fetchLikedProjectsQuery, + fetchMultiFilterAllProjectsQuery, fetchNewProjectsPerDate, fetchProjectBySlugQuery, fetchProjectUpdatesQuery, @@ -48,6 +50,7 @@ import { ProjectUpdate, ProjStatus, ReviewStatus, + RevokeSteps, } from '../entities/project'; import { Category } from '../entities/category'; import { Reaction } from '../entities/reaction'; @@ -79,7 +82,6 @@ import { import { PowerBalanceSnapshot } from '../entities/powerBalanceSnapshot'; import { PowerBoostingSnapshot } from '../entities/powerBoostingSnapshot'; import { ProjectAddress } from '../entities/projectAddress'; -import moment from 'moment'; import { PowerBoosting } from '../entities/powerBoosting'; import { refreshUserProjectPowerView } from '../repositories/userProjectPowerViewRepository'; import { AppDataSource } from '../orm'; @@ -96,7 +98,6 @@ import { PROJECT_DESCRIPTION_MAX_LENGTH, PROJECT_TITLE_MAX_LENGTH, } from '../constants/validators'; -import { ArgumentValidationError } from 'type-graphql'; import { InstantPowerBalance } from '../entities/instantPowerBalance'; import { saveOrUpdateInstantPowerBalances } from '../repositories/instantBoostingRepository'; import { updateInstantBoosting } from '../services/instantBoostingServices'; @@ -159,6 +160,8 @@ describe( similarProjectsBySlugTestCases, ); +describe('projectSearch test cases --->', projectSearchTestCases); + describe('projectUpdates query test cases --->', projectUpdatesTestCases); describe( @@ -174,11 +177,11 @@ describe('projectsPerDate() test cases --->', projectsPerDateTestCases); function projectsPerDateTestCases() { it('should projects created in a time range', async () => { - const project = await saveProjectDirectlyToDb({ + await saveProjectDirectlyToDb({ ...createProjectData(), creationDate: moment().add(10, 'days').toDate(), }); - const project2 = await saveProjectDirectlyToDb({ + await saveProjectDirectlyToDb({ ...createProjectData(), creationDate: moment().add(44, 'days').toDate(), }); @@ -546,6 +549,66 @@ function projectsByUserIdTestCases() { } function createProjectTestCases() { + it('should not create projects with same slug and title', async () => { + const accessToken = await generateTestAccessToken(SEED_DATA.FIRST_USER.id); + const sampleProject1 = { + title: 'title1', + admin: String(SEED_DATA.FIRST_USER.id), + addresses: [ + { + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.XDAI, + }, + ], + }; + const sampleProject2 = { + title: 'title1', + admin: String(SEED_DATA.FIRST_USER.id), + addresses: [ + { + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.XDAI, + }, + ], + }; + const promise1 = axios.post( + graphqlUrl, + { + query: createProjectQuery, + variables: { + project: sampleProject1, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + const promise2 = axios.post( + graphqlUrl, + { + query: createProjectQuery, + variables: { + project: sampleProject2, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + 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'); + }); it('Create Project should return <>, calling without token IN ENGLISH when no-lang header is sent', async () => { const sampleProject = { title: 'title1', @@ -1762,10 +1825,6 @@ function updateProjectTestCases() { }); const newWalletAddress = project.walletAddress; - const queriedAddress0 = await findAllRelatedAddressByWalletAddress( - walletAddress, - ); - const editProjectResult = await axios.post( graphqlUrl, { @@ -1826,7 +1885,6 @@ function updateProjectTestCases() { admin: String(user.id), }); const newWalletAddress = generateRandomEtheriumAddress(); - const newWalletAddress2 = generateRandomEtheriumAddress(); const editProjectResult = await axios.post( graphqlUrl, { @@ -3982,7 +4040,7 @@ function featuredProjectUpdateTestCases() { isMain: false, }).save(); - const featuredProject = await saveFeaturedProjectDirectlyToDb( + await saveFeaturedProjectDirectlyToDb( Number(project.id), Number(projectUpdate.id), ); @@ -4018,15 +4076,16 @@ function featureProjectsTestCases() { [ReviewStatus.NotReviewed, ProjStatus.active], // Not listed [ReviewStatus.Listed, ProjStatus.deactive], // Not active ]; - const projectsPromises = settings.map(([reviewStatus, projectStatus]) => { - return saveProjectDirectlyToDb({ + const projects: Project[] = []; + for (const element of settings) { + const project = await saveProjectDirectlyToDb({ ...createProjectData(), - reviewStatus, - statusId: projectStatus, + reviewStatus: element[0], + statusId: element[1], }); - }); - const projects = await Promise.all(projectsPromises); - const projetUpdatePromises = projects.map(project => { + projects.push(project); + } + const projectUpdatePromises = projects.map(project => { return ProjectUpdate.create({ userId: user!.id, projectId: project.id, @@ -4036,7 +4095,7 @@ function featureProjectsTestCases() { isMain: false, }).save(); }); - const projectUpdates = await Promise.all(projetUpdatePromises); + const projectUpdates = await Promise.all(projectUpdatePromises); for (let i = 0; i < projects.length; i++) { const project = projects[i]; const projectUpdate = projectUpdates[i]; @@ -4145,6 +4204,50 @@ function projectUpdatesTestCases() { }); } +function projectSearchTestCases() { + it('should return projects with a typo in the end of searchTerm', async () => { + const limit = 1; + const USER_DATA = SEED_DATA.FIRST_USER; + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + limit, + // Typo in the title + searchTerm: SEED_DATA.SECOND_PROJECT.title.slice(0, -1) + 'a', + connectedWalletUserId: USER_DATA.id, + }, + }); + + const projects = result.data.data.allProjects.projects; + assert.equal(projects.length, limit); + assert.equal(projects[0].title, SEED_DATA.SECOND_PROJECT.title); + assert.equal(projects[0].slug, SEED_DATA.SECOND_PROJECT.slug); + assert.equal(projects[0].id, SEED_DATA.SECOND_PROJECT.id); + }); + + it('should return projects with the project title inverted in the searchTerm', async () => { + const limit = 1; + const USER_DATA = SEED_DATA.FIRST_USER; + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + limit, + searchTerm: SEED_DATA.SECOND_PROJECT.title + .split(' ') + .reverse() + .join(' '), + connectedWalletUserId: USER_DATA.id, + }, + }); + + const projects = result.data.data.allProjects.projects; + assert.equal(projects.length, limit); + assert.equal(projects[0].title, SEED_DATA.SECOND_PROJECT.title); + assert.equal(projects[0].slug, SEED_DATA.SECOND_PROJECT.slug); + assert.equal(projects[0].id, SEED_DATA.SECOND_PROJECT.id); + }); +} + function getProjectUpdatesTestCases() { it('should return project updates with current take', async () => { const take = 2; @@ -4171,14 +4274,14 @@ function getProjectUpdatesTestCases() { }, }); assert.isOk(result); - const projectUpdates: ProjectUpdate[] = result.data.data.getProjectUpdates; + // const projectUpdates: ProjectUpdate[] = result.data.data.getProjectUpdates; - const likedProject = projectUpdates.find( - pu => +pu.id === PROJECT_UPDATE_SEED_DATA.FIRST_PROJECT_UPDATE.id, - ); - const noLikedProject = projectUpdates.find( - pu => +pu.id !== PROJECT_UPDATE_SEED_DATA.FIRST_PROJECT_UPDATE.id, - ); + // const likedProject = projectUpdates.find( + // pu => +pu.id === PROJECT_UPDATE_SEED_DATA.FIRST_PROJECT_UPDATE.id, + // ); + // const noLikedProject = projectUpdates.find( + // pu => +pu.id !== PROJECT_UPDATE_SEED_DATA.FIRST_PROJECT_UPDATE.id, + // ); // assert.equal( // likedProject?.reaction?.id, @@ -5041,7 +5144,7 @@ function similarProjectsBySlugTestCases() { }); const c = await Category.findOne({ where: { name: 'food8' } }); - const [_, relatedCount] = await Project.createQueryBuilder('project') + const [, relatedCount] = await Project.createQueryBuilder('project') .innerJoinAndSelect('project.categories', 'categories') .where('categories.id IN (:...ids)', { ids: [c?.id] }) .andWhere('project.id != :id', { id: viewedProject.id }) @@ -5086,7 +5189,7 @@ function similarProjectsBySlugTestCases() { }); const totalCount = result.data.data.similarProjectsBySlug.totalCount; - const [_, relatedCount] = await Project.createQueryBuilder('project') + const [, relatedCount] = await Project.createQueryBuilder('project') .innerJoinAndSelect('project.categories', 'categories') .where('project.id != :id', { id: viewedProject?.id }) .andWhere('project.admin = :ownerId', { @@ -5159,6 +5262,62 @@ function addProjectUpdateTestCases() { 'testProjectUpdateFateme', ); }); + + it('should change verificationStatus to null after adding update', async () => { + const verifiedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + verificationStatus: RevokeSteps.UpForRevoking, + }); + const revokedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + verificationStatus: RevokeSteps.Revoked, + }); + const accessTokenUser1 = await generateTestAccessToken( + verifiedProject.adminUserId, + ); + + await axios.post( + graphqlUrl, + { + query: addProjectUpdateQuery, + variables: { + projectId: verifiedProject.id, + content: 'Test Project Update content', + title: 'test Project Update title', + }, + }, + { + headers: { + Authorization: `Bearer ${accessTokenUser1}`, + }, + }, + ); + await axios.post( + graphqlUrl, + { + query: addProjectUpdateQuery, + variables: { + projectId: revokedProject.id, + content: 'Test Project Update content', + title: 'test Project Update title', + }, + }, + { + headers: { + Authorization: `Bearer ${accessTokenUser1}`, + }, + }, + ); + const _verifiedProject = await Project.findOne({ + where: { id: verifiedProject.id }, + }); + const _revokedProject = await Project.findOne({ + where: { id: revokedProject.id }, + }); + assert.equal(_verifiedProject?.verificationStatus, null); + assert.equal(_revokedProject?.verificationStatus, RevokeSteps.Revoked); + }); + it('should can not add project update because of ownerShip ', async () => { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index ecf8d01fc..ad2f6726f 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1,3 +1,22 @@ +import { Max, Min } from 'class-validator'; +import { Brackets, Repository } from 'typeorm'; +import { Service } from 'typedi'; +import { + Arg, + Args, + ArgsType, + Ctx, + Field, + InputType, + Int, + Mutation, + ObjectType, + Query, + registerEnumType, + Resolver, +} from 'type-graphql'; +import { SelectQueryBuilder } from 'typeorm/query-builder/SelectQueryBuilder'; +import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; import { Reaction } from '../entities/reaction'; import { FilterField, @@ -6,6 +25,7 @@ import { ProjectUpdate, ProjStatus, ReviewStatus, + RevokeSteps, SortingField, } from '../entities/project'; import { ProjectStatus } from '../entities/projectStatus'; @@ -19,27 +39,10 @@ import { Category } from '../entities/category'; import { Donation } from '../entities/donation'; import { ProjectImage } from '../entities/projectImage'; import { ApolloContext } from '../types/ApolloContext'; -import { Max, Min } from 'class-validator'; import { publicSelectionFields, User } from '../entities/user'; +import config from '../config'; import { Context } from '../context'; -import { Brackets, Repository } from 'typeorm'; -import { Service } from 'typedi'; import SentryLogger from '../sentryLogger'; -import { - Arg, - Args, - ArgsType, - Ctx, - Field, - ID, - InputType, - Int, - Mutation, - ObjectType, - Query, - registerEnumType, - Resolver, -} from 'type-graphql'; import { errorMessages, i18n, @@ -54,7 +57,6 @@ import { } from '../utils/validators/projectValidator'; import { updateTotalProjectUpdatesOfAProject } from '../services/projectUpdatesService'; import { logger } from '../utils/logger'; -import { SelectQueryBuilder } from 'typeorm/query-builder/SelectQueryBuilder'; import { getLoggedInUser } from '../services/authorizationServices'; import { getAppropriateSlug, @@ -95,9 +97,8 @@ import { } from '../repositories/projectPowerViewRepository'; import { ResourcePerDateRange } from './donationResolver'; import { findUserReactionsByProjectIds } from '../repositories/reactionRepository'; -import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; import { AppDataSource } from '../orm'; -import { creteSlugFromProject } from '../utils/utils'; +import { creteSlugFromProject, isSocialMediaEqual } from '../utils/utils'; import { findCampaignBySlug } from '../repositories/campaignRepository'; import { Campaign } from '../entities/campaign'; import { FeaturedUpdate } from '../entities/featuredUpdate'; @@ -107,10 +108,11 @@ import { ProjectBySlugResponse } from './types/projectResolver'; import { ChainType } from '../types/network'; import { findActiveQfRound } from '../repositories/qfRoundRepository'; import { getAllProjectsRelatedToActiveCampaigns } from '../services/campaignService'; +import { getAppropriateNetworkId } from '../services/chains'; import { - getAppropriateNetworkId, - getDefaultSolanaChainId, -} from '../services/chains'; + addBulkProjectSocialMedia, + removeProjectSocialMedia, +} from '../repositories/projectSocialMediaRepository'; const projectFiltersCacheDuration = Number( process.env.PROJECT_FILTERS_THREADS_POOL_DURATION || 60000, @@ -118,43 +120,34 @@ const projectFiltersCacheDuration = Number( @ObjectType() class AllProjects { - @Field(type => [Project]) + @Field(_type => [Project]) projects: Project[]; - @Field(type => Int) + @Field(_type => Int) totalCount: number; - @Field(type => [Category], { nullable: true }) + @Field(_type => [Category], { nullable: true }) categories: Category[]; - @Field(type => Campaign, { nullable: true }) + @Field(_type => Campaign, { nullable: true }) campaign?: Campaign; } @ObjectType() class TopProjects { - @Field(type => [Project]) + @Field(_type => [Project]) projects: Project[]; - @Field(type => Int) + @Field(_type => Int) totalCount: number; } -@ObjectType() -class ProjectAndAdmin { - @Field(type => Project) - project: Project; - - @Field(type => User, { nullable: true }) - admin: User; -} - @ObjectType() class ProjectUpdatesResponse { - @Field(type => [ProjectUpdate]) + @Field(_type => [ProjectUpdate]) projectUpdates: ProjectUpdate[]; - @Field(type => Int, { nullable: false }) + @Field(_type => Int, { nullable: false }) count: number; } @@ -190,40 +183,40 @@ registerEnumType(ChainType, { @InputType() export class OrderBy { - @Field(type => OrderField) + @Field(_type => OrderField) field: OrderField; - @Field(type => OrderDirection) + @Field(_type => OrderDirection) direction: OrderDirection; } @InputType() export class FilterBy { - @Field(type => FilterField, { nullable: true }) + @Field(_type => FilterField, { nullable: true }) field: FilterField; - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) value: boolean; } @Service() @ArgsType() class GetProjectsArgs { - @Field(type => Int, { defaultValue: 0 }) + @Field(_type => Int, { defaultValue: 0 }) @Min(0) skip: number; - @Field(type => Int, { defaultValue: 10 }) + @Field(_type => Int, { defaultValue: 10 }) @Min(0) @Max(50) take: number; - @Field(type => Int, { defaultValue: 10 }) + @Field(_type => Int, { defaultValue: 10 }) @Min(0) @Max(50) limit: number; - @Field(type => OrderBy, { + @Field(_type => OrderBy, { defaultValue: { field: OrderField.GIVPower, direction: OrderDirection.DESC, @@ -231,7 +224,7 @@ class GetProjectsArgs { }) orderBy: OrderBy; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) searchTerm: string; @Field({ nullable: true }) @@ -240,24 +233,24 @@ class GetProjectsArgs { @Field({ nullable: true }) mainCategory: string; - @Field(type => FilterBy, { + @Field(_type => FilterBy, { nullable: true, defaultValue: { field: null, value: null }, }) filterBy: FilterBy; - @Field(type => [FilterField], { + @Field(_type => [FilterField], { nullable: true, defaultValue: [], }) filters: FilterField[]; - @Field(type => String, { + @Field(_type => String, { nullable: true, }) campaignSlug: string; - @Field(type => SortingField, { + @Field(_type => SortingField, { nullable: true, defaultValue: SortingField.QualityScore, }) @@ -266,36 +259,29 @@ class GetProjectsArgs { @Field({ nullable: true }) admin?: number; - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) connectedWalletUserId?: number; - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) qfRoundId?: number; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) qfRoundSlug?: string; } -@Service() -@ArgsType() -class GetProjectArgs { - @Field(type => ID!, { defaultValue: 0 }) - id: number; -} - @ObjectType() class ImageResponse { - @Field(type => String) + @Field(_type => String) url: string; - @Field(type => Number, { nullable: true }) + @Field(_type => Number, { nullable: true }) projectId?: number; - @Field(type => Number) + @Field(_type => Number) projectImageId: number; } -@Resolver(of => Project) +@Resolver(_of => Project) export class ProjectResolver { static addCategoryQuery( query: SelectQueryBuilder, @@ -333,21 +319,39 @@ export class ProjectResolver { searchTerm?: string, ) { if (!searchTerm) return query; + const similarityThreshold = + Number(config.get('PROJECT_SEARCH_SIMILARITY_THRESHOLD')) || 0.4; return query.andWhere( new Brackets(qb => { - qb.where('project.title ILIKE :searchTerm', { - searchTerm: `%${searchTerm}%`, - }) - .orWhere('project.description ILIKE :searchTerm', { - searchTerm: `%${searchTerm}%`, - }) - .orWhere('project.impactLocation ILIKE :searchTerm', { - searchTerm: `%${searchTerm}%`, - }) - .orWhere('user.name ILIKE :searchTerm', { + qb.where( + 'WORD_SIMILARITY(project.title, :searchTerm) > :similarityThreshold', + { searchTerm: `%${searchTerm}%`, - }); + similarityThreshold, + }, + ) + .orWhere( + 'WORD_SIMILARITY(project.description, :searchTerm) > :similarityThreshold', + { + searchTerm: `%${searchTerm}%`, + similarityThreshold, + }, + ) + .orWhere( + 'WORD_SIMILARITY(project.impactLocation, :searchTerm) > :similarityThreshold', + { + searchTerm: `%${searchTerm}%`, + similarityThreshold, + }, + ) + .orWhere( + 'WORD_SIMILARITY(user.name, :searchTerm) > :similarityThreshold', + { + searchTerm: `%${searchTerm}%`, + similarityThreshold, + }, + ); }), ); } @@ -543,7 +547,7 @@ export class ProjectResolver { networkIds.push(NETWORK_IDS.OPTIMISTIC); // Add this to make sure works on Staging - networkIds.push(NETWORK_IDS.OPTIMISM_GOERLI); + networkIds.push(NETWORK_IDS.OPTIMISM_SEPOLIA); return; case FilterField.AcceptFundOnETC: networkIds.push(NETWORK_IDS.ETC); @@ -647,7 +651,7 @@ export class ProjectResolver { this.projectImageRepository = ds.getRepository(ProjectImage); } - @Query(returns => TopProjects) + @Query(_returns => TopProjects) async featuredProjects( @Args() { limit, skip, connectedWalletUserId }: GetProjectsArgs, @@ -683,9 +687,9 @@ export class ProjectResolver { }; } - @Query(returns => ProjectUpdate) + @Query(_returns => ProjectUpdate) async featuredProjectUpdate( - @Arg('projectId', type => Int, { nullable: false }) projectId: number, + @Arg('projectId', _type => Int, { nullable: false }) projectId: number, ): Promise { const featuredProject = await FeaturedUpdate.createQueryBuilder( 'featuredProject', @@ -699,7 +703,7 @@ export class ProjectResolver { return featuredProject!.projectUpdate; } - @Query(returns => AllProjects) + @Query(_returns => AllProjects) async allProjects( @Args() { @@ -721,7 +725,10 @@ export class ProjectResolver { let totalCount: number; let activeQfRoundId: number | undefined; - if (sortingBy === SortingField.ActiveQfRoundRaisedFunds) { + if ( + sortingBy === SortingField.ActiveQfRoundRaisedFunds || + sortingBy === SortingField.EstimatedMatching + ) { activeQfRoundId = (await findActiveQfRound())?.id; } @@ -761,6 +768,7 @@ export class ProjectResolver { cache: projectFiltersCacheDuration, }); + // eslint-disable-next-line prefer-const [projects, totalCount] = await projectsQuery .cache(projectsQueryCacheKey, projectFiltersCacheDuration) .getManyAndCount(); @@ -784,7 +792,7 @@ export class ProjectResolver { return { projects, totalCount, categories, campaign }; } - @Query(returns => TopProjects) + @Query(_returns => TopProjects) async topProjects( @Args() { take, skip, orderBy, category, connectedWalletUserId }: GetProjectsArgs, @@ -820,10 +828,10 @@ export class ProjectResolver { return { projects, totalCount }; } - @Query(returns => Project) + @Query(_returns => Project) async projectById( @Arg('id') id: number, - @Arg('connectedWalletUserId', type => Int, { nullable: true }) + @Arg('connectedWalletUserId', _type => Int, { nullable: true }) connectedWalletUserId: number, @Ctx() { req: { user } }: ApolloContext, ) { @@ -841,6 +849,7 @@ export class ProjectResolver { ) .leftJoinAndSelect('categories.mainCategory', 'mainCategory') .leftJoinAndSelect('project.addresses', 'addresses') + .leftJoinAndSelect('project.socialMedia', 'socialMedia') .leftJoinAndSelect('project.anchorContracts', 'anchor_contract_address') .leftJoinAndSelect('project.organization', 'organization') .leftJoin('project.adminUser', 'user') @@ -853,10 +862,10 @@ export class ProjectResolver { return project; } - @Query(returns => ProjectBySlugResponse) + @Query(_returns => ProjectBySlugResponse) async projectBySlug( @Arg('slug') slug: string, - @Arg('connectedWalletUserId', type => Int, { nullable: true }) + @Arg('connectedWalletUserId', _type => Int, { nullable: true }) connectedWalletUserId: number, @Ctx() { req: { user } }: ApolloContext, ) { @@ -891,6 +900,7 @@ export class ProjectResolver { .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') @@ -939,7 +949,7 @@ export class ProjectResolver { return { ...project, givbackFactor }; } - @Mutation(returns => Project) + @Mutation(_returns => Project) async updateProject( @Arg('projectId') projectId: number, @Arg('newProjectData') newProjectData: UpdateProjectInput, @@ -966,7 +976,7 @@ export class ProjectResolver { ); for (const field in newProjectData) { - if (field === 'addresses') { + if (field === 'addresses' || field === 'socialMedia') { // We will take care of addresses and relations manually continue; } @@ -1027,6 +1037,11 @@ export class ProjectResolver { ); } const slugBase = creteSlugFromProject(newProjectData.title); + if (!slugBase) { + throw new Error( + i18n.__(translationErrorMessagesKeys.INVALID_PROJECT_TITLE), + ); + } const newSlug = await getAppropriateSlug(slugBase, projectId); if (project.slug !== newSlug && !project.slugHistory?.includes(newSlug)) { // it's just needed for editProject, we dont add current slug in slugHistory so it's not needed to do this in addProject @@ -1044,6 +1059,23 @@ export class ProjectResolver { await project.save(); await project.reload(); + if (!isSocialMediaEqual(project.socialMedia, newProjectData.socialMedia)) { + await removeProjectSocialMedia(projectId); + if (newProjectData.socialMedia && newProjectData.socialMedia.length > 0) { + const socialMediaEntities = newProjectData.socialMedia.map( + socialMediaInput => { + return { + type: socialMediaInput.type, + link: socialMediaInput.link, + projectId, + userId: user.userId, + }; + }, + ); + await addBulkProjectSocialMedia(socialMediaEntities); + } + } + const adminUser = (await findUserById(Number(project.admin))) as User; if (newProjectData.addresses) { await removeRecipientAddressOfProject({ project }); @@ -1078,12 +1110,12 @@ export class ProjectResolver { return project; } - @Mutation(returns => Project) + @Mutation(_returns => Project) async addRecipientAddressToProject( @Arg('projectId') projectId: number, @Arg('networkId') networkId: number, @Arg('address') address: string, - @Arg('chainType', type => ChainType, { defaultValue: ChainType.EVM }) + @Arg('chainType', _type => ChainType, { defaultValue: ChainType.EVM }) chainType: ChainType, @Ctx() { req: { user } }: ApolloContext, ) { @@ -1123,23 +1155,18 @@ export class ProjectResolver { return project; } - @Mutation(returns => ImageResponse) + @Mutation(_returns => ImageResponse) async uploadImage( @Arg('imageUpload') imageUpload: ImageUpload, - @Ctx() ctx: ApolloContext, + @Ctx() _ctx: ApolloContext, ): Promise { - const user = await getLoggedInUser(ctx); let url = ''; if (imageUpload.image) { - const { filename, createReadStream, encoding } = await imageUpload.image; + const { filename, createReadStream } = await imageUpload.image; try { - const pinResponse = await pinFile( - createReadStream(), - filename, - encoding, - ); + const pinResponse = await pinFile(createReadStream(), filename); url = `${process.env.PINATA_GATEWAY_ADDRESS}/ipfs/${pinResponse.IpfsHash}`; const projectImage = this.projectImageRepository.create({ @@ -1163,7 +1190,7 @@ export class ProjectResolver { throw Error(i18n.__(translationErrorMessagesKeys.UPLOAD_FAILED)); } - @Query(returns => ResourcePerDateRange, { nullable: true }) + @Query(_returns => ResourcePerDateRange, { nullable: true }) async projectsPerDate( // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss @Arg('fromDate', { nullable: true }) fromDate?: string, @@ -1202,7 +1229,7 @@ export class ProjectResolver { } } - @Mutation(returns => Project) + @Mutation(_returns => Project) async createProject( @Arg('project') projectInput: CreateProjectInput, @Ctx() ctx: ApolloContext, @@ -1255,6 +1282,11 @@ export class ProjectResolver { ); await validateProjectTitle(projectInput.title); const slugBase = creteSlugFromProject(projectInput.title); + if (!slugBase) { + throw new Error( + i18n.__(translationErrorMessagesKeys.INVALID_PROJECT_TITLE), + ); + } const slug = await getAppropriateSlug(slugBase); const status = await this.projectStatusRepository.findOne({ @@ -1314,6 +1346,21 @@ export class ProjectResolver { }); await project.save(); + + if (projectInput.socialMedia && projectInput.socialMedia.length > 0) { + const socialMediaEntities = projectInput.socialMedia.map( + socialMediaInput => { + return { + type: socialMediaInput.type, + link: socialMediaInput.link, + projectId: project.id, + userId: ctx.req.user.userId, + }; + }, + ); + await addBulkProjectSocialMedia(socialMediaEntities); + } + // const adminUser = (await findUserById(Number(newProject.admin))) as User; // newProject.adminUser = adminUser; await addBulkNewProjectAddress( @@ -1337,7 +1384,7 @@ export class ProjectResolver { projectId: project.id, }); - const update = await ProjectUpdate.create({ + const update = ProjectUpdate.create({ userId: ctx.req.user.userId, projectId: project.id, content: '', @@ -1360,7 +1407,7 @@ export class ProjectResolver { return project; } - @Mutation(returns => ProjectUpdate) + @Mutation(_returns => ProjectUpdate) async addProjectUpdate( @Arg('projectId') projectId: number, @Arg('title') title: string, @@ -1397,7 +1444,7 @@ export class ProjectResolver { i18n.__(translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT), ); - const update = await ProjectUpdate.create({ + const update = ProjectUpdate.create({ userId: user.userId, projectId: project.id, content, @@ -1406,15 +1453,11 @@ export class ProjectResolver { isMain: false, }); - const projectUpdateInfo = { - title: project.title, - email: owner.email, - slug: project.slug, - update: title, - projectId: project.id, - firstName: owner.firstName, - }; const save = await ProjectUpdate.save(update); + if (project.verificationStatus !== RevokeSteps.Revoked) { + project.verificationStatus = null; + await project.save(); + } await updateTotalProjectUpdatesOfAProject(update.projectId); @@ -1425,7 +1468,7 @@ export class ProjectResolver { return save; } - @Mutation(returns => ProjectUpdate) + @Mutation(_returns => ProjectUpdate) async editProjectUpdate( @Arg('updateId') updateId: number, @Arg('title') title: string, @@ -1469,11 +1512,11 @@ export class ProjectResolver { return update; } - @Mutation(returns => Boolean) + @Mutation(_returns => Boolean) async deleteProjectUpdate( @Arg('updateId') updateId: number, @Ctx() { req: { user } }: ApolloContext, - ): Promise { + ): Promise { if (!user) throw new Error( i18n.__(translationErrorMessagesKeys.AUTHENTICATION_REQUIRED), @@ -1504,14 +1547,14 @@ export class ProjectResolver { return true; } - @Query(returns => [ProjectUpdate]) + @Query(_returns => [ProjectUpdate]) async getProjectUpdates( - @Arg('projectId', type => Int) projectId: number, - @Arg('skip', type => Int, { defaultValue: 0 }) skip: number, - @Arg('take', type => Int, { defaultValue: 10 }) take: number, - @Arg('connectedWalletUserId', type => Int, { nullable: true }) + @Arg('projectId', _type => Int) projectId: number, + @Arg('skip', _type => Int, { defaultValue: 0 }) skip: number, + @Arg('take', _type => Int, { defaultValue: 10 }) take: number, + @Arg('connectedWalletUserId', _type => Int, { nullable: true }) connectedWalletUserId: number, - @Arg('orderBy', type => OrderBy, { + @Arg('orderBy', _type => OrderBy, { defaultValue: { field: OrderField.CreationAt, direction: OrderDirection.DESC, @@ -1545,8 +1588,8 @@ export class ProjectResolver { // TODO after finalizing getPurpleList and when Ashley filled that table we can remove this query and then change // givback-calculation script to use getPurpleList query - @Query(returns => [String]) - async getProjectsRecipients(): Promise { + @Query(_returns => [String]) + async getProjectsRecipients(): Promise { const recipients = await Project.query( ` SELECT "walletAddress" FROM project @@ -1556,20 +1599,20 @@ export class ProjectResolver { return recipients.map(({ walletAddress }) => walletAddress); } - @Query(returns => [String]) - async getPurpleList(): Promise { + @Query(_returns => [String]) + async getPurpleList(): Promise { const relatedAddresses = await getPurpleListAddresses(); return relatedAddresses.map(({ projectAddress }) => projectAddress); } - @Query(returns => Boolean) + @Query(_returns => Boolean) async walletAddressIsPurpleListed( @Arg('address') address: string, - ): Promise { + ): Promise { return isWalletAddressInPurpleList(address); } - @Query(returns => [Token]) + @Query(_returns => [Token]) async getProjectAcceptTokens( @Arg('projectId') projectId: number, ): Promise { @@ -1600,10 +1643,10 @@ export class ProjectResolver { } } - @Query(returns => [Reaction]) + @Query(_returns => [Reaction]) async getProjectReactions( @Arg('projectId') projectId: number, - @Ctx() { user }: Context, + @Ctx() { user: _user }: Context, ): Promise { const update = await ProjectUpdate.findOne({ where: { projectId, isMain: true }, @@ -1614,7 +1657,7 @@ export class ProjectResolver { }); } - // @Query(returns => Boolean) + // @Query(_returns => Boolean) // async isWalletSmartContract(@Arg('address') address: string) { // return isWalletAddressSmartContract(address); // } @@ -1624,7 +1667,7 @@ export class ProjectResolver { * @param address wallet address * @returns */ - @Query(returns => Boolean) + @Query(_returns => Boolean) async walletAddressIsValid(@Arg('address') address: string) { return validateProjectWalletAddress(address); } @@ -1635,7 +1678,7 @@ export class ProjectResolver { * @param projectId * @returns */ - @Query(returns => Boolean) + @Query(_returns => Boolean) async isValidTitleForProject( @Arg('title') title: string, @Arg('projectId', { nullable: true }) projectId?: number, @@ -1646,14 +1689,14 @@ export class ProjectResolver { return validateProjectTitle(title); } - @Query(returns => AllProjects, { nullable: true }) + @Query(_returns => AllProjects, { nullable: true }) async projectsByUserId( - @Arg('userId', type => Int) userId: number, + @Arg('userId', _type => Int) userId: number, @Arg('take', { defaultValue: 10 }) take: number, @Arg('skip', { defaultValue: 0 }) skip: number, - @Arg('connectedWalletUserId', type => Int, { nullable: true }) + @Arg('connectedWalletUserId', _type => Int, { nullable: true }) connectedWalletUserId: number, - @Arg('orderBy', type => OrderBy, { + @Arg('orderBy', _type => OrderBy, { defaultValue: { field: OrderField.CreationDate, direction: OrderDirection.DESC, @@ -1704,15 +1747,15 @@ export class ProjectResolver { }; } - @Query(returns => AllProjects, { nullable: true }) + @Query(_returns => AllProjects, { nullable: true }) async projectsBySlugs( // TODO Write test cases - @Arg('slugs', type => [String]) slugs: string[], + @Arg('slugs', _type => [String]) slugs: string[], @Arg('take', { defaultValue: 10 }) take: number, @Arg('skip', { defaultValue: 0 }) skip: number, - @Arg('connectedWalletUserId', type => Int, { nullable: true }) + @Arg('connectedWalletUserId', _type => Int, { nullable: true }) connectedWalletUserId: number, - @Arg('orderBy', type => OrderBy, { + @Arg('orderBy', _type => OrderBy, { defaultValue: { field: OrderField.CreationDate, direction: OrderDirection.DESC, @@ -1749,11 +1792,11 @@ export class ProjectResolver { }; } - @Query(returns => AllProjects, { nullable: true }) + @Query(_returns => AllProjects, { nullable: true }) async similarProjectsBySlug( - @Arg('slug', type => String, { nullable: false }) slug: string, - @Arg('take', type => Int, { defaultValue: 10 }) take: number, - @Arg('skip', type => Int, { defaultValue: 0 }) skip: number, + @Arg('slug', _type => String, { nullable: false }) slug: string, + @Arg('take', _type => Int, { defaultValue: 10 }) take: number, + @Arg('skip', _type => Int, { defaultValue: 0 }) skip: number, @Ctx() { req: { user } }: ApolloContext, ) { try { @@ -1834,10 +1877,10 @@ export class ProjectResolver { } } - @Query(returns => ProjectUpdatesResponse, { nullable: true }) + @Query(_returns => ProjectUpdatesResponse, { nullable: true }) async projectUpdates( - @Arg('take', type => Int, { defaultValue: 10 }) take: number, - @Arg('skip', type => Int, { defaultValue: 0 }) skip: number, + @Arg('take', _type => Int, { defaultValue: 10 }) take: number, + @Arg('skip', _type => Int, { defaultValue: 0 }) skip: number, @Ctx() { req: { user } }: ApolloContext, ): Promise { const latestProjectUpdates = await ProjectUpdate.query(` @@ -1878,11 +1921,11 @@ export class ProjectResolver { }; } - @Query(returns => AllProjects, { nullable: true }) + @Query(_returns => AllProjects, { nullable: true }) async likedProjectsByUserId( - @Arg('userId', type => Int, { nullable: false }) userId: number, - @Arg('take', type => Int, { defaultValue: 10 }) take: number, - @Arg('skip', type => Int, { defaultValue: 0 }) skip: number, + @Arg('userId', _type => Int, { nullable: false }) userId: number, + @Arg('take', _type => Int, { defaultValue: 10 }) take: number, + @Arg('skip', _type => Int, { defaultValue: 0 }) skip: number, @Ctx() { req: { user } }: ApolloContext, ) { let query = this.projectRepository @@ -1958,12 +2001,12 @@ export class ProjectResolver { return project; } - @Mutation(returns => Boolean) + @Mutation(_returns => Boolean) async deactivateProject( @Arg('projectId') projectId: number, @Ctx() ctx: ApolloContext, @Arg('reasonId', { nullable: true }) reasonId?: number, - ): Promise { + ): Promise { try { const user = await getLoggedInUser(ctx); const project = await this.updateProjectStatus({ @@ -1987,11 +2030,11 @@ export class ProjectResolver { throw error; } } - @Mutation(returns => Boolean) + @Mutation(_returns => Boolean) async activateProject( @Arg('projectId') projectId: number, @Ctx() ctx: ApolloContext, - ): Promise { + ): Promise { try { const user = await getLoggedInUser(ctx); const project = await this.updateProjectStatus({ diff --git a/src/resolvers/projectVerificationFormResolver.test.ts b/src/resolvers/projectVerificationFormResolver.test.ts index 97d44e53d..d2a1acb6b 100644 --- a/src/resolvers/projectVerificationFormResolver.test.ts +++ b/src/resolvers/projectVerificationFormResolver.test.ts @@ -29,7 +29,7 @@ import { ProjectRegistry, ProjectVerificationForm, } from '../entities/projectVerificationForm'; -import { Project, ProjStatus, ReviewStatus } from '../entities/project'; +import { ProjStatus, ReviewStatus } from '../entities/project'; import { createProjectVerificationForm, findProjectVerificationFormById, @@ -168,7 +168,6 @@ function createProjectVerificationFormMutationTestCases() { }); it('should not create project verification because project not found', async () => { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const projectId = Number(await Project.count()) + 3; const accessToken = await generateTestAccessToken(user.id); const result = await axios.post( graphqlUrl, @@ -294,7 +293,7 @@ function updateProjectVerificationFormMutationTestCases() { }, { address: generateRandomEtheriumAddress(), - networkId: NETWORK_IDS.OPTIMISM_GOERLI, + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, title: 'test title', }, { diff --git a/src/resolvers/projectVerificationFormResolver.ts b/src/resolvers/projectVerificationFormResolver.ts index e13431039..eee286f20 100644 --- a/src/resolvers/projectVerificationFormResolver.ts +++ b/src/resolvers/projectVerificationFormResolver.ts @@ -1,4 +1,6 @@ import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql'; +import * as jwt from 'jsonwebtoken'; +import moment from 'moment'; import { ApolloContext } from '../types/ApolloContext'; import { errorMessages, @@ -28,20 +30,15 @@ import { } from '../entities/projectVerificationForm'; import { updateProjectVerificationFormByUser } from '../services/projectVerificationFormService'; import { ProjectVerificationUpdateInput } from './types/ProjectVerificationUpdateInput'; -import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; -import * as jwt from 'jsonwebtoken'; import config from '../config'; import { countriesList } from '../utils/utils'; import { Country } from '../entities/Country'; import { sendMailConfirmationEmail } from '../services/mailerService'; -import moment from 'moment'; - -const dappUrl = process.env.FRONTEND_URL as string; -@Resolver(of => ProjectVerificationForm) +@Resolver(_of => ProjectVerificationForm) export class ProjectVerificationFormResolver { // https://github.com/Giveth/impact-graph/pull/519#issuecomment-1136845612 - @Mutation(returns => ProjectVerificationForm) + @Mutation(_returns => ProjectVerificationForm) async projectVerificationConfirmEmail( @Arg('emailConfirmationToken') emailConfirmationToken: string, ): Promise { @@ -114,7 +111,7 @@ export class ProjectVerificationFormResolver { } } - @Mutation(returns => ProjectVerificationForm) + @Mutation(_returns => ProjectVerificationForm) async projectVerificationSendEmailConfirmation( @Arg('projectVerificationFormId') projectVerificationFormId: number, @@ -194,7 +191,7 @@ export class ProjectVerificationFormResolver { } } - @Mutation(returns => ProjectVerificationForm) + @Mutation(_returns => ProjectVerificationForm) async createProjectVerificationForm( @Arg('slug') slug: string, @Ctx() { req: { user } }: ApolloContext, @@ -247,7 +244,7 @@ export class ProjectVerificationFormResolver { } } - @Mutation(returns => ProjectVerificationForm) + @Mutation(_returns => ProjectVerificationForm) async updateProjectVerificationForm( @Arg('projectVerificationUpdateInput') projectVerificationUpdateInput: ProjectVerificationUpdateInput, @@ -296,7 +293,7 @@ export class ProjectVerificationFormResolver { } } - @Query(returns => ProjectVerificationForm) + @Query(_returns => ProjectVerificationForm) async getCurrentProjectVerificationForm( @Arg('slug') slug: string, @Ctx() { req: { user } }: ApolloContext, @@ -341,7 +338,7 @@ export class ProjectVerificationFormResolver { } } - @Query(returns => [Country]) + @Query(_returns => [Country]) getAllowedCountries(): Country[] { return countriesList; } diff --git a/src/resolvers/qfRoundHistoryResolver.test.ts b/src/resolvers/qfRoundHistoryResolver.test.ts index eae47a428..d89e081cc 100644 --- a/src/resolvers/qfRoundHistoryResolver.test.ts +++ b/src/resolvers/qfRoundHistoryResolver.test.ts @@ -1,4 +1,6 @@ import { assert } from 'chai'; +import moment from 'moment'; +import axios from 'axios'; import { createDonationData, createProjectData, @@ -10,13 +12,8 @@ import { } from '../../test/testUtils'; import { Project } from '../entities/project'; import { QfRound } from '../entities/qfRound'; -import moment from 'moment'; import { fillQfRoundHistory } from '../repositories/qfRoundHistoryRepository'; -import axios from 'axios'; -import { - fetchProjectBySlugQuery, - getQfRoundHistoryQuery, -} from '../../test/graphqlQueries'; +import { getQfRoundHistoryQuery } from '../../test/graphqlQueries'; describe('Fetch getQfRoundHistory test cases', getQfRoundHistoryTestCases); diff --git a/src/resolvers/qfRoundHistoryResolver.ts b/src/resolvers/qfRoundHistoryResolver.ts index 9f1850e3c..9f8f3972b 100644 --- a/src/resolvers/qfRoundHistoryResolver.ts +++ b/src/resolvers/qfRoundHistoryResolver.ts @@ -3,7 +3,7 @@ import { Arg, Int, Query, Resolver } from 'type-graphql'; import { QfRoundHistory } from '../entities/qfRoundHistory'; import { getQfRoundHistory } from '../repositories/qfRoundHistoryRepository'; -@Resolver(of => QfRoundHistory) +@Resolver(_of => QfRoundHistory) export class QfRoundHistoryResolver { @Query(() => QfRoundHistory, { nullable: true }) async getQfRoundHistory( diff --git a/src/resolvers/qfRoundResolver.test.ts b/src/resolvers/qfRoundResolver.test.ts index 5976910da..572fd5ca9 100644 --- a/src/resolvers/qfRoundResolver.test.ts +++ b/src/resolvers/qfRoundResolver.test.ts @@ -1,4 +1,6 @@ import { assert, expect } from 'chai'; +import moment from 'moment'; +import axios from 'axios'; import { createDonationData, createProjectData, @@ -10,17 +12,11 @@ import { } from '../../test/testUtils'; import { Project } from '../entities/project'; import { QfRound } from '../entities/qfRound'; -import moment from 'moment'; import { refreshProjectDonationSummaryView, refreshProjectEstimatedMatchingView, } from '../services/projectViewsService'; -import { getProjectDonationsSqrtRootSum } from '../repositories/qfRoundRepository'; -import axios from 'axios'; -import { - fetchProjectBySlugQuery, - qfRoundStatsQuery, -} from '../../test/graphqlQueries'; +import { qfRoundStatsQuery } from '../../test/graphqlQueries'; import { generateRandomString } from '../utils/utils'; describe('Fetch estimatedMatching test cases', fetchEstimatedMatchingTestCases); diff --git a/src/resolvers/qfRoundResolver.ts b/src/resolvers/qfRoundResolver.ts index 0eb019786..322ca6ab5 100644 --- a/src/resolvers/qfRoundResolver.ts +++ b/src/resolvers/qfRoundResolver.ts @@ -1,5 +1,4 @@ import { Arg, Field, ObjectType, Query, Resolver } from 'type-graphql'; -import { Repository } from 'typeorm'; import { User } from '../entities/user'; import { @@ -35,9 +34,9 @@ export class ExpectedMatchingResponse { matchingPool: number; } -@Resolver(of => User) +@Resolver(_of => User) export class QfRoundResolver { - @Query(returns => [QfRound], { nullable: true }) + @Query(_returns => [QfRound], { nullable: true }) async qfRounds() { return findAllQfRounds(); } diff --git a/src/resolvers/reactionResolver.test.ts b/src/resolvers/reactionResolver.test.ts index 9b74bb25a..32a0195b4 100644 --- a/src/resolvers/reactionResolver.test.ts +++ b/src/resolvers/reactionResolver.test.ts @@ -1,17 +1,17 @@ +import axios from 'axios'; +import { assert } from 'chai'; import { generateTestAccessToken, graphqlUrl, PROJECT_UPDATE_SEED_DATA, SEED_DATA, } from '../../test/testUtils'; -import axios from 'axios'; import { likeProjectQuery, likeProjectUpdateQuery, unlikeProjectQuery, unlikeProjectUpdateQuery, } from '../../test/graphqlQueries'; -import { assert } from 'chai'; import { Project, ProjectUpdate } from '../entities/project'; import { Reaction } from '../entities/reaction'; diff --git a/src/resolvers/reactionResolver.ts b/src/resolvers/reactionResolver.ts index b38b89d66..ac5faa82f 100644 --- a/src/resolvers/reactionResolver.ts +++ b/src/resolvers/reactionResolver.ts @@ -1,13 +1,4 @@ -import { - Arg, - Ctx, - Field, - Int, - Mutation, - ObjectType, - Query, - Resolver, -} from 'type-graphql'; +import { Arg, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'; import { Reaction } from '../entities/reaction'; import { Context } from '../context'; import { Project, ProjectUpdate, ProjStatus } from '../entities/project'; @@ -18,30 +9,21 @@ import { getNotificationAdapter } from '../adapters/adaptersFactory'; import { findProjectById } from '../repositories/projectRepository'; import { AppDataSource } from '../orm'; -@ObjectType() -class ToggleResponse { - @Field(type => Boolean) - reaction: boolean; - - @Field(type => Number) - reactionCount: number; -} - -@Resolver(of => Reaction) +@Resolver(_of => Reaction) export class ReactionResolver { - @Query(returns => [Reaction]) + @Query(_returns => [Reaction]) async getProjectReactions( @Arg('projectId') projectId: number, - @Ctx() { user }: Context, + @Ctx() { user: _user }: Context, ): Promise { return await Reaction.find({ where: { projectId: projectId || -1 }, }); } - @Mutation(returns => Reaction) + @Mutation(_returns => Reaction) async likeProjectUpdate( - @Arg('projectUpdateId', type => Int) projectUpdateId: number, + @Arg('projectUpdateId', _type => Int) projectUpdateId: number, @Ctx() { req: { user } }: ApolloContext, ): Promise { if (!user || !user?.userId) @@ -102,9 +84,9 @@ export class ReactionResolver { } } - @Mutation(returns => Boolean) + @Mutation(_returns => Boolean) async unlikeProjectUpdate( - @Arg('reactionId', type => Int) reactionId: number, + @Arg('reactionId', _type => Int) reactionId: number, @Ctx() { req: { user } }: ApolloContext, ): Promise { @@ -163,9 +145,9 @@ export class ReactionResolver { } } - @Mutation(returns => Reaction) + @Mutation(_returns => Reaction) async likeProject( - @Arg('projectId', type => Int) projectId: number, + @Arg('projectId', _type => Int) projectId: number, @Ctx() { req: { user } }: ApolloContext, ): Promise { if (!user || !user?.userId) @@ -233,9 +215,9 @@ export class ReactionResolver { } } - @Mutation(returns => Boolean) + @Mutation(_returns => Boolean) async unlikeProject( - @Arg('reactionId', type => Int) reactionId: number, + @Arg('reactionId', _type => Int) reactionId: number, @Ctx() { req: { user } }: ApolloContext, ): Promise { diff --git a/src/resolvers/recurringDonationResolver.test.ts b/src/resolvers/recurringDonationResolver.test.ts index 04c05a064..fb263b010 100644 --- a/src/resolvers/recurringDonationResolver.test.ts +++ b/src/resolvers/recurringDonationResolver.test.ts @@ -1,33 +1,38 @@ +import moment from 'moment'; +import { assert } from 'chai'; +import axios from 'axios'; import { NETWORK_IDS } from '../provider'; import { - createDonationData, createProjectData, - DONATION_SEED_DATA, generateRandomEtheriumAddress, generateRandomEvmTxHash, generateTestAccessToken, graphqlUrl, - saveDonationDirectlyToDb, saveProjectDirectlyToDb, saveRecurringDonationDirectlyToDb, saveUserDirectlyToDb, - SEED_DATA, } from '../../test/testUtils'; -import { assert } from 'chai'; -import axios from 'axios'; import { createRecurringDonationQuery, fetchRecurringDonationsByProjectIdQuery, fetchRecurringDonationsByUserIdQuery, + updateRecurringDonationQuery, + updateRecurringDonationQueryById, updateRecurringDonationStatusMutation, } from '../../test/graphqlQueries'; +describe( + 'createRecurringDonation test cases', + createRecurringDonationTestCases, +); import { errorMessages } from '../utils/errorMessages'; import { addNewAnchorAddress } from '../repositories/anchorContractAddressRepository'; import { RECURRING_DONATION_STATUS } from '../entities/recurringDonation'; +import { QfRound } from '../entities/qfRound'; +import { generateRandomString } from '../utils/utils'; describe( - 'createRecurringDonation test cases', - createRecurringDonationTestCases, + 'updateRecurringDonation test cases', + updateRecurringDonationTestCases, ); describe( 'recurringDonationsByProjectId test cases', @@ -44,6 +49,11 @@ describe( updateRecurringDonationStatusTestCases, ); +describe( + 'updateRecurringDonationById test cases', + updateRecurringDonationByIdTestCases, +); + function createRecurringDonationTestCases() { it('should create recurringDonation successfully', async () => { const projectOwner = await saveUserDirectlyToDb( @@ -57,7 +67,105 @@ function createRecurringDonationTestCases() { generateRandomEtheriumAddress(), ); - const anchorContractAddress = await addNewAnchorAddress({ + await addNewAnchorAddress({ + project, + owner: projectOwner, + creator: contractCreator, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const accessToken = await generateTestAccessToken(donor.id); + const result = await axios.post( + graphqlUrl, + { + query: createRecurringDonationQuery, + variables: { + projectId: project.id, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + flowRate: '100', + currency: 'GIV', + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isNotNull(result.data.data.createRecurringDonation); + assert.equal( + result.data.data.createRecurringDonation.networkId, + NETWORK_IDS.OPTIMISTIC, + ); + assert.equal(result.data.data.createRecurringDonation.anonymous, false); + assert.equal(result.data.data.createRecurringDonation.isBatch, false); + }); + it('should create recurringDonation successfully with anonymous true', async () => { + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const contractCreator = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + + await addNewAnchorAddress({ + project, + owner: projectOwner, + creator: contractCreator, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const accessToken = await generateTestAccessToken(donor.id); + const result = await axios.post( + graphqlUrl, + { + query: createRecurringDonationQuery, + variables: { + projectId: project.id, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + flowRate: '100', + currency: 'GIV', + anonymous: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isNotNull(result.data.data.createRecurringDonation); + assert.equal( + result.data.data.createRecurringDonation.networkId, + NETWORK_IDS.OPTIMISTIC, + ); + assert.equal(result.data.data.createRecurringDonation.anonymous, true); + }); + it('should create recurringDonation successfully with isBatch true', async () => { + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const contractCreator = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + + await addNewAnchorAddress({ project, owner: projectOwner, creator: contractCreator, @@ -76,9 +184,9 @@ function createRecurringDonationTestCases() { projectId: project.id, networkId: NETWORK_IDS.OPTIMISTIC, txHash: generateRandomEvmTxHash(), - amount: 100, + flowRate: '100', currency: 'GIV', - interval: 'monthly', + isBatch: true, }, }, { @@ -92,6 +200,7 @@ function createRecurringDonationTestCases() { result.data.data.createRecurringDonation.networkId, NETWORK_IDS.OPTIMISTIC, ); + assert.equal(result.data.data.createRecurringDonation.isBatch, true); }); it('should return unAuthorized error when not sending JWT', async () => { @@ -104,7 +213,7 @@ function createRecurringDonationTestCases() { ); const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const anchorContractAddress = await addNewAnchorAddress({ + await addNewAnchorAddress({ project, owner: projectOwner, creator: donor, @@ -119,9 +228,8 @@ function createRecurringDonationTestCases() { projectId: project.id, networkId: NETWORK_IDS.OPTIMISTIC, txHash: generateRandomEvmTxHash(), - amount: 100, + flowRate: '100', currency: 'GIV', - interval: 'monthly', }, }); @@ -135,7 +243,6 @@ function createRecurringDonationTestCases() { ); const accessToken = await generateTestAccessToken(contractCreator.id); - const contractAddress = generateRandomEtheriumAddress(); const result = await axios.post( graphqlUrl, { @@ -144,9 +251,8 @@ function createRecurringDonationTestCases() { projectId: 99999, networkId: NETWORK_IDS.OPTIMISTIC, txHash: generateRandomEvmTxHash(), - amount: 100, + flowRate: '100', currency: 'GIV', - interval: 'monthly', }, }, { @@ -182,9 +288,8 @@ function createRecurringDonationTestCases() { projectId: project.id, networkId: NETWORK_IDS.OPTIMISTIC, txHash: generateRandomEvmTxHash(), - amount: 100, + flowRate: '100', currency: 'GIV', - interval: 'monthly', }, }, { @@ -202,350 +307,2353 @@ function createRecurringDonationTestCases() { }); } -function recurringDonationsByProjectIdTestCases() { - it('should sort by the createdAt DESC', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); - - await saveRecurringDonationDirectlyToDb({ +function updateRecurringDonationTestCases() { + it('should allow to end recurring donation when its active, and archive when its ended', async () => { + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + flowRate: '1000', + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency: 'GIV', + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: user, + creator: user, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const donation = await saveRecurringDonationDirectlyToDb({ donationData: { projectId: project.id, + currency: 'ETH', + networkId: NETWORK_IDS.OPTIMISTIC, + donorId: donor.id, + anchorContractAddressId: anchorContractAddress.id, + status: RECURRING_DONATION_STATUS.ACTIVE, }, }); - await saveRecurringDonationDirectlyToDb({ + + assert.equal(donation.status, RECURRING_DONATION_STATUS.ACTIVE); + + const accessToken = await generateTestAccessToken(donor.id); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQuery, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + networkId: donation.networkId, + currency: donation.currency, + status: RECURRING_DONATION_STATUS.ENDED, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParams.status, + RECURRING_DONATION_STATUS.ENDED, + ); + + const archivingResult = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQuery, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + networkId: donation.networkId, + currency: donation.currency, + isArchived: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + assert.equal( + archivingResult.data.data.updateRecurringDonationParams.isArchived, + true, + ); + }); + it('should not allow to archive recurring donation when its not ended', async () => { + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + flowRate: '1000', + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency: 'GIV', + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: user, + creator: user, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const donation = await saveRecurringDonationDirectlyToDb({ donationData: { projectId: project.id, + currency: 'ETH', + networkId: NETWORK_IDS.OPTIMISTIC, + donorId: donor.id, + anchorContractAddressId: anchorContractAddress.id, + status: RECURRING_DONATION_STATUS.ACTIVE, }, }); + assert.equal(donation.status, RECURRING_DONATION_STATUS.ACTIVE); + + const accessToken = await generateTestAccessToken(donor.id); const result = await axios.post( graphqlUrl, { - query: fetchRecurringDonationsByProjectIdQuery, + query: updateRecurringDonationQuery, variables: { + recurringDonationId: donation.id, projectId: project.id, - orderBy: { - field: 'createdAt', - direction: 'DESC', - }, + networkId: donation.networkId, + currency: donation.currency, + isArchived: true, }, }, - {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParams.isArchived, + false, ); + }); + it('should not change isArchived when its already true and we dont send it', async () => { + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + flowRate: '1000', + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency: 'GIV', + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: user, + creator: user, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + currency: 'ETH', + networkId: NETWORK_IDS.OPTIMISTIC, + donorId: donor.id, + anchorContractAddressId: anchorContractAddress.id, + status: RECURRING_DONATION_STATUS.ENDED, + isArchived: true, + }, + }); + assert.equal(donation.isArchived, true); - const donations = - result.data.data.recurringDonationsByProjectId.recurringDonations; - assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 2); - for (let i = 0; i < donations.length - 1; i++) { - assert.isTrue(donations[i].createdAt >= donations[i + 1].createdAt); - } + const accessToken = await generateTestAccessToken(donor.id); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQuery, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + networkId: donation.networkId, + currency: donation.currency, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParams.isArchived, + true, + ); }); - it('should sort by the createdAt ASC', async () => { + it('should not change isArchived when its already false and we dont send it', async () => { + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + flowRate: '1000', + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency: 'GIV', + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: user, + creator: user, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + currency: 'ETH', + networkId: NETWORK_IDS.OPTIMISTIC, + donorId: donor.id, + anchorContractAddressId: anchorContractAddress.id, + status: RECURRING_DONATION_STATUS.ENDED, + isArchived: false, + }, + }); + assert.equal(donation.isArchived, false); + + const accessToken = await generateTestAccessToken(donor.id); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQuery, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + networkId: donation.networkId, + currency: donation.currency, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParams.isArchived, + false, + ); + }); + + it('should update recurring donation successfully', async () => { + const currency = 'GIV'; + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + amount: 1, + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency, + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: donor, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency, + }, + }); + + assert.equal(donation.flowRate, '300'); + + const accessToken = await generateTestAccessToken(donor.id); + const flowRate = '201'; + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQuery, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + flowRate, + currency, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + anonymous: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParams.flowRate, + flowRate, + ); + assert.isTrue(result.data.data.updateRecurringDonationParams.anonymous); + }); + it('should update recurring donation successfully for project that is related to a QF', async () => { + const currency = 'GIV'; + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + amount: 1, + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency, + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + await QfRound.update({}, { isActive: false }); + const qfRound = QfRound.create({ + isActive: true, + name: 'test', + slug: generateRandomString(10), + allocatedFund: 100, + minimumPassportScore: 8, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }); + await qfRound.save(); + project.qfRounds = [qfRound]; + await project.save(); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: donor, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency, + }, + }); + + assert.equal(donation.flowRate, '300'); + + const accessToken = await generateTestAccessToken(donor.id); + const flowRate = '201'; + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQuery, + variables: { + projectId: project.id, + flowRate, + currency, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + anonymous: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParams.flowRate, + flowRate, + ); + assert.isTrue(result.data.data.updateRecurringDonationParams.anonymous); + qfRound.isActive = false; + await qfRound.save(); + }); + it('should change status and isFinished when updating flowRate and txHash ', async () => { + const currency = 'GIV'; + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + amount: 1, + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency, + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: donor, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency, + finished: true, + }, + }); + + assert.equal(donation.flowRate, '300'); + + const accessToken = await generateTestAccessToken(donor.id); + const flowRate = '201'; + const txHash = generateRandomEvmTxHash(); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQuery, + variables: { + projectId: project.id, + flowRate, + currency, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash, + anonymous: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParams.flowRate, + flowRate, + ); + assert.isFalse(result.data.data.updateRecurringDonationParams.finished); + assert.equal( + result.data.data.updateRecurringDonationParams.status, + RECURRING_DONATION_STATUS.PENDING, + ); + assert.equal(result.data.data.updateRecurringDonationParams.txHash, txHash); + assert.equal( + result.data.data.updateRecurringDonationParams.flowRate, + flowRate, + ); + }); + it('should not change txHash when flowRate has not sent ', async () => { + const currency = 'GIV'; + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + amount: 1, + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency, + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: donor, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency, + finished: true, + status: RECURRING_DONATION_STATUS.ACTIVE, + }, + }); + + assert.equal(donation.flowRate, '300'); + + const accessToken = await generateTestAccessToken(donor.id); + const txHash = generateRandomEvmTxHash(); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQuery, + variables: { + projectId: project.id, + currency, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash, + anonymous: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + assert.equal( + result.data.data.updateRecurringDonationParams.status, + RECURRING_DONATION_STATUS.ACTIVE, + ); + assert.notEqual( + result.data.data.updateRecurringDonationParams.txHash, + txHash, + ); + }); + it('should not change flowRate when txHash has not sent ', async () => { + const currency = 'GIV'; + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + amount: 1, + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency, + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: donor, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency, + finished: true, + status: RECURRING_DONATION_STATUS.ACTIVE, + }, + }); + + assert.equal(donation.flowRate, '300'); + + const accessToken = await generateTestAccessToken(donor.id); + const flowRate = '201'; + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQuery, + variables: { + projectId: project.id, + flowRate, + currency, + networkId: NETWORK_IDS.OPTIMISTIC, + anonymous: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.notEqual( + result.data.data.updateRecurringDonationParams.flowRate, + flowRate, + ); + assert.equal( + result.data.data.updateRecurringDonationParams.status, + RECURRING_DONATION_STATUS.ACTIVE, + ); + }); + + it('should get error when someone wants to update someone else recurring donation', async () => { + const currency = 'GIV'; + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + amount: 1, + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency, + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + projectId: project.id, + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + }, + }); + assert.equal(donation.status, RECURRING_DONATION_STATUS.PENDING); + const anotherUser = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const accessToken = await generateTestAccessToken(anotherUser.id); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQuery, + variables: { + projectId: project.id, + flowRate: '10', + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + anonymous: false, + currency, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.errors[0].message, + errorMessages.RECURRING_DONATION_NOT_FOUND, + ); + }); + + it('should return unAuthorized error when not sending JWT', async () => { + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await addNewAnchorAddress({ + project, + owner: projectOwner, + creator: donor, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const result = await axios.post(graphqlUrl, { + query: updateRecurringDonationQuery, + variables: { + projectId: project.id, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + flowRate: '100', + anonymous: true, + currency: 'GIV', + }, + }); + + assert.isNull(result.data.data.updateRecurringDonationParams); + assert.equal(result.data.errors[0].message, errorMessages.UN_AUTHORIZED); + }); + + it('should return unAuthorized error when project not found', async () => { + const contractCreator = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + + const accessToken = await generateTestAccessToken(contractCreator.id); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQuery, + variables: { + projectId: 99999, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + flowRate: '100', + anonymous: true, + currency: 'GIV', + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isNull(result.data.data.updateRecurringDonationParams); + assert.equal( + result.data.errors[0].message, + errorMessages.PROJECT_NOT_FOUND, + ); + }); +} + +function updateRecurringDonationByIdTestCases() { + it('should allow to end recurring donation when its active, and archive when its ended', async () => { + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + flowRate: '1000', + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency: 'GIV', + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: user, + creator: user, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + currency: 'ETH', + networkId: NETWORK_IDS.OPTIMISTIC, + donorId: donor.id, + anchorContractAddressId: anchorContractAddress.id, + status: RECURRING_DONATION_STATUS.ACTIVE, + }, + }); + + assert.equal(donation.status, RECURRING_DONATION_STATUS.ACTIVE); + + const accessToken = await generateTestAccessToken(donor.id); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQueryById, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + networkId: donation.networkId, + currency: donation.currency, + status: RECURRING_DONATION_STATUS.ENDED, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParamsById.status, + RECURRING_DONATION_STATUS.ENDED, + ); + + const archivingResult = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQueryById, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + networkId: donation.networkId, + currency: donation.currency, + isArchived: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + assert.equal( + archivingResult.data.data.updateRecurringDonationParamsById.isArchived, + true, + ); + }); + it('should not allow to archive recurring donation when its not ended', async () => { + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + flowRate: '1000', + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency: 'GIV', + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: user, + creator: user, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + currency: 'ETH', + networkId: NETWORK_IDS.OPTIMISTIC, + donorId: donor.id, + anchorContractAddressId: anchorContractAddress.id, + status: RECURRING_DONATION_STATUS.ACTIVE, + }, + }); + + assert.equal(donation.status, RECURRING_DONATION_STATUS.ACTIVE); + + const accessToken = await generateTestAccessToken(donor.id); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQueryById, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + networkId: donation.networkId, + currency: donation.currency, + isArchived: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParamsById.isArchived, + false, + ); + }); + it('should not change isArchived when its already true and we dont send it', async () => { + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + flowRate: '1000', + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency: 'GIV', + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: user, + creator: user, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + currency: 'ETH', + networkId: NETWORK_IDS.OPTIMISTIC, + donorId: donor.id, + anchorContractAddressId: anchorContractAddress.id, + status: RECURRING_DONATION_STATUS.ENDED, + isArchived: true, + }, + }); + assert.equal(donation.isArchived, true); + + const accessToken = await generateTestAccessToken(donor.id); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQueryById, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + networkId: donation.networkId, + currency: donation.currency, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParamsById.isArchived, + true, + ); + }); + it('should not change isArchived when its already false and we dont send it', async () => { + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + flowRate: '1000', + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency: 'GIV', + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: user, + creator: user, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + currency: 'ETH', + networkId: NETWORK_IDS.OPTIMISTIC, + donorId: donor.id, + anchorContractAddressId: anchorContractAddress.id, + status: RECURRING_DONATION_STATUS.ENDED, + isArchived: false, + }, + }); + assert.equal(donation.isArchived, false); + + const accessToken = await generateTestAccessToken(donor.id); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQueryById, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + networkId: donation.networkId, + currency: donation.currency, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParamsById.isArchived, + false, + ); + }); + + it('should update recurring donation successfully', async () => { + const currency = 'GIV'; + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + amount: 1, + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency, + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: donor, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency, + }, + }); + + assert.equal(donation.flowRate, '300'); + + const accessToken = await generateTestAccessToken(donor.id); + const flowRate = '201'; + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQueryById, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + flowRate, + currency, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + anonymous: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParamsById.flowRate, + flowRate, + ); + assert.isTrue(result.data.data.updateRecurringDonationParamsById.anonymous); + }); + it('should change status and isFinished when updating flowRate and txHash ', async () => { + const currency = 'GIV'; + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + amount: 1, + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency, + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: donor, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency, + finished: true, + }, + }); + + assert.equal(donation.flowRate, '300'); + + const accessToken = await generateTestAccessToken(donor.id); + const flowRate = '201'; + const txHash = generateRandomEvmTxHash(); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQueryById, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + flowRate, + currency, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash, + anonymous: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.updateRecurringDonationParamsById.flowRate, + flowRate, + ); + assert.isFalse(result.data.data.updateRecurringDonationParamsById.finished); + assert.equal( + result.data.data.updateRecurringDonationParamsById.status, + RECURRING_DONATION_STATUS.PENDING, + ); + assert.equal( + result.data.data.updateRecurringDonationParamsById.txHash, + txHash, + ); + assert.equal( + result.data.data.updateRecurringDonationParamsById.flowRate, + flowRate, + ); + }); + it('should not change txHash when flowRate has not sent ', async () => { + const currency = 'GIV'; + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + amount: 1, + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency, + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: donor, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency, + finished: true, + status: RECURRING_DONATION_STATUS.ACTIVE, + }, + }); + + assert.equal(donation.flowRate, '300'); + + const accessToken = await generateTestAccessToken(donor.id); + const txHash = generateRandomEvmTxHash(); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQueryById, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + currency, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash, + anonymous: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + assert.equal( + result.data.data.updateRecurringDonationParamsById.status, + RECURRING_DONATION_STATUS.ACTIVE, + ); + assert.notEqual( + result.data.data.updateRecurringDonationParamsById.txHash, + txHash, + ); + }); + it('should not change flowRate when txHash has not sent ', async () => { + const currency = 'GIV'; + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + amount: 1, + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency, + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: donor, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency, + finished: true, + status: RECURRING_DONATION_STATUS.ACTIVE, + }, + }); + + assert.equal(donation.flowRate, '300'); + + const accessToken = await generateTestAccessToken(donor.id); + const flowRate = '201'; + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQueryById, + variables: { + recurringDonationId: donation.id, + projectId: project.id, + flowRate, + currency, + networkId: NETWORK_IDS.OPTIMISTIC, + anonymous: true, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.notEqual( + result.data.data.updateRecurringDonationParamsById.flowRate, + flowRate, + ); + assert.equal( + result.data.data.updateRecurringDonationParamsById.status, + RECURRING_DONATION_STATUS.ACTIVE, + ); + }); + + it('should get error when someone wants to update someone else recurring donation', async () => { + const currency = 'GIV'; + const transactionInfo = { + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.XDAI, + amount: 1, + fromAddress: generateRandomEtheriumAddress(), + toAddress: generateRandomEtheriumAddress(), + currency, + timestamp: 1647069070, + }; + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const donation = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + projectId: project.id, + }, + }); + const recurringDonation = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + }, + }); + assert.equal(donation.status, RECURRING_DONATION_STATUS.PENDING); + const anotherUser = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const accessToken = await generateTestAccessToken(anotherUser.id); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQueryById, + variables: { + recurringDonationId: recurringDonation.id, + projectId: project.id, + flowRate: '10', + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + anonymous: false, + currency, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.errors[0].message, + errorMessages.RECURRING_DONATION_NOT_FOUND, + ); + }); + + it('should return unAuthorized error when not sending JWT', async () => { + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await addNewAnchorAddress({ + project, + owner: projectOwner, + creator: donor, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const result = await axios.post(graphqlUrl, { + query: updateRecurringDonationQueryById, + variables: { + recurringDonationId: 1, + projectId: project.id, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + flowRate: '100', + anonymous: true, + currency: 'GIV', + }, + }); + + assert.isNull(result.data.data.updateRecurringDonationParamsById); + assert.equal(result.data.errors[0].message, errorMessages.UN_AUTHORIZED); + }); + + it('should return unAuthorized error when project not found', async () => { + const contractCreator = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + + const accessToken = await generateTestAccessToken(contractCreator.id); + const result = await axios.post( + graphqlUrl, + { + query: updateRecurringDonationQueryById, + variables: { + recurringDonationId: 1, + projectId: 99999, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + flowRate: '100', + anonymous: true, + currency: 'GIV', + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isNull(result.data.data.updateRecurringDonationParamsById); + assert.equal( + result.data.errors[0].message, + errorMessages.PROJECT_NOT_FOUND, + ); + }); +} + +function recurringDonationsByProjectIdTestCases() { + it('should sort by the createdAt DESC', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByProjectIdQuery, + variables: { + projectId: project.id, + orderBy: { + field: 'createdAt', + direction: 'DESC', + }, + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByProjectId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 2); + for (let i = 0; i < donations.length - 1; i++) { + assert.isTrue(donations[i].createdAt >= donations[i + 1].createdAt); + } + }); + it('should sort by the createdAt ASC', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByProjectIdQuery, + variables: { + projectId: project.id, + orderBy: { + field: 'createdAt', + direction: 'ASC', + }, + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByProjectId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 2); + for (let i = 0; i < donations.length - 1; i++) { + assert.isTrue(donations[i].createdAt <= donations[i + 1].createdAt); + } + }); + it('should sort by the flowRate ASC', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + flowRate: '1000', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + flowRate: '2000', + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByProjectIdQuery, + variables: { + projectId: project.id, + orderBy: { + field: 'flowRate', + direction: 'ASC', + }, + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByProjectId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 2); + for (let i = 0; i < donations.length - 1; i++) { + // assertion flor big int of flowRates + assert.isTrue( + BigInt(donations[i].flowRate) <= BigInt(donations[i + 1].flowRate), + ); + } + }); + it('should sort by the flowRate DESC', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + flowRate: '100', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + flowRate: '2000', + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByProjectIdQuery, + variables: { + projectId: project.id, + orderBy: { + field: 'flowRate', + direction: 'DESC', + }, + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByProjectId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 2); + for (let i = 0; i < donations.length - 1; i++) { + assert.isTrue( + BigInt(donations[i].flowRate) >= BigInt(donations[i + 1].flowRate), + ); + } + }); + it('should search by the currency', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + currency: 'USDT', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + currency: 'GIV', + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByProjectIdQuery, + variables: { + projectId: project.id, + searchTerm: 'GIV', + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByProjectId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 1); + assert.equal(donations[0].currency, 'GIV'); + }); + it('should search by the flowRate', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + flowRate: '100', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + flowRate: '200', + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByProjectIdQuery, + variables: { + projectId: project.id, + searchTerm: '100', + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByProjectId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 1); + assert.equal(donations[0].flowRate, '100'); + }); + it('should search by the failed status', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'failed', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'verified', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'pending', + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByProjectIdQuery, + variables: { + projectId: project.id, + status: 'failed', + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByProjectId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 1); + assert.equal(donations[0].status, 'failed'); + }); + it('should search by the pending status', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'failed', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'verified', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'pending', + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByProjectIdQuery, + variables: { + projectId: project.id, + status: 'pending', + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByProjectId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 1); + assert.equal(donations[0].status, 'pending'); + }); + it('should search by the verified status', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'failed', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'verified', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'pending', + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByProjectIdQuery, + variables: { + projectId: project.id, + status: 'verified', + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByProjectId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 1); + assert.equal(donations[0].status, 'verified'); + }); + it('should filter include archived ones when passing includeArchived:true', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + + const recurringDonation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'verified', + }, + }); + + const archivedRecurringDonation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'verified', + isArchived: true, + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByProjectIdQuery, + variables: { + projectId: project.id, + includeArchived: true, + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByProjectId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 2); + assert.isOk( + donations.find(d => Number(d.id) === archivedRecurringDonation.id), + ); + assert.isOk(donations.find(d => Number(d.id) === recurringDonation.id)); + }); + it('should not include archived ones when passing includeArchived:false', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'verified', + }, + }); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'verified', + isArchived: true, + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByProjectIdQuery, + variables: { + projectId: project.id, + includeArchived: false, + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByProjectId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 1); + assert.equal(donations[0].isArchived, false); + }); + it('should return non archived if we dont send includeArchived field', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); - await saveRecurringDonationDirectlyToDb({ + const recurringDonation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'verified', + isArchived: false, + }, + }); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + status: 'verified', + isArchived: true, + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByProjectIdQuery, + variables: { + projectId: project.id, + orderBy: { + field: 'createdAt', + direction: 'DESC', + }, + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByProjectId.recurringDonations; + + assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 1); + assert.isOk(donations.find(d => Number(d.id) === recurringDonation.id)); + }); +} + +function recurringDonationsByUserIdTestCases() { + it('should sort by the createdAt DESC', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByUserIdQuery, + variables: { + userId: donor.id, + orderBy: { + field: 'createdAt', + direction: 'DESC', + }, + }, + }, + {}, + ); + const donations = + result.data.data.recurringDonationsByUserId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 2); + for (let i = 0; i < donations.length - 1; i++) { + assert.isTrue(donations[i].createdAt >= donations[i + 1].createdAt); + } + }); + it('should sort by the createdAt ASC', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByUserIdQuery, + variables: { + userId: donor.id, + orderBy: { + field: 'createdAt', + direction: 'ASC', + }, + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByUserId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 2); + for (let i = 0; i < donations.length - 1; i++) { + assert.isTrue(donations[i].createdAt <= donations[i + 1].createdAt); + } + }); + it('should sort by the flowRate ASC', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + flowRate: '100', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + flowRate: '200', + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByUserIdQuery, + variables: { + userId: donor.id, + orderBy: { + field: 'flowRate', + direction: 'ASC', + }, + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByUserId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 2); + for (let i = 0; i < donations.length - 1; i++) { + assert.isTrue( + BigInt(donations[i].flowRate) <= BigInt(donations[i + 1].flowRate), + ); + } + }); + it('should sort by the flowRate DESC', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + flowRate: '100', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + flowRate: '200', + }, + }); + + const result = await axios.post( + graphqlUrl, + { + query: fetchRecurringDonationsByUserIdQuery, + variables: { + userId: donor.id, + orderBy: { + field: 'flowRate', + direction: 'DESC', + }, + }, + }, + {}, + ); + + const donations = + result.data.data.recurringDonationsByUserId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 2); + for (let i = 0; i < donations.length - 1; i++) { + assert.isTrue( + BigInt(donations[i].flowRate) >= BigInt(donations[i + 1].flowRate), + ); + } + }); + it('should filter by two tokens', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const d1 = await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, + donorId: donor.id, + currency: 'DAI', + }, + }); + const d2 = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + currency: 'USDC', }, }); await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, + donorId: donor.id, + currency: 'USDT', }, }); const result = await axios.post( graphqlUrl, { - query: fetchRecurringDonationsByProjectIdQuery, + query: fetchRecurringDonationsByUserIdQuery, variables: { - projectId: project.id, - orderBy: { - field: 'createdAt', - direction: 'ASC', - }, + userId: donor.id, + filteredTokens: ['DAI', 'USDC'], }, }, {}, ); - const donations = - result.data.data.recurringDonationsByProjectId.recurringDonations; - assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 2); - for (let i = 0; i < donations.length - 1; i++) { - assert.isTrue(donations[i].createdAt <= donations[i + 1].createdAt); - } + result.data.data.recurringDonationsByUserId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 2); + assert.isOk(donations.find(d => Number(d.id) === d1.id)); + assert.isOk(donations.find(d => Number(d.id) === d2.id)); }); - it('should sort by the amount ASC', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); + it('should filter by one token', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const d1 = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + currency: 'DAI', + }, + }); await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, - amount: 100, + donorId: donor.id, + currency: 'USDT', }, }); await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, - amount: 200, + donorId: donor.id, + currency: 'USDT', }, }); const result = await axios.post( graphqlUrl, { - query: fetchRecurringDonationsByProjectIdQuery, + query: fetchRecurringDonationsByUserIdQuery, variables: { - projectId: project.id, - orderBy: { - field: 'amount', - direction: 'ASC', - }, + userId: donor.id, + filteredTokens: ['DAI'], }, }, {}, ); - const donations = - result.data.data.recurringDonationsByProjectId.recurringDonations; - assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 2); - for (let i = 0; i < donations.length - 1; i++) { - assert.isTrue(donations[i].amount <= donations[i + 1].amount); - } + result.data.data.recurringDonationsByUserId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 1); + assert.isOk(donations.find(d => Number(d.id) === d1.id)); }); - it('should sort by the amount DESC', async () => { + it('should join with anchor contracts', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb(createProjectData()); - - await saveRecurringDonationDirectlyToDb({ - donationData: { - projectId: project.id, - amount: 100, - }, + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator: projectOwner, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), }); + await saveRecurringDonationDirectlyToDb({ donationData: { + donorId: donor.id, projectId: project.id, - amount: 200, + currency: 'DAI', }, }); const result = await axios.post( graphqlUrl, { - query: fetchRecurringDonationsByProjectIdQuery, + query: fetchRecurringDonationsByUserIdQuery, variables: { - projectId: project.id, - orderBy: { - field: 'amount', - direction: 'DESC', - }, + userId: donor.id, }, }, {}, ); - const donations = - result.data.data.recurringDonationsByProjectId.recurringDonations; - assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 2); - for (let i = 0; i < donations.length - 1; i++) { - assert.isTrue(donations[i].amount >= donations[i + 1].amount); - } + result.data.data.recurringDonationsByUserId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 1); + assert.equal( + donations[0].project.anchorContracts[0].id, + anchorContractAddress.id, + ); }); - it('should search by the currency', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); + it('should not filter if filteredTokens is not passing', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const d1 = await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + currency: 'DAI', + }, + }); await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, - currency: 'USDT', + donorId: donor.id, + currency: 'USDC', }, }); await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, - currency: 'GIV', + donorId: donor.id, + currency: 'USDT', }, }); const result = await axios.post( graphqlUrl, { - query: fetchRecurringDonationsByProjectIdQuery, + query: fetchRecurringDonationsByUserIdQuery, variables: { - projectId: project.id, - searchTerm: 'GIV', + userId: donor.id, }, }, {}, ); - const donations = - result.data.data.recurringDonationsByProjectId.recurringDonations; - assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 1); - assert.equal(donations[0].currency, 'GIV'); + result.data.data.recurringDonationsByUserId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 3); + assert.isOk(donations.find(d => Number(d.id) === d1.id)); }); - it('should search by the amount', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); + it('should filter by finishStatus filter both active and ended', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - await saveRecurringDonationDirectlyToDb({ + const d1 = await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, - amount: 100, + donorId: donor.id, + finished: true, }, }); - await saveRecurringDonationDirectlyToDb({ + const d2 = await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, - amount: 200, + donorId: donor.id, + finished: false, }, }); const result = await axios.post( graphqlUrl, { - query: fetchRecurringDonationsByProjectIdQuery, + query: fetchRecurringDonationsByUserIdQuery, variables: { - projectId: project.id, - searchTerm: '100', + userId: donor.id, + finishStatus: { active: true, ended: true }, }, }, {}, ); const donations = - result.data.data.recurringDonationsByProjectId.recurringDonations; - assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 1); - assert.equal(donations[0].amount, '100'); + result.data.data.recurringDonationsByUserId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 2); + assert.isOk(donations.find(d => Number(d.id) === d1.id)); + assert.isOk(donations.find(d => Number(d.id) === d2.id)); }); - it('should search by the failed status', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); + it('should return just not finished recurring donations when not passing finishStatus', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - await saveRecurringDonationDirectlyToDb({ + const d1 = await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, - status: 'failed', - }, - }); - await saveRecurringDonationDirectlyToDb({ - donationData: { - projectId: project.id, - status: 'verified', + donorId: donor.id, + finished: true, }, }); - await saveRecurringDonationDirectlyToDb({ + const d2 = await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, - status: 'pending', + donorId: donor.id, + finished: false, }, }); const result = await axios.post( graphqlUrl, { - query: fetchRecurringDonationsByProjectIdQuery, + query: fetchRecurringDonationsByUserIdQuery, variables: { - projectId: project.id, - status: 'failed', + userId: donor.id, }, }, {}, ); const donations = - result.data.data.recurringDonationsByProjectId.recurringDonations; - assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 1); - assert.equal(donations[0].status, 'failed'); + result.data.data.recurringDonationsByUserId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 1); + assert.isNotOk(donations.find(d => Number(d.id) === d1.id)); + assert.isOk(donations.find(d => Number(d.id) === d2.id)); }); - it('should search by the pending status', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); + it('should filter by finishStatus filter just active', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - await saveRecurringDonationDirectlyToDb({ - donationData: { - projectId: project.id, - status: 'failed', - }, - }); - await saveRecurringDonationDirectlyToDb({ + const endedOne = await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, - status: 'verified', + donorId: donor.id, + finished: true, }, }); - await saveRecurringDonationDirectlyToDb({ + const activeOne = await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, - status: 'pending', + donorId: donor.id, + finished: false, }, }); const result = await axios.post( graphqlUrl, { - query: fetchRecurringDonationsByProjectIdQuery, + query: fetchRecurringDonationsByUserIdQuery, variables: { - projectId: project.id, - status: 'pending', + userId: donor.id, + finishStatus: { active: true, ended: false }, }, }, {}, ); const donations = - result.data.data.recurringDonationsByProjectId.recurringDonations; - assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 1); - assert.equal(donations[0].status, 'pending'); + result.data.data.recurringDonationsByUserId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 1); + assert.isOk(donations.find(d => Number(d.id) === activeOne.id)); + assert.isNotOk(donations.find(d => Number(d.id) === endedOne.id)); }); - it('should search by the verified status', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); + it('should filter by finishStatus filter just ended', async () => { + await saveProjectDirectlyToDb(createProjectData()); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - await saveRecurringDonationDirectlyToDb({ - donationData: { - projectId: project.id, - status: 'failed', - }, - }); - await saveRecurringDonationDirectlyToDb({ + const endedOne = await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, - status: 'verified', + donorId: donor.id, + finished: true, }, }); - await saveRecurringDonationDirectlyToDb({ + const activeOne = await saveRecurringDonationDirectlyToDb({ donationData: { - projectId: project.id, - status: 'pending', + donorId: donor.id, + finished: false, }, }); const result = await axios.post( graphqlUrl, { - query: fetchRecurringDonationsByProjectIdQuery, + query: fetchRecurringDonationsByUserIdQuery, variables: { - projectId: project.id, - status: 'verified', + userId: donor.id, + finishStatus: { active: false, ended: true }, }, }, {}, ); const donations = - result.data.data.recurringDonationsByProjectId.recurringDonations; - assert.equal(result.data.data.recurringDonationsByProjectId.totalCount, 1); - assert.equal(donations[0].status, 'verified'); + result.data.data.recurringDonationsByUserId.recurringDonations; + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 1); + assert.isNotOk(donations.find(d => Number(d.id) === activeOne.id)); + assert.isOk(donations.find(d => Number(d.id) === endedOne.id)); }); -} -function recurringDonationsByUserIdTestCases() { - it('should sort by the createdAt DESC', async () => { + it('should filter by status and return active recurring donations', async () => { + await saveProjectDirectlyToDb(createProjectData()); const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - await saveRecurringDonationDirectlyToDb({ + + const activeDonation = await saveRecurringDonationDirectlyToDb({ donationData: { donorId: donor.id, + status: RECURRING_DONATION_STATUS.ACTIVE, }, }); - await saveRecurringDonationDirectlyToDb({ + const pendingDonation = await saveRecurringDonationDirectlyToDb({ donationData: { donorId: donor.id, + status: RECURRING_DONATION_STATUS.PENDING, }, }); @@ -555,33 +2663,34 @@ function recurringDonationsByUserIdTestCases() { query: fetchRecurringDonationsByUserIdQuery, variables: { userId: donor.id, - orderBy: { - field: 'createdAt', - direction: 'DESC', - }, + status: RECURRING_DONATION_STATUS.ACTIVE, }, }, {}, ); + const donations = result.data.data.recurringDonationsByUserId.recurringDonations; - assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 2); - for (let i = 0; i < donations.length - 1; i++) { - assert.isTrue(donations[i].createdAt >= donations[i + 1].createdAt); - } + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 1); + assert.isOk(donations.find(d => Number(d.id) === activeDonation.id)); + assert.isNotOk(donations.find(d => Number(d.id) === pendingDonation.id)); }); - it('should sort by the createdAt ASC', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); + it('should filter include archived ones when passing includeArchived:true', async () => { + await saveProjectDirectlyToDb(createProjectData()); const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - await saveRecurringDonationDirectlyToDb({ + const recurringDonation = await saveRecurringDonationDirectlyToDb({ donationData: { donorId: donor.id, + status: RECURRING_DONATION_STATUS.ACTIVE, + isArchived: false, }, }); - await saveRecurringDonationDirectlyToDb({ + const archivedRecurringDonation = await saveRecurringDonationDirectlyToDb({ donationData: { donorId: donor.id, + status: RECURRING_DONATION_STATUS.PENDING, + isArchived: true, }, }); @@ -591,10 +2700,7 @@ function recurringDonationsByUserIdTestCases() { query: fetchRecurringDonationsByUserIdQuery, variables: { userId: donor.id, - orderBy: { - field: 'createdAt', - direction: 'ASC', - }, + includeArchived: true, }, }, {}, @@ -603,24 +2709,27 @@ function recurringDonationsByUserIdTestCases() { const donations = result.data.data.recurringDonationsByUserId.recurringDonations; assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 2); - for (let i = 0; i < donations.length - 1; i++) { - assert.isTrue(donations[i].createdAt <= donations[i + 1].createdAt); - } + assert.isOk( + donations.find(d => Number(d.id) === archivedRecurringDonation.id), + ); + assert.isOk(donations.find(d => Number(d.id) === recurringDonation.id)); }); - it('should sort by the amount ASC', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); + it('should not include archived ones when passing includeArchived:false', async () => { + await saveProjectDirectlyToDb(createProjectData()); const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - await saveRecurringDonationDirectlyToDb({ + const recurringDonation = await saveRecurringDonationDirectlyToDb({ donationData: { donorId: donor.id, - amount: 100, + status: RECURRING_DONATION_STATUS.ACTIVE, + isArchived: false, }, }); - await saveRecurringDonationDirectlyToDb({ + const archivedRecurringDonation = await saveRecurringDonationDirectlyToDb({ donationData: { donorId: donor.id, - amount: 200, + status: RECURRING_DONATION_STATUS.PENDING, + isArchived: true, }, }); @@ -630,10 +2739,7 @@ function recurringDonationsByUserIdTestCases() { query: fetchRecurringDonationsByUserIdQuery, variables: { userId: donor.id, - orderBy: { - field: 'amount', - direction: 'ASC', - }, + includeArchived: false, }, }, {}, @@ -641,25 +2747,28 @@ function recurringDonationsByUserIdTestCases() { const donations = result.data.data.recurringDonationsByUserId.recurringDonations; - assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 2); - for (let i = 0; i < donations.length - 1; i++) { - assert.isTrue(donations[i].amount <= donations[i + 1].amount); - } + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 1); + assert.isNotOk( + donations.find(d => Number(d.id) === archivedRecurringDonation.id), + ); + assert.isOk(donations.find(d => Number(d.id) === recurringDonation.id)); }); - it('should sort by the amount DESC', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); + it('should return non archived if we dont send includeArchived field', async () => { + await saveProjectDirectlyToDb(createProjectData()); const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - await saveRecurringDonationDirectlyToDb({ + const recurringDonation = await saveRecurringDonationDirectlyToDb({ donationData: { donorId: donor.id, - amount: 100, + status: RECURRING_DONATION_STATUS.ACTIVE, + isArchived: false, }, }); - await saveRecurringDonationDirectlyToDb({ + const archivedRecurringDonation = await saveRecurringDonationDirectlyToDb({ donationData: { donorId: donor.id, - amount: 200, + status: RECURRING_DONATION_STATUS.PENDING, + isArchived: true, }, }); @@ -669,10 +2778,6 @@ function recurringDonationsByUserIdTestCases() { query: fetchRecurringDonationsByUserIdQuery, variables: { userId: donor.id, - orderBy: { - field: 'amount', - direction: 'DESC', - }, }, }, {}, @@ -680,28 +2785,30 @@ function recurringDonationsByUserIdTestCases() { const donations = result.data.data.recurringDonationsByUserId.recurringDonations; - assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 2); - for (let i = 0; i < donations.length - 1; i++) { - assert.isTrue(donations[i].amount >= donations[i + 1].amount); - } + assert.equal(result.data.data.recurringDonationsByUserId.totalCount, 1); + assert.isNotOk( + donations.find(d => Number(d.id) === archivedRecurringDonation.id), + ); + assert.isOk(donations.find(d => Number(d.id) === recurringDonation.id)); }); } + function updateRecurringDonationStatusTestCases() { it('should donation status remain pending after calling without sending status (we assume its not mined so far)', async () => { const transactionInfo = { txHash: generateRandomEvmTxHash(), networkId: NETWORK_IDS.XDAI, - amount: 1, + flowRate: '1000', fromAddress: generateRandomEtheriumAddress(), toAddress: generateRandomEtheriumAddress(), currency: 'GIV', timestamp: 1647069070, }; - const project = await saveProjectDirectlyToDb({ + await saveProjectDirectlyToDb({ ...createProjectData(), walletAddress: transactionInfo.toAddress, }); - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + await saveUserDirectlyToDb(transactionInfo.fromAddress); const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const donation = await saveRecurringDonationDirectlyToDb({ donationData: { @@ -740,17 +2847,17 @@ function updateRecurringDonationStatusTestCases() { const transactionInfo = { txHash: generateRandomEvmTxHash(), networkId: NETWORK_IDS.XDAI, - amount: 1, + flowRate: '200', fromAddress: generateRandomEtheriumAddress(), toAddress: generateRandomEtheriumAddress(), currency: 'GIV', timestamp: 1647069070, }; - const project = await saveProjectDirectlyToDb({ + await saveProjectDirectlyToDb({ ...createProjectData(), walletAddress: transactionInfo.toAddress, }); - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + await saveUserDirectlyToDb(transactionInfo.fromAddress); const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const donation = await saveRecurringDonationDirectlyToDb({ donationData: { diff --git a/src/resolvers/recurringDonationResolver.ts b/src/resolvers/recurringDonationResolver.ts index 8ca6ef1ae..a58f4609e 100644 --- a/src/resolvers/recurringDonationResolver.ts +++ b/src/resolvers/recurringDonationResolver.ts @@ -13,9 +13,16 @@ import { Resolver, } from 'type-graphql'; +import { Brackets } from 'typeorm'; +import { Service } from 'typedi'; +import { Max, Min } from 'class-validator'; import { AnchorContractAddress } from '../entities/anchorContractAddress'; import { findProjectById } from '../repositories/projectRepository'; -import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; +import { + errorMessages, + i18n, + translationErrorMessagesKeys, +} from '../utils/errorMessages'; import { findActiveAnchorAddress } from '../repositories/anchorContractAddressRepository'; import { ApolloContext } from '../types/ApolloContext'; import { findUserById } from '../repositories/userRepository'; @@ -26,34 +33,41 @@ import { import { createNewRecurringDonation, findRecurringDonationById, + findRecurringDonationByProjectIdAndUserIdAndCurrency, + updateRecurringDonation, } from '../repositories/recurringDonationRepository'; -import { publicSelectionFields } from '../entities/user'; -import { Brackets } from 'typeorm'; import { detectAddressChainType } from '../utils/networks'; -import { Service } from 'typedi'; -import { Max, Min } from 'class-validator'; import { logger } from '../utils/logger'; import { updateDonationQueryValidator, validateWithJoiSchema, } from '../utils/validators/graphqlQueryValidators'; import { sleep } from '../utils/utils'; -import { syncDonationStatusWithBlockchainNetwork } from '../services/donationService'; import SentryLogger from '../sentryLogger'; import { updateRecurringDonationStatusWithNetwork } from '../services/recurringDonationService'; +import { markDraftRecurringDonationStatusMatched } from '../repositories/draftRecurringDonationRepository'; @InputType() class RecurringDonationSortBy { - @Field(type => RecurringDonationSortField) + @Field(_type => RecurringDonationSortField) field: RecurringDonationSortField; - @Field(type => RecurringDonationSortDirection) + @Field(_type => RecurringDonationSortDirection) direction: RecurringDonationSortDirection; } +@InputType() +class FinishStatus { + @Field(_type => Boolean) + active: boolean; + + @Field(_type => Boolean) + ended: boolean; +} + export enum RecurringDonationSortField { createdAt = 'createdAt', - amount = 'amount', + flowRate = 'flowRate', } enum RecurringDonationSortDirection { @@ -76,28 +90,33 @@ registerEnumType(RecurringDonationSortDirection, { description: 'Sort direction', }); +registerEnumType(FinishStatus, { + name: 'FinishStatus', + description: 'Filter active status', +}); + @ObjectType() class PaginateRecurringDonations { - @Field(type => [RecurringDonation], { nullable: true }) + @Field(_type => [RecurringDonation], { nullable: true }) recurringDonations: RecurringDonation[]; - @Field(type => Number, { nullable: true }) + @Field(_type => Number, { nullable: true }) totalCount: number; } @Service() @ArgsType() class UserRecurringDonationsArgs { - @Field(type => Int, { defaultValue: 0 }) + @Field(_type => Int, { defaultValue: 0 }) @Min(0) skip: number; - @Field(type => Int, { defaultValue: 10 }) + @Field(_type => Int, { defaultValue: 10 }) @Min(0) @Max(50) take: number; - @Field(type => RecurringDonationSortBy, { + @Field(_type => RecurringDonationSortBy, { defaultValue: { field: RecurringDonationSortField.createdAt, direction: RecurringDonationSortDirection.DESC, @@ -105,35 +124,50 @@ class UserRecurringDonationsArgs { }) orderBy: RecurringDonationSortBy; - @Field(type => Int, { nullable: false }) + @Field(_type => Int, { nullable: false }) userId: number; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) status: string; - @Field(type => Boolean, { nullable: true, defaultValue: false }) - finished: boolean; + @Field(_type => Boolean, { nullable: true, defaultValue: false }) + includeArchived: boolean; + + @Field(_type => FinishStatus, { + nullable: true, + defaultValue: { + active: true, + ended: false, + }, + }) + finishStatus: FinishStatus; + + @Field(_type => [String], { nullable: true, defaultValue: [] }) + filteredTokens: string[]; } @ObjectType() class UserRecurringDonations { - @Field(type => [RecurringDonation]) + @Field(_type => [RecurringDonation]) recurringDonations: RecurringDonation[]; - @Field(type => Int) + @Field(_type => Int) totalCount: number; } -@Resolver(of => AnchorContractAddress) +@Resolver(_of => AnchorContractAddress) export class RecurringDonationResolver { - @Mutation(returns => RecurringDonation, { nullable: true }) + @Mutation(_returns => RecurringDonation, { nullable: true }) async createRecurringDonation( @Ctx() ctx: ApolloContext, @Arg('projectId', () => Int) projectId: number, @Arg('networkId', () => Int) networkId: number, @Arg('txHash', () => String) txHash: string, @Arg('currency', () => String) currency: string, - @Arg('interval', () => String) interval: string, - @Arg('amount', () => Int) amount: number, + @Arg('flowRate', () => String) flowRate: string, + @Arg('anonymous', () => Boolean, { defaultValue: false }) + anonymous: boolean, + @Arg('isBatch', () => Boolean, { defaultValue: false }) + isBatch: boolean, ): Promise { const userId = ctx?.req?.user?.userId; const donor = await findUserById(userId); @@ -156,30 +190,190 @@ export class RecurringDonationResolver { ), ); } - - return createNewRecurringDonation({ + const recurringDonation = await createNewRecurringDonation({ donor, project, anchorContractAddress: currentAnchorProjectAddress, networkId, txHash, - amount, - interval, + flowRate, currency, + anonymous, + isBatch, }); + + await markDraftRecurringDonationStatusMatched({ + matchedRecurringDonationId: recurringDonation.id, + networkId, + currency, + projectId, + flowRate, + }); + + return recurringDonation; } - @Query(returns => PaginateRecurringDonations, { nullable: true }) - async recurringDonationsByProjectId( + @Mutation(_returns => RecurringDonation, { nullable: true }) + async updateRecurringDonationParamsById( @Ctx() ctx: ApolloContext, - @Arg('take', type => Int, { defaultValue: 10 }) take: number, - @Arg('skip', type => Int, { defaultValue: 0 }) skip: number, - @Arg('projectId', type => Int, { nullable: false }) projectId: number, - @Arg('status', type => String, { nullable: true }) status: string, - @Arg('finished', type => Boolean, { nullable: true, defaultValue: false }) - finished: boolean, - @Arg('searchTerm', type => String, { nullable: true }) searchTerm: string, - @Arg('orderBy', type => RecurringDonationSortBy, { + @Arg('recurringDonationId', () => Int) recurringDonationId: number, + @Arg('projectId', () => Int) projectId: number, + @Arg('networkId', () => Int) networkId: number, + @Arg('currency', () => String) currency: string, + @Arg('txHash', () => String, { nullable: true }) txHash?: string, + @Arg('flowRate', () => String, { nullable: true }) flowRate?: string, + @Arg('anonymous', () => Boolean, { nullable: true }) anonymous?: boolean, + @Arg('isArchived', () => Boolean, { nullable: true }) isArchived?: boolean, + @Arg('status', () => String, { nullable: true }) status?: string, + ): Promise { + const userId = ctx?.req?.user?.userId; + const donor = await findUserById(userId); + + if (!donor) { + throw new Error(i18n.__(translationErrorMessagesKeys.UN_AUTHORIZED)); + } + const project = await findProjectById(projectId); + if (!project) { + throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); + } + const recurringDonation = + await findRecurringDonationById(recurringDonationId); + + if (!recurringDonation) { + // TODO set proper error message + throw new Error(errorMessages.RECURRING_DONATION_NOT_FOUND); + } + + if (recurringDonation.donor.id !== donor.id) { + throw new Error(errorMessages.RECURRING_DONATION_NOT_FOUND); + } + + const currentAnchorProjectAddress = await findActiveAnchorAddress({ + projectId, + networkId, + }); + + if (!currentAnchorProjectAddress) { + throw new Error( + i18n.__( + translationErrorMessagesKeys.THERE_IS_NOT_ACTIVE_ANCHOR_ADDRESS_FOR_THIS_PROJECT, + ), + ); + } + + const updatedRecurringDonation = await updateRecurringDonation({ + recurringDonation, + txHash, + flowRate, + anonymous, + status, + isArchived, + }); + + await markDraftRecurringDonationStatusMatched({ + matchedRecurringDonationId: recurringDonation.id, + networkId, + currency, + projectId, + flowRate: updatedRecurringDonation.flowRate, + }); + + return updatedRecurringDonation; + } + + @Mutation(_returns => RecurringDonation, { nullable: true }) + async updateRecurringDonationParams( + @Ctx() ctx: ApolloContext, + @Arg('projectId', () => Int) projectId: number, + @Arg('networkId', () => Int) networkId: number, + @Arg('currency', () => String) currency: string, + @Arg('txHash', () => String, { nullable: true }) txHash?: string, + @Arg('flowRate', () => String, { nullable: true }) flowRate?: string, + @Arg('anonymous', () => Boolean, { nullable: true }) anonymous?: boolean, + @Arg('isArchived', () => Boolean, { nullable: true }) isArchived?: boolean, + @Arg('status', () => String, { nullable: true }) status?: string, + ): Promise { + const userId = ctx?.req?.user?.userId; + const donor = await findUserById(userId); + + if (!donor) { + throw new Error(i18n.__(translationErrorMessagesKeys.UN_AUTHORIZED)); + } + const project = await findProjectById(projectId); + if (!project) { + throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); + } + const recurringDonation = + await findRecurringDonationByProjectIdAndUserIdAndCurrency({ + projectId, + userId: donor.id, + currency, + }); + + if (!recurringDonation) { + // TODO set proper error message + throw new Error(errorMessages.RECURRING_DONATION_NOT_FOUND); + } + + if (recurringDonation.donor.id !== donor.id) { + throw new Error(errorMessages.RECURRING_DONATION_NOT_FOUND); + } + + const currentAnchorProjectAddress = await findActiveAnchorAddress({ + projectId, + networkId, + }); + + if (!currentAnchorProjectAddress) { + throw new Error( + i18n.__( + translationErrorMessagesKeys.THERE_IS_NOT_ACTIVE_ANCHOR_ADDRESS_FOR_THIS_PROJECT, + ), + ); + } + + const updatedRecurringDonation = await updateRecurringDonation({ + recurringDonation, + txHash, + flowRate, + anonymous, + status, + isArchived, + }); + + await markDraftRecurringDonationStatusMatched({ + matchedRecurringDonationId: recurringDonation.id, + networkId, + currency, + projectId, + flowRate: updatedRecurringDonation.flowRate, + }); + + return updatedRecurringDonation; + } + + @Query(_returns => PaginateRecurringDonations, { nullable: true }) + async recurringDonationsByProjectId( + @Ctx() _ctx: ApolloContext, + @Arg('take', _type => Int, { defaultValue: 10 }) take: number, + @Arg('skip', _type => Int, { defaultValue: 0 }) skip: number, + @Arg('projectId', _type => Int, { nullable: false }) projectId: number, + @Arg('status', _type => String, { nullable: true }) status: string, + @Arg('includeArchived', _type => Boolean, { + nullable: true, + defaultValue: false, + }) + includeArchived: boolean, + @Arg('finishStatus', _type => FinishStatus, { + nullable: true, + defaultValue: { + active: true, + ended: false, + }, + }) + finishStatus: FinishStatus, + @Arg('searchTerm', _type => String, { nullable: true }) searchTerm: string, + @Arg('orderBy', _type => RecurringDonationSortBy, { defaultValue: { field: RecurringDonationSortField.createdAt, direction: RecurringDonationSortDirection.DESC, @@ -208,13 +402,23 @@ export class RecurringDonationResolver { 'donor.passportStamps', ]) .where(`recurringDonation.projectId = ${projectId}`); - query - .andWhere(`recurringDonation.finished = ${finished}`) - .orderBy( - `recurringDonation.${orderBy.field}`, - orderBy.direction, - RecurringDonationNullDirection[orderBy.direction as string], - ); + query.orderBy( + `recurringDonation.${orderBy.field}`, + orderBy.direction, + RecurringDonationNullDirection[orderBy.direction as string], + ); + const finishStatusArray: boolean[] = []; + if (finishStatus.active) { + finishStatusArray.push(false); + } + if (finishStatus.ended) { + finishStatusArray.push(true); + } + if (finishStatusArray.length > 0) { + query.andWhere(`recurringDonation.finished in (:...finishStatusArray)`, { + finishStatusArray, + }); + } if (status) { query.andWhere(`recurringDonation.status = :status`, { @@ -222,6 +426,13 @@ export class RecurringDonationResolver { }); } + if (!includeArchived) { + // Return only non-archived recurring donations + query.andWhere(`recurringDonation.isArchived = :isArchived`, { + isArchived: false, + }); + } + if (searchTerm) { query.andWhere( new Brackets(qb => { @@ -242,16 +453,13 @@ export class RecurringDonationResolver { detectAddressChainType(searchTerm) === undefined && Number(searchTerm) ) { - const amount = Number(searchTerm); - - qb.orWhere('recurringDonation.amount = :amount', { - amount, + qb.orWhere('recurringDonation.flowRate = :searchTerm', { + searchTerm, }); } }), ); } - const [recurringDonations, donationsCount] = await query .take(take) .skip(skip) @@ -262,7 +470,7 @@ export class RecurringDonationResolver { }; } - @Query(returns => UserRecurringDonations, { nullable: true }) + @Query(_returns => UserRecurringDonations, { nullable: true }) async recurringDonationsByUserId( @Args() { @@ -271,13 +479,16 @@ export class RecurringDonationResolver { orderBy, userId, status, - finished, + includeArchived, + finishStatus, + filteredTokens, }: UserRecurringDonationsArgs, @Ctx() ctx: ApolloContext, ) { const loggedInUserId = ctx?.req?.user?.userId; const query = RecurringDonation.createQueryBuilder('recurringDonation') .leftJoinAndSelect('recurringDonation.project', 'project') + .leftJoinAndSelect('project.anchorContracts', 'anchor_contract_address') .leftJoin('recurringDonation.donor', 'donor') .addSelect([ 'donor.id', @@ -303,11 +514,37 @@ export class RecurringDonationResolver { } if (status) { - query.andWhere(`donation.status = :status`, { + query.andWhere(`recurringDonation.status = :status`, { status, }); } + if (!includeArchived) { + // Return only non-archived recurring donations + query.andWhere(`recurringDonation.isArchived = :isArchived`, { + isArchived: false, + }); + } + + const finishStatusArray: boolean[] = []; + if (finishStatus.active) { + finishStatusArray.push(false); + } + if (finishStatus.ended) { + finishStatusArray.push(true); + } + if (finishStatusArray.length > 0) { + query.andWhere(`recurringDonation.finished in (:...finishStatusArray)`, { + finishStatusArray, + }); + } + + if (filteredTokens && filteredTokens.length > 0) { + query.andWhere(`recurringDonation.currency IN (:...filteredTokens)`, { + filteredTokens, + }); + } + const [recurringDonations, totalCount] = await query .take(take) .skip(skip) @@ -318,7 +555,7 @@ export class RecurringDonationResolver { }; } - @Mutation(returns => RecurringDonation) + @Mutation(_returns => RecurringDonation) async updateRecurringDonationStatus( @Arg('donationId') donationId: number, @Arg('status', { nullable: true }) status: string, @@ -383,12 +620,13 @@ export class RecurringDonationResolver { // We just update status of donation with tx status in blockchain network // but if user send failed status, and there were nothing in network we change it to failed updatedRecurringDonation.status = RECURRING_DONATION_STATUS.FAILED; + updatedRecurringDonation.finished = true; await updatedRecurringDonation.save(); } return updatedRecurringDonation; } catch (e) { SentryLogger.captureException(e); - logger.error('updateRecurringDonationStatus() error', e); + logger.error('updateRecurringDonationStatus() error ', e); throw e; } } diff --git a/src/resolvers/resolvers.ts b/src/resolvers/resolvers.ts index fa8f8e846..b0ce54c14 100644 --- a/src/resolvers/resolvers.ts +++ b/src/resolvers/resolvers.ts @@ -1,7 +1,5 @@ import { UserResolver } from './userResolver'; import { ProjectResolver } from './projectResolver'; -import { LoginResolver } from '../user/LoginResolver'; -import { RegisterResolver } from '../user/register/RegisterResolver'; import { MeResolver } from '../user/MeResolver'; import { UploadResolver } from './uploadResolver'; import { CategoryResolver } from './categoryResolver'; @@ -23,6 +21,7 @@ import { AnchorContractAddressResolver } from './anchorContractAddressResolver'; import { RecurringDonationResolver } from './recurringDonationResolver'; import { DraftDonationResolver } from './draftDonationResolver'; +// eslint-disable-next-line @typescript-eslint/ban-types export const getResolvers = (): Function[] => { return [ UserResolver, @@ -30,8 +29,6 @@ export const getResolvers = (): Function[] => { ChainvineResolver, StatusReasonResolver, - LoginResolver, - RegisterResolver, MeResolver, UploadResolver, CategoryResolver, diff --git a/src/resolvers/socialProfilesResolver.test.ts b/src/resolvers/socialProfilesResolver.test.ts index 0b32b73fe..02466bb18 100644 --- a/src/resolvers/socialProfilesResolver.test.ts +++ b/src/resolvers/socialProfilesResolver.test.ts @@ -1,3 +1,5 @@ +import axios from 'axios'; +import { assert } from 'chai'; import { createProjectData, generateRandomEtheriumAddress, @@ -8,13 +10,11 @@ import { saveUserDirectlyToDb, SEED_DATA, } from '../../test/testUtils'; -import axios from 'axios'; import { addNewSocialProfileMutation, removeSocialProfileMutation, } from '../../test/graphqlQueries'; import { SOCIAL_NETWORKS } from '../entities/socialProfile'; -import { assert } from 'chai'; import { errorMessages } from '../utils/errorMessages'; import { PROJECT_VERIFICATION_STATUSES } from '../entities/projectVerificationForm'; import { createSocialProfile } from '../repositories/socialProfileRepository'; diff --git a/src/resolvers/socialProfilesResolver.ts b/src/resolvers/socialProfilesResolver.ts index ce4c77301..38e0eb9cf 100644 --- a/src/resolvers/socialProfilesResolver.ts +++ b/src/resolvers/socialProfilesResolver.ts @@ -1,14 +1,9 @@ -import { Arg, Ctx, Float, Int, Mutation, Query, Resolver } from 'type-graphql'; +import { Arg, Ctx, Int, Mutation, Resolver } from 'type-graphql'; import { SocialProfile } from '../entities/socialProfile'; import { ApolloContext } from '../types/ApolloContext'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { findProjectVerificationFormById } from '../repositories/projectVerificationRepository'; import { - createSocialProfile, findSocialProfileById, removeSocialProfileById, } from '../repositories/socialProfileRepository'; @@ -16,12 +11,12 @@ import { getSocialNetworkAdapter } from '../adapters/adaptersFactory'; import { PROJECT_VERIFICATION_STATUSES } from '../entities/projectVerificationForm'; import { setOauth2SocialProfileInRedis } from '../services/socialProfileService'; -@Resolver(of => SocialProfile) +@Resolver(_of => SocialProfile) export class SocialProfilesResolver { - @Mutation(returns => String) + @Mutation(_returns => String) async addNewSocialProfile( - @Arg('projectVerificationId', type => Int) projectVerificationId: number, - @Arg('socialNetwork', type => String) socialNetwork: string, + @Arg('projectVerificationId', _type => Int) projectVerificationId: number, + @Arg('socialNetwork', _type => String) socialNetwork: string, @Ctx() { req: { user } }: ApolloContext, ): Promise { if (!user || !user?.userId) { @@ -68,9 +63,9 @@ export class SocialProfilesResolver { }); } - @Mutation(returns => Boolean) + @Mutation(_returns => Boolean) async removeSocialProfile( - @Arg('socialProfileId', type => Int) socialProfileId: number, + @Arg('socialProfileId', _type => Int) socialProfileId: number, @Ctx() { req: { user } }: ApolloContext, ): Promise { if (!user || !user?.userId) { diff --git a/src/resolvers/statusReasonResolver.ts b/src/resolvers/statusReasonResolver.ts index 310d44ef8..596fb88cd 100644 --- a/src/resolvers/statusReasonResolver.ts +++ b/src/resolvers/statusReasonResolver.ts @@ -1,4 +1,4 @@ -import { Arg, Int, Query, Resolver } from 'type-graphql'; +import { Arg, Query, Resolver } from 'type-graphql'; import { ProjectStatusReason } from '../entities/projectStatusReason'; import { findAllStatusReasons, @@ -6,9 +6,9 @@ import { } from '../repositories/statusReasonRepository'; import { logger } from '../utils/logger'; -@Resolver(of => ProjectStatusReason) +@Resolver(_of => ProjectStatusReason) export class StatusReasonResolver { - @Query(returns => [ProjectStatusReason]) + @Query(_returns => [ProjectStatusReason]) async getStatusReasons( @Arg('statusId', { nullable: true }) statusId?: number, ) { diff --git a/src/resolvers/types/ProjectVerificationUpdateInput.ts b/src/resolvers/types/ProjectVerificationUpdateInput.ts index a0f9cd1b0..b02ece474 100644 --- a/src/resolvers/types/ProjectVerificationUpdateInput.ts +++ b/src/resolvers/types/ProjectVerificationUpdateInput.ts @@ -1,7 +1,6 @@ import { Field, InputType } from 'type-graphql'; -import { ProjectContacts } from '../../entities/projectVerificationForm'; -import { Column } from 'typeorm'; import { ChainType } from '../../types/network'; +import { ProjectSocialMediaType } from '../../types/projectSocialMediaType'; @InputType() class ProjectPersonalInfoInputType { @@ -15,19 +14,19 @@ class ProjectPersonalInfoInputType { @InputType() class MilestonesInputType { - @Field(type => String, { nullable: true }) - foundationDate?: String; + @Field(_type => String, { nullable: true }) + foundationDate?: string; @Field({ nullable: true }) mission?: string; @Field({ nullable: true }) achievedMilestones?: string; - @Field(type => [String], { nullable: true }) + @Field(_type => [String], { nullable: true }) achievedMilestonesProofs?: string[]; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) problem?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) plans?: string; - @Field(type => String, { nullable: true }) + @Field(_type => String, { nullable: true }) impact?: string; } @@ -47,10 +46,19 @@ export class RelatedAddressInputType { address: string; @Field({ nullable: true }) networkId: number; - @Field(type => ChainType, { defaultValue: ChainType.EVM }) + @Field(_type => ChainType, { defaultValue: ChainType.EVM }) chainType?: ChainType; } +@InputType() +export class ProjectSocialMediaInput { + @Field(_type => ProjectSocialMediaType) + type: ProjectSocialMediaType; + + @Field() + link: string; +} + @InputType() class ManagingFundsInputType { @Field({ nullable: true }) @@ -72,7 +80,7 @@ class ProjectRegistryInputType { organizationDescription?: string; @Field({ nullable: true }) organizationName?: string; - @Field(type => [String], { nullable: true }) + @Field(_type => [String], { nullable: true }) attachments: string[]; } @@ -90,7 +98,7 @@ export class ProjectVerificationUpdateInput { @Field({ nullable: true }) projectRegistry?: ProjectRegistryInputType; - @Field(type => [ProjectContactsInputType], { nullable: true }) + @Field(_type => [ProjectContactsInputType], { nullable: true }) projectContacts?: ProjectContactsInputType[]; @Field({ nullable: true }) diff --git a/src/resolvers/types/project-input.ts b/src/resolvers/types/project-input.ts index 63fa699fd..444df5016 100644 --- a/src/resolvers/types/project-input.ts +++ b/src/resolvers/types/project-input.ts @@ -1,8 +1,12 @@ import { Field, InputType } from 'type-graphql'; -import { RelatedAddressInputType } from './ProjectVerificationUpdateInput'; - import { FileUpload } from 'graphql-upload/Upload.js'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'; +import { MaxLength } from 'class-validator'; +import { + ProjectSocialMediaInput, + RelatedAddressInputType, +} from './ProjectVerificationUpdateInput'; + import { IMAGE_LINK_MAX_SIZE, IMPACT_LOCATION_MAX_SIZE, @@ -10,12 +14,11 @@ import { PROJECT_TITLE_MAX_LENGTH, } from '../../constants/validators'; import { errorMessages } from '../../utils/errorMessages'; -import { MaxLength } from 'class-validator'; @InputType() export class ImageUpload { // Client uploads image file - @Field(type => GraphQLUpload, { nullable: true }) + @Field(_type => GraphQLUpload, { nullable: true }) image: FileUpload; @Field({ nullable: true }) @@ -37,7 +40,7 @@ class ProjectInput { }) description?: string; - @Field(type => [String], { nullable: true, defaultValue: [] }) + @Field(_type => [String], { nullable: true, defaultValue: [] }) categories?: string[]; @Field({ nullable: true }) @@ -48,11 +51,14 @@ class ProjectInput { @MaxLength(IMPACT_LOCATION_MAX_SIZE) impactLocation?: string; - @Field(type => Boolean, { nullable: true, defaultValue: false }) + @Field(_type => Boolean, { nullable: true, defaultValue: false }) isDraft?: boolean; @Field({ nullable: true }) organisationId?: number; + + @Field(() => [ProjectSocialMediaInput], { nullable: true }) + socialMedia?: ProjectSocialMediaInput[]; } @InputType() diff --git a/src/resolvers/types/userResolver.ts b/src/resolvers/types/userResolver.ts index 27b008e04..94a9c5761 100644 --- a/src/resolvers/types/userResolver.ts +++ b/src/resolvers/types/userResolver.ts @@ -1,8 +1,8 @@ -import { User } from '../../entities/user'; import { Field, ObjectType } from 'type-graphql'; +import { User } from '../../entities/user'; @ObjectType() export class UserByAddressResponse extends User { - @Field(type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: true }) isSignedIn?: boolean; } diff --git a/src/resolvers/uploadResolver.test.ts b/src/resolvers/uploadResolver.test.ts index 940067f37..0ec08e446 100644 --- a/src/resolvers/uploadResolver.test.ts +++ b/src/resolvers/uploadResolver.test.ts @@ -1,3 +1,4 @@ +import { createReadStream, readFileSync } from 'fs'; import { assert } from 'chai'; import sinon from 'sinon'; import axios from 'axios'; @@ -11,12 +12,11 @@ import { SEED_DATA, } from '../../test/testUtils'; import * as pinataUtils from '../middleware/pinataUtils'; -import { createReadStream, readFileSync } from 'fs'; import { errorMessages } from '../utils/errorMessages'; import { TraceImageOwnerType } from './uploadResolver'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const path = require('path'); -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const FormData = require('form-data'); // test cases diff --git a/src/resolvers/uploadResolver.ts b/src/resolvers/uploadResolver.ts index 68ddca8a0..ffeca236a 100644 --- a/src/resolvers/uploadResolver.ts +++ b/src/resolvers/uploadResolver.ts @@ -10,21 +10,16 @@ import { import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'; import { FileUpload } from 'graphql-upload/Upload.js'; import { ApolloContext } from '../types/ApolloContext'; - import { pinFile, pinFileDataBase64 } from '../middleware/pinataUtils'; import { logger } from '../utils/logger'; import { getLoggedInUser } from '../services/authorizationServices'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import SentryLogger from '../sentryLogger'; @InputType() export class FileUploadInputType { // Client uploads image file - @Field(type => GraphQLUpload) + @Field(_type => GraphQLUpload) image: FileUpload; } @@ -52,7 +47,7 @@ export class TraceFileUploadInputType { @Field() entityId: string; - @Field(type => TraceImageOwnerType) + @Field(_type => TraceImageOwnerType) imageOwnerType: TraceImageOwnerType; @Field() @@ -65,15 +60,15 @@ export class UploadResolver { async upload( @Arg('fileUpload') fileUpload: FileUploadInputType, @Ctx() ctx: ApolloContext, - ): Promise { + ): Promise { await getLoggedInUser(ctx); // if (!fileUpload.image) { // throw Error('Upload file failed'); // } - const { filename, createReadStream, encoding } = await fileUpload.image; + const { filename, createReadStream } = await fileUpload.image; try { - const response = await pinFile(createReadStream(), filename, encoding); + const response = await pinFile(createReadStream(), filename); return `${process.env.PINATA_GATEWAY_ADDRESS}/ipfs/${response.IpfsHash}`; } catch (e) { logger.error('upload() error', e); @@ -87,14 +82,14 @@ export class UploadResolver { async traceImageUpload( @Arg('traceFileUpload') traceFileUpload: TraceFileUploadInputType, @Ctx() ctx: ApolloContext, - ): Promise { - const { fileDataBase64, user, imageOwnerType, password } = traceFileUpload; + ): Promise { + const { fileDataBase64, password } = traceFileUpload; let errorMessage; if (!process.env.TRACE_FILE_UPLOADER_PASSWORD) { errorMessage = `No password is defined for trace file uploader `; } else if (password !== process.env.TRACE_FILE_UPLOADER_PASSWORD) { - // @ts-ignore + // @ts-expect-error ip is not null errorMessage = `Invalid password to upload trace image from ip ${ctx?.req?.ip}`; } @@ -106,11 +101,7 @@ export class UploadResolver { } try { - const response = await pinFileDataBase64( - fileDataBase64, - undefined, - 'base64', - ); + const response = await pinFileDataBase64(fileDataBase64, undefined); return `/ipfs/${response.IpfsHash}`; } catch (e) { logger.error('upload() error', e); diff --git a/src/resolvers/userProjectPowerResolver.test.ts b/src/resolvers/userProjectPowerResolver.test.ts index 2877c28db..5f4f7c403 100644 --- a/src/resolvers/userProjectPowerResolver.test.ts +++ b/src/resolvers/userProjectPowerResolver.test.ts @@ -1,3 +1,5 @@ +import axios from 'axios'; +import { assert } from 'chai'; import { createProjectData, generateRandomEtheriumAddress, @@ -5,9 +7,7 @@ import { saveProjectDirectlyToDb, saveUserDirectlyToDb, } from '../../test/testUtils'; -import axios from 'axios'; import { getUserProjectPowerQuery } from '../../test/graphqlQueries'; -import { assert } from 'chai'; import { errorMessages } from '../utils/errorMessages'; import { setPowerRound } from '../repositories/powerRoundRepository'; import { refreshUserProjectPowerView } from '../repositories/userProjectPowerViewRepository'; diff --git a/src/resolvers/userProjectPowerResolver.ts b/src/resolvers/userProjectPowerResolver.ts index e037caa81..9d66d426f 100644 --- a/src/resolvers/userProjectPowerResolver.ts +++ b/src/resolvers/userProjectPowerResolver.ts @@ -9,14 +9,10 @@ import { registerEnumType, Resolver, } from 'type-graphql'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; -import { PowerBoosting } from '../entities/powerBoosting'; import { Max, Min } from 'class-validator'; import { Service } from 'typedi'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; +import { PowerBoosting } from '../entities/powerBoosting'; import { UserProjectPowerView } from '../views/userProjectPowerView'; import { getUserProjectPowers } from '../repositories/userProjectPowerViewRepository'; @@ -42,13 +38,13 @@ registerEnumType(UserPowerOrderDirection, { @InputType() export class UserPowerOrderBy { - @Field(type => UserPowerOrderField, { + @Field(_type => UserPowerOrderField, { nullable: true, defaultValue: UserPowerOrderField.BoostedPower, }) field: UserPowerOrderField | null; - @Field(type => UserPowerOrderDirection, { + @Field(_type => UserPowerOrderDirection, { nullable: true, defaultValue: UserPowerOrderDirection.DESC, }) @@ -58,16 +54,16 @@ export class UserPowerOrderBy { @Service() @ArgsType() export class UserProjectPowerArgs { - @Field(type => Int, { defaultValue: 0 }) + @Field(_type => Int, { defaultValue: 0 }) @Min(0) skip: number; - @Field(type => Int, { defaultValue: 20 }) + @Field(_type => Int, { defaultValue: 20 }) @Min(0) @Max(50) take: number; - @Field(type => UserPowerOrderBy, { + @Field(_type => UserPowerOrderBy, { nullable: true, defaultValue: { field: UserPowerOrderField.BoostedPower, @@ -76,28 +72,28 @@ export class UserProjectPowerArgs { }) orderBy: UserPowerOrderBy; - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) projectId?: number; - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) userId?: number; - @Field(type => Int, { nullable: true }) + @Field(_type => Int, { nullable: true }) round?: number; } @ObjectType() class UserProjectPowers { - @Field(type => [UserProjectPowerView]) + @Field(_type => [UserProjectPowerView]) userProjectPowers: UserProjectPowerView[]; - @Field(type => Int) + @Field(_type => Int) totalCount: number; } -@Resolver(of => PowerBoosting) +@Resolver(_of => PowerBoosting) export class UserProjectPowerResolver { - @Query(returns => UserProjectPowers) + @Query(_returns => UserProjectPowers) async userProjectPowers( @Args() { take, skip, projectId, userId, orderBy }: UserProjectPowerArgs, diff --git a/src/resolvers/userResolver.test.ts b/src/resolvers/userResolver.test.ts index ba4abc666..1832bdabd 100644 --- a/src/resolvers/userResolver.test.ts +++ b/src/resolvers/userResolver.test.ts @@ -1,30 +1,34 @@ // TODO Write test cases +import axios from 'axios'; +import { assert } from 'chai'; import { User } from '../entities/user'; import { createDonationData, createProjectData, generateRandomEtheriumAddress, + generateRandomEvmTxHash, generateTestAccessToken, graphqlUrl, saveDonationDirectlyToDb, saveProjectDirectlyToDb, + saveRecurringDonationDirectlyToDb, saveUserDirectlyToDb, SEED_DATA, } from '../../test/testUtils'; -import axios from 'axios'; import { refreshUserScores, updateUser, userByAddress, } from '../../test/graphqlQueries'; -import { assert } from 'chai'; import { errorMessages } from '../utils/errorMessages'; import { insertSinglePowerBoosting } from '../repositories/powerBoostingRepository'; -import { create } from 'domain'; import { DONATION_STATUS } from '../entities/donation'; import { getGitcoinAdapter } from '../adapters/adaptersFactory'; -import { findUserById } from '../repositories/userRepository'; +import { updateUserTotalDonated } from '../services/userService'; +import { addNewAnchorAddress } from '../repositories/anchorContractAddressRepository'; +import { NETWORK_IDS } from '../provider'; +import { RECURRING_DONATION_STATUS } from '../entities/recurringDonation'; describe('updateUser() test cases', updateUserTestCases); describe('userByAddress() test cases', userByAddressTestCases); @@ -164,6 +168,290 @@ function userByAddressTestCases() { assert.equal(result.data.data.userByAddress.email, userData.email); assert.equal(result.data.data.userByAddress.url, userData.url); }); + it('Get donationsCount and totalDonated correctly, when there is just one time donations', async () => { + const userData = { + firstName: 'firstName', + lastName: 'lastName', + email: 'giveth@gievth.com', + avatar: 'pinata address', + url: 'website url', + loginType: 'wallet', + walletAddress: generateRandomEtheriumAddress(), + }; + const user = await User.create(userData).save(); + const project = await saveProjectDirectlyToDb(createProjectData()); + await saveDonationDirectlyToDb( + { + ...createDonationData(), + amount: 100, + valueUsd: 100, + currency: 'USDT', + status: DONATION_STATUS.VERIFIED, + }, + user.id, + project.id, + ); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + amount: 50, + valueUsd: 50, + currency: 'USDT', + status: DONATION_STATUS.VERIFIED, + }, + user.id, + project.id, + ); + await updateUserTotalDonated(user.id); + + const accessToken = await generateTestAccessToken(user.id); + const result = await axios.post( + graphqlUrl, + { + query: userByAddress, + variables: { + address: userData.walletAddress, + }, + }, + { + headers: { + authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.userByAddress.walletAddress, + userData.walletAddress, + ); + assert.equal(result.data.data.userByAddress.donationsCount, 2); + assert.equal(result.data.data.userByAddress.totalDonated, 150); + }); + it('Get donationsCount and totalDonated correctly, when there is just recurringDonations, one active', async () => { + const userData = { + firstName: 'firstName', + lastName: 'lastName', + email: 'giveth@gievth.com', + avatar: 'pinata address', + url: 'website url', + loginType: 'wallet', + walletAddress: generateRandomEtheriumAddress(), + }; + const user = await User.create(userData).save(); + const project = await saveProjectDirectlyToDb(createProjectData()); + + await addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: user, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: user.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency: 'USDT', + totalUsdStreamed: 200, + status: RECURRING_DONATION_STATUS.ACTIVE, + }, + }); + + await updateUserTotalDonated(user.id); + + const accessToken = await generateTestAccessToken(user.id); + const result = await axios.post( + graphqlUrl, + { + query: userByAddress, + variables: { + address: userData.walletAddress, + }, + }, + { + headers: { + authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.userByAddress.walletAddress, + userData.walletAddress, + ); + assert.equal(result.data.data.userByAddress.totalDonated, 200); + assert.equal(result.data.data.userByAddress.donationsCount, 1); + }); + it('Get donationsCount and totalDonated correctly, when there is just recurringDonations, active, pending, failed, ended', async () => { + const userData = { + firstName: 'firstName', + lastName: 'lastName', + email: 'giveth@gievth.com', + avatar: 'pinata address', + url: 'website url', + loginType: 'wallet', + walletAddress: generateRandomEtheriumAddress(), + }; + const user = await User.create(userData).save(); + const project = await saveProjectDirectlyToDb(createProjectData()); + + await addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: user, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: user.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency: 'USDT', + totalUsdStreamed: 200, + status: RECURRING_DONATION_STATUS.ACTIVE, + }, + }); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: user.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency: 'USDT', + totalUsdStreamed: 200, + status: RECURRING_DONATION_STATUS.PENDING, + }, + }); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: user.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency: 'USDT', + totalUsdStreamed: 200, + status: RECURRING_DONATION_STATUS.ENDED, + }, + }); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: user.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency: 'USDT', + totalUsdStreamed: 200, + status: RECURRING_DONATION_STATUS.FAILED, + }, + }); + + await updateUserTotalDonated(user.id); + + const accessToken = await generateTestAccessToken(user.id); + const result = await axios.post( + graphqlUrl, + { + query: userByAddress, + variables: { + address: userData.walletAddress, + }, + }, + { + headers: { + authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.userByAddress.walletAddress, + userData.walletAddress, + ); + // for totalDonated we consider all recurring donations but for donationsCount we consider only active recurring donations + assert.equal(result.data.data.userByAddress.totalDonated, 800); + assert.equal(result.data.data.userByAddress.donationsCount, 1); + }); + it('Get donationsCount and totalDonated correctly, when there is both recurringDonations and one time donation', async () => { + const userData = { + firstName: 'firstName', + lastName: 'lastName', + email: 'giveth@gievth.com', + avatar: 'pinata address', + url: 'website url', + loginType: 'wallet', + walletAddress: generateRandomEtheriumAddress(), + }; + const user = await User.create(userData).save(); + const project = await saveProjectDirectlyToDb(createProjectData()); + + await addNewAnchorAddress({ + project, + owner: project.adminUser, + creator: user, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + amount: 50, + valueUsd: 50, + currency: 'USDT', + status: DONATION_STATUS.VERIFIED, + }, + user.id, + project.id, + ); + await updateUserTotalDonated(user.id); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: user.id, + projectId: project.id, + flowRate: '300', + anonymous: false, + currency: 'USDT', + totalUsdStreamed: 200, + status: RECURRING_DONATION_STATUS.ACTIVE, + }, + }); + + await updateUserTotalDonated(user.id); + + const accessToken = await generateTestAccessToken(user.id); + const result = await axios.post( + graphqlUrl, + { + query: userByAddress, + variables: { + address: userData.walletAddress, + }, + }, + { + headers: { + authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.data.userByAddress.walletAddress, + userData.walletAddress, + ); + + assert.equal(result.data.data.userByAddress.totalDonated, 250); + assert.equal(result.data.data.userByAddress.donationsCount, 2); + }); // TODO write test cases for likedProjectsCount, donationsCount, projectsCount fields it('Return boostedProjectsCount of a user', async () => { diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index a2d59f6dc..bbec66ecd 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -10,7 +10,6 @@ import { import { Repository } from 'typeorm'; import { User } from '../entities/user'; -import { RegisterInput } from '../user/register/RegisterInput'; import { AccountVerificationInput } from './types/accountVerificationInput'; import { ApolloContext } from '../types/ApolloContext'; import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; @@ -29,27 +28,28 @@ import { import { logger } from '../utils/logger'; import { isWalletAddressInPurpleList } from '../repositories/projectAddressRepository'; import { addressHasDonated } from '../repositories/donationRepository'; +import { getOrttoPersonAttributes } from '../adapters/notifications/NotificationCenterAdapter'; @ObjectType() class UserRelatedAddressResponse { - @Field(type => Boolean, { nullable: false }) + @Field(_type => Boolean, { nullable: false }) hasRelatedProject: boolean; - @Field(type => Boolean, { nullable: false }) + @Field(_type => Boolean, { nullable: false }) hasDonated: boolean; } -@Resolver(of => User) +@Resolver(_of => User) export class UserResolver { constructor(private readonly userRepository: Repository) { this.userRepository = AppDataSource.getDataSource().getRepository(User); } - async create(@Arg('data', () => RegisterInput) data: any) { - // return User.create(data).save(); - } + // async create(@Arg('data', () => RegisterInput) data: any) { + // return User.create(data).save(); + // } - @Query(returns => UserRelatedAddressResponse) + @Query(_returns => UserRelatedAddressResponse) async walletAddressUsed(@Arg('address') address: string) { return { hasRelatedProject: await isWalletAddressInPurpleList(address), @@ -57,9 +57,9 @@ export class UserResolver { }; } - @Query(returns => UserByAddressResponse, { nullable: true }) + @Query(_returns => UserByAddressResponse, { nullable: true }) async userByAddress( - @Arg('address', type => String) address: string, + @Arg('address', _type => String) address: string, @Ctx() { req: { user } }: ApolloContext, ) { const includeSensitiveFields = @@ -68,15 +68,18 @@ export class UserResolver { address, includeSensitiveFields, ); + if (!foundUser) { + throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); + } return { isSignedIn: Boolean(user), ...foundUser, }; } - @Query(returns => User, { nullable: true }) + @Query(_returns => User, { nullable: true }) async refreshUserScores( - @Arg('address', type => String) address: string, + @Arg('address', _type => String) address: string, @Ctx() { req: { user } }: ApolloContext, ) { const includeSensitiveFields = @@ -92,9 +95,8 @@ export class UserResolver { const passportScore = await getGitcoinAdapter().submitPassport({ address, }); - const passportStamps = await getGitcoinAdapter().getPassportStamps( - address, - ); + const passportStamps = + await getGitcoinAdapter().getPassportStamps(address); if (passportScore && passportScore?.score) { const score = Number(passportScore.score); @@ -110,7 +112,7 @@ export class UserResolver { return foundUser; } - @Mutation(returns => Boolean) + @Mutation(_returns => Boolean) async updateUser( @Arg('firstName', { nullable: true }) firstName: string, @Arg('lastName', { nullable: true }) lastName: string, @@ -171,21 +173,22 @@ export class UserResolver { dbUser.name = `${dbUser.firstName || ''} ${dbUser.lastName || ''}`.trim(); await dbUser.save(); - await getNotificationAdapter().updateOrttoUser({ + const orttoPerson = getOrttoPersonAttributes({ firstName: dbUser.firstName, lastName: dbUser.lastName, email: dbUser.email, userId: dbUser.id.toString(), }); + await getNotificationAdapter().updateOrttoPeople([orttoPerson]); return true; } // Sets the current account verification and creates related verifications - @Mutation(returns => Boolean) + @Mutation(_returns => Boolean) async addUserVerification( @Arg('dId', { nullable: true }) dId: string, - @Arg('verifications', type => [AccountVerificationInput]) + @Arg('verifications', _type => [AccountVerificationInput]) verificationsInput: AccountVerificationInput[], @Ctx() { req: { user } }: ApolloContext, ): Promise { diff --git a/src/routers/apiGivRoutes.test.ts b/src/routers/apiGivRoutes.test.ts index a8d357b1f..380fbe677 100644 --- a/src/routers/apiGivRoutes.test.ts +++ b/src/routers/apiGivRoutes.test.ts @@ -1,11 +1,11 @@ import { assert } from 'chai'; +import axios from 'axios'; import { createProjectData, generateRandomEtheriumAddress, generateRandomEvmTxHash, saveProjectDirectlyToDb, } from '../../test/testUtils'; -import axios from 'axios'; import { createBasicAuthentication } from '../utils/utils'; import { NETWORK_IDS } from '../provider'; import { DONATION_STATUS } from '../entities/donation'; diff --git a/src/routers/apiGivRoutes.ts b/src/routers/apiGivRoutes.ts index f8d84fd79..879346dd8 100644 --- a/src/routers/apiGivRoutes.ts +++ b/src/routers/apiGivRoutes.ts @@ -1,11 +1,7 @@ import express, { Request, Response } from 'express'; import { apiGivAuthentication } from '../middleware/apiGivAuthentication'; import { findDonationsByTransactionId } from '../repositories/donationRepository'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { Donation } from '../entities/donation'; import { findProjectByWalletAddress } from '../repositories/projectRepository'; import { findTokenByTokenAddress } from '../repositories/tokenRepository'; diff --git a/src/routers/oauth2Callbacks.ts b/src/routers/oauth2Callbacks.ts index 634bb803c..2f64b8a43 100644 --- a/src/routers/oauth2Callbacks.ts +++ b/src/routers/oauth2Callbacks.ts @@ -1,5 +1,4 @@ import express, { Request, Response } from 'express'; -import { handleExpressError } from './standardError'; import { logger } from '../utils/logger'; import { SOCIAL_NETWORKS } from '../entities/socialProfile'; import { @@ -7,7 +6,6 @@ import { oauth2CallbackHandler, } from '../services/socialProfileService'; import { findProjectVerificationFormById } from '../repositories/projectVerificationRepository'; -import { getSocialNetworkAdapter } from '../adapters/adaptersFactory'; export const oauth2CallbacksRouter = express.Router(); diff --git a/src/routers/standardError.ts b/src/routers/standardError.ts index 8812ab2dd..6d3529594 100644 --- a/src/routers/standardError.ts +++ b/src/routers/standardError.ts @@ -1,4 +1,3 @@ -import { messages } from '../utils/messages'; import { Response } from 'express'; export class ApiGivStandardError extends Error { diff --git a/src/scripts/findConflicts.ts b/src/scripts/findConflicts.ts index 317bc3ef5..953e33448 100644 --- a/src/scripts/findConflicts.ts +++ b/src/scripts/findConflicts.ts @@ -11,17 +11,16 @@ // values (transactionId, transactionNetworkId, toWalletAddress, fromWalletAddress, currency, anonymous, amount, // userId, projectId, createdAt, valueUsd, valuEth, priceEth, priceUsd, status) +import { writeFileSync } from 'fs'; import axios from 'axios'; -import { getBlockExplorerApiUrl, NETWORK_IDS } from '../provider'; import { Container } from 'typedi'; import * as TypeORM from 'typeorm'; +import { getBlockExplorerApiUrl, NETWORK_IDS } from '../provider'; import { getEntities } from '../entities/entities'; import { Donation } from '../entities/donation'; import { Project } from '../entities/project'; import { sleep } from '../utils/utils'; -import { writeFileSync } from 'fs'; import { findTokenByNetworkAndSymbol } from '../utils/tokenUtils'; -import { isWalletAddressSmartContract } from '../utils/validators/projectValidator'; import { fetchGivHistoricPrice } from '../services/givPriceService'; const EXCLUDED_FROM_ADDRESSES = ['0x0000000000000000000000000000000000000000']; @@ -54,12 +53,12 @@ setupDb().then(async () => { `./src/scripts/transaction${new Date()}.json`, JSON.stringify(missedDonations, null, 4), ); - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('missedDonations ', missedDonations); }); async function setupDb() { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('setupDb connections', { host: process.env.TYPEORM_DATABASE_HOST, }); @@ -88,7 +87,7 @@ async function findMissedDonations() { const walletAddresses = projects.map(project => { return project.walletAddress; }); - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log( 'findMissedDonations walletAddresses.length', walletAddresses.length, @@ -97,7 +96,7 @@ async function findMissedDonations() { let i = 0; for (const project of projects) { i++; - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('project ', { id: project.id, index: i, @@ -107,7 +106,7 @@ async function findMissedDonations() { networkId: NETWORK_IDS.XDAI, project, }); - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('xdaiWalletTransactions', xdaiWalletTransactions); transactions = transactions.concat(xdaiWalletTransactions); @@ -120,7 +119,7 @@ async function findMissedDonations() { './src/scripts/transaction.json', JSON.stringify(transactions, null, 4), ); - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('missed transactions length', transactions.length); } return transactions; @@ -143,7 +142,7 @@ async function getTokenTransfers(input: { offset, networkId, }); - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('getListOfERC20TokenTransfers result', { transactions: userTransactions.walletTransactions.length, walletAddress, @@ -198,7 +197,7 @@ async function checkTransactionWithOurDonations( }; } else if (transaction.timestamp > endTimestamp) { const message = `Token is not is newer than time range ${transaction.tokenSymbol} ${transaction.hash}`; - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('checkTransactionWithOurDonations() message', message); // token is not in time range return { message }; @@ -206,7 +205,7 @@ async function checkTransactionWithOurDonations( !findTokenByNetworkAndSymbol(networkId, transaction.tokenSymbol) ) { const message = `Token is not whitelisted ${transaction.timestamp} ${transaction.hash}`; - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('checkTransactionWithOurDonations() message', message); // token is not whitelisted return { message }; @@ -227,7 +226,7 @@ async function checkTransactionWithOurDonations( }, }); if (!correspondingDonation) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('Transaction is not in our DB ', { hash: transaction.hash, walletAddress: transaction.to, @@ -236,7 +235,7 @@ async function checkTransactionWithOurDonations( }); return { transaction }; } else if (correspondingDonation.status !== 'verified') { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('Transaction is in our DB, but not verified status ', { hash: transaction.hash, statusInOurDb: correspondingDonation.status, @@ -253,7 +252,7 @@ async function checkTransactionWithOurDonations( }; } } catch (e) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('checkTransactionWithOurDonations error', e.message); return { message: e.message, @@ -325,7 +324,7 @@ async function getListOfERC20TokenTransfers(input: { isTransactionListEmpty: result.data.result.length === 0, }; } catch (e) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('getListOfERC20TokenTransfers() error', { error: e.message, input, diff --git a/src/seedToken-ormconfig.ts b/src/seedToken-ormconfig.ts index deb1dc2ea..ababf853f 100644 --- a/src/seedToken-ormconfig.ts +++ b/src/seedToken-ormconfig.ts @@ -1,5 +1,5 @@ -import * as dotenv from 'dotenv'; import * as path from 'path'; +import * as dotenv from 'dotenv'; import { DataSourceOptions } from 'typeorm'; import { getEntities } from './entities/entities'; diff --git a/src/server/adminJs/adminJs.ts b/src/server/adminJs/adminJs.ts index 07aa2022b..f5fff6708 100644 --- a/src/server/adminJs/adminJs.ts +++ b/src/server/adminJs/adminJs.ts @@ -1,11 +1,11 @@ import adminJs, { ActionContext, AdminJSOptions } from 'adminjs'; -import { User } from '../../entities/user'; import adminJsExpress from '@adminjs/express'; +import { Database, Resource } from '@adminjs/typeorm'; +import { IncomingMessage } from 'connect'; +import { User } from '../../entities/user'; import config from '../../config'; import { redis } from '../../redis'; -import { Database, Resource } from '@adminjs/typeorm'; import { logger } from '../../utils/logger'; -import { IncomingMessage } from 'connect'; import { findUserById } from '../../repositories/userRepository'; import { fetchAdminAndValidatePassword } from '../../services/userService'; import { campaignsTab } from './tabs/campaignsTab'; @@ -31,12 +31,12 @@ import { SybilTab } from './tabs/sybilTab'; import { ProjectFraudTab } from './tabs/projectFraudTab'; import { RecurringDonationTab } from './tabs/recurringDonationTab'; // use redis for session data instead of in-memory storage -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const RedisStore = require('connect-redis').default; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const cookie = require('cookie'); -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const cookieParser = require('cookie-parser'); const secret = config.get('ADMIN_BRO_COOKIE_SECRET') as string; const adminJsCookie = 'adminjs'; @@ -82,7 +82,7 @@ export const extractAdminJsReferrerUrlParams = (req: ActionContext) => { // Extract filter names and values from URL query string parameters for (const [key, value] of searchParams.entries()) { - const [_, filter] = key.split('.'); + const [, filter] = key.split('.'); if (!filter) continue; queryStrings[filter] = value; diff --git a/src/server/adminJs/adminJsPermissions.test.ts b/src/server/adminJs/adminJsPermissions.test.ts new file mode 100644 index 000000000..aff23d379 --- /dev/null +++ b/src/server/adminJs/adminJsPermissions.test.ts @@ -0,0 +1,287 @@ +import { assert } from 'chai'; +import { + ResourceActions, + canAccessUserAction, + canAccessCategoryAction, + canAccessMainCategoryAction, + canAccessOrganizationAction, + canAccessProjectAction, + canAccessProjectAddressAction, + canAccessProjectStatusAction, + canAccessProjectStatusHistoryAction, + canAccessProjectUpdateAction, + canAccessQfRoundAction, + canAccessQfRoundHistoryAction, + canAccessDonationAction, + canAccessProjectVerificationFormAction, + canAccessFeaturedUpdateAction, + canAccessThirdPartyProjectImportAction, + canAccessBroadcastNotificationAction, + canAccessProjectStatusReasonAction, + canAccessCampaignAction, + canAccessSybilAction, + canAccessProjectFraudAction, +} from './adminJsPermissions'; +import { UserRole } from '../../entities/user'; + +const roles = Object.freeze([ + UserRole.ADMIN, + UserRole.CAMPAIGN_MANAGER, + UserRole.VERIFICATION_FORM_REVIEWER, + UserRole.OPERATOR, + UserRole.QF_MANAGER, +]); + +const actions = Object.values(ResourceActions); + +// sum up the actions for each role on each page +const actionsPerRole = Object.freeze({ + admin: { + users: ['list', 'new', 'show', 'edit'], + organization: ['list', 'show'], + projectStatusHistory: ['list', 'show'], + campaign: ['list', 'show', 'new', 'edit', 'delete'], + qfRound: ['list', 'show', 'new', 'edit', 'returnAllDonationData'], + qfRoundHistory: [ + 'list', + 'show', + 'edit', + 'delete', + 'bulkDelete', + 'updateQfRoundHistories', + 'relateDonationsWithDistributedFunds', + ], + projectStatusReason: ['list', 'show', 'new', 'edit'], + projectAddress: ['list', 'show', 'new', 'edit', 'delete', 'bulkDelete'], + projectStatus: ['list', 'show', 'edit'], + project: [ + 'list', + 'show', + 'edit', + 'exportFilterToCsv', + 'listProject', + 'unlistProject', + 'verifyProject', + 'rejectProject', + 'revokeBadge', + 'activateProject', + 'deactivateProject', + 'cancelProject', + 'addToQfRound', + 'removeFromQfRound', + ], + projectUpdate: ['list', 'show', 'addFeaturedProjectUpdate'], + thirdPartyProjectImport: [ + 'list', + 'show', + 'new', + 'edit', + 'delete', + 'bulkDelete', + ], + featuredUpdate: ['list', 'show', 'new', 'edit', 'delete', 'bulkDelete'], + donation: ['list', 'show', 'new', 'edit', 'delete', 'exportFilterToCsv'], + projectVerificationForm: [ + 'list', + 'delete', + 'edit', + 'show', + 'verifyProject', + 'makeEditableByUser', + 'rejectProject', + 'verifyProjects', + 'rejectProjects', + ], + mainCategory: ['list', 'show', 'new', 'edit'], + category: ['list', 'show', 'new', 'edit'], + broadcastNotification: ['list', 'show', 'new'], + sybil: ['list', 'show', 'new', 'edit', 'delete', 'bulkDelete'], + projectFraud: ['list', 'show', 'new', 'edit', 'delete', 'bulkDelete'], + // recurringDonation: ['list', 'show', 'new', 'edit', 'delete', 'bulkDelete'], + }, + campaignManager: { + users: ['list', 'show'], + organization: ['list', 'show'], + projectStatusHistory: ['list', 'show'], + campaign: ['list', 'show', 'new', 'edit'], + qfRound: ['list', 'show'], + qfRoundHistory: ['list', 'show'], + projectStatusReason: ['list', 'show'], + projectAddress: ['list', 'show'], + projectStatus: ['list', 'show'], + project: ['list', 'show'], + projectUpdate: ['list', 'show'], + thirdPartyProjectImport: ['list', 'show'], + featuredUpdate: ['list', 'show'], + donation: ['list', 'show'], + projectVerificationForm: ['list', 'show'], + mainCategory: ['list', 'show'], + category: ['list', 'show'], + broadcastNotification: ['list', 'show'], + sybil: ['list', 'show'], + projectFraud: ['list', 'show'], + // recurringDonation: ['list', 'show'], + }, + reviewer: { + users: ['list', 'show'], + organization: ['list', 'show'], + projectStatusHistory: ['list', 'show'], + campaign: ['list', 'show'], + qfRound: ['list', 'show'], + qfRoundHistory: ['list', 'show'], + projectStatusReason: ['list', 'show'], + projectAddress: ['list', 'show'], + projectStatus: ['list', 'show'], + project: [ + 'list', + 'show', + 'exportFilterToCsv', + 'listProject', + 'unlistProject', + 'verifyProject', + 'rejectProject', + 'revokeBadge', + 'activateProject', + 'deactivateProject', + 'cancelProject', + ], + projectUpdate: ['list', 'show'], + thirdPartyProjectImport: ['list', 'show'], + featuredUpdate: ['list', 'show'], + donation: ['list', 'show', 'exportFilterToCsv'], + projectVerificationForm: [ + 'list', + 'delete', + 'edit', + 'show', + 'verifyProject', + 'makeEditableByUser', + 'rejectProject', + 'verifyProjects', + 'rejectProjects', + ], + mainCategory: ['list', 'show'], + category: ['list', 'show'], + broadcastNotification: ['list', 'show'], + sybil: ['list', 'show'], + projectFraud: ['list', 'show'], + // recurringDonation: ['list', 'show'], + }, + operator: { + users: ['list', 'show'], + organization: ['list', 'show'], + projectStatusHistory: ['list', 'show'], + campaign: ['list', 'show'], + qfRound: ['list', 'show'], + qfRoundHistory: ['list', 'show'], + projectStatusReason: ['list', 'show'], + projectAddress: ['list', 'show'], + projectStatus: ['list', 'show'], + project: [ + 'list', + 'show', + 'exportFilterToCsv', + 'listProject', + 'unlistProject', + 'verifyProject', + 'rejectProject', + 'revokeBadge', + 'activateProject', + 'deactivateProject', + 'cancelProject', + ], + projectUpdate: ['list', 'show', 'addFeaturedProjectUpdate'], + thirdPartyProjectImport: ['list', 'show'], + featuredUpdate: ['list', 'show'], + donation: ['list', 'show', 'exportFilterToCsv'], + projectVerificationForm: ['list', 'show'], + mainCategory: ['list', 'show'], + category: ['list', 'show'], + broadcastNotification: ['list', 'show'], + sybil: ['list', 'show'], + projectFraud: ['list', 'show'], + // recurringDonation: ['list', 'show'], + }, + qfManager: { + qfRound: ['list', 'show', 'edit', 'new', 'returnAllDonationData'], + qfRoundHistory: ['list', 'show', 'updateQfRoundHistories'], + project: [ + 'list', + 'show', + 'listProject', + 'addToQfRound', + 'removeFromQfRound', + ], + sybil: ['list', 'show', 'new', 'edit', 'delete', 'bulkDelete'], + projectFraud: ['list', 'show', 'new', 'edit', 'delete', 'bulkDelete'], + }, +}); + +const canAccessAction = ( + role: UserRole, + page: string, + action: ResourceActions, +): boolean => { + const args = { currentAdmin: { role } }; + + switch (page) { + case 'users': + return canAccessUserAction(args, action); + case 'organization': + return canAccessOrganizationAction(args, action); + case 'projectStatusHistory': + return canAccessProjectStatusHistoryAction(args, action); + case 'campaign': + return canAccessCampaignAction(args, action); + case 'qfRound': + return canAccessQfRoundAction(args, action); + case 'qfRoundHistory': + return canAccessQfRoundHistoryAction(args, action); + case 'projectStatusReason': + return canAccessProjectStatusReasonAction(args, action); + case 'projectAddress': + return canAccessProjectAddressAction(args, action); + case 'projectStatus': + return canAccessProjectStatusAction(args, action); + case 'project': + return canAccessProjectAction(args, action); + case 'projectUpdate': + return canAccessProjectUpdateAction(args, action); + case 'thirdPartyProjectImport': + return canAccessThirdPartyProjectImportAction(args, action); + case 'featuredUpdate': + return canAccessFeaturedUpdateAction(args, action); + case 'donation': + return canAccessDonationAction(args, action); + case 'projectVerificationForm': + return canAccessProjectVerificationFormAction(args, action); + case 'mainCategory': + return canAccessMainCategoryAction(args, action); + case 'category': + return canAccessCategoryAction(args, action); + case 'broadcastNotification': + return canAccessBroadcastNotificationAction(args, action); + case 'sybil': + return canAccessSybilAction(args, action); + case 'projectFraud': + return canAccessProjectFraudAction(args, action); + default: + return false; + } +}; + +// 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', () => { + roles.forEach(role => { + Object.keys(actionsPerRole[role]).forEach(page => { + actions.forEach(action => { + it(`should return ${actionsPerRole[role][page].includes(action)} for ${role} --> ${action} on ${page}`, function () { + assert.strictEqual( + canAccessAction(role, page, action), + actionsPerRole[role][page].includes(action), + ); + }); + }); + }); + }); +}); diff --git a/src/server/adminJs/adminJsPermissions.ts b/src/server/adminJs/adminJsPermissions.ts index 7a51f5779..b660b185b 100644 --- a/src/server/adminJs/adminJsPermissions.ts +++ b/src/server/adminJs/adminJsPermissions.ts @@ -2,6 +2,7 @@ import { UserRole } from '../../entities/user'; // Current Actions Enum export enum ResourceActions { + LIST = 'list', DELETE = 'delete', NEW = 'new', SHOW = 'show', @@ -23,20 +24,26 @@ export enum ResourceActions { ADD_PROJECT_TO_QF_ROUND = 'addToQfRound', REMOVE_PROJECT_FROM_QF_ROUND = 'removeFromQfRound', UPDATE_QF_ROUND_HISTORIES = 'updateQfRoundHistories', + RETURN_ALL_DONATIONS_DATA = 'returnAllDonationData', + RELATE_DONATIONS_WITH_DISTRIBUTED_FUNDS = 'relateDonationsWithDistributedFunds', } // All permissions listed per resource, per role and action const organizationPermissions = { [UserRole.ADMIN]: { + list: true, show: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -44,17 +51,21 @@ const organizationPermissions = { const userPermissions = { [UserRole.ADMIN]: { + list: true, new: true, show: true, edit: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -62,15 +73,19 @@ const userPermissions = { const projectStatusHistoryPermissions = { [UserRole.ADMIN]: { + list: true, show: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -78,18 +93,22 @@ const projectStatusHistoryPermissions = { const campaignPermissions = { [UserRole.ADMIN]: { + list: true, delete: true, new: true, show: true, edit: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, new: true, show: true, edit: true, @@ -99,60 +118,81 @@ const campaignPermissions = { const qfRoundPermissions = { [UserRole.ADMIN]: { - delete: true, - new: true, + list: true, show: true, + returnAllDonationData: true, + new: true, edit: true, - addToQfRound: true, - removeFromQfRound: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, - addToQfRound: true, - removeFromQfRound: true, + }, + [UserRole.QF_MANAGER]: { + list: true, + show: true, + returnAllDonationData: true, + new: true, + edit: true, }, // Add more roles here as needed }; const qfRoundHistoryPermissions = { [UserRole.ADMIN]: { + list: true, delete: true, bulkDelete: true, show: true, edit: true, updateQfRoundHistories: true, + relateDonationsWithDistributedFunds: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, + show: true, + }, + [UserRole.QF_MANAGER]: { + list: true, show: true, + updateQfRoundHistories: true, }, // Add more roles here as needed }; const projectStatusReasonPermissions = { [UserRole.ADMIN]: { + list: true, new: true, show: true, edit: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -160,6 +200,7 @@ const projectStatusReasonPermissions = { const projectAddressPermissions = { [UserRole.ADMIN]: { + list: true, delete: true, new: true, show: true, @@ -167,12 +208,15 @@ const projectAddressPermissions = { bulkDelete: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -180,16 +224,20 @@ const projectAddressPermissions = { const projectStatusPermissions = { [UserRole.ADMIN]: { + list: true, show: true, edit: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -197,6 +245,7 @@ const projectStatusPermissions = { const projectPermissions = { [UserRole.ADMIN]: { + list: true, show: true, edit: true, exportFilterToCsv: true, @@ -208,8 +257,11 @@ const projectPermissions = { activateProject: true, deactivateProject: true, cancelProject: true, + addToQfRound: true, + removeFromQfRound: true, }, [UserRole.OPERATOR]: { + list: true, show: true, exportFilterToCsv: true, listProject: true, @@ -222,6 +274,7 @@ const projectPermissions = { cancelProject: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, exportFilterToCsv: true, listProject: true, @@ -234,24 +287,36 @@ const projectPermissions = { cancelProject: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, + [UserRole.QF_MANAGER]: { + list: true, + show: true, + listProject: true, + removeFromQfRound: true, + addToQfRound: true, + }, // Add more roles here as needed }; const projectUpdatePermissions = { [UserRole.ADMIN]: { + list: true, show: true, addFeaturedProjectUpdate: true, }, [UserRole.OPERATOR]: { + list: true, show: true, addFeaturedProjectUpdate: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -259,6 +324,7 @@ const projectUpdatePermissions = { const thirdPartyProjectImportPermissions = { [UserRole.ADMIN]: { + list: true, delete: true, new: true, show: true, @@ -266,12 +332,15 @@ const thirdPartyProjectImportPermissions = { bulkDelete: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -279,6 +348,7 @@ const thirdPartyProjectImportPermissions = { const featuredUpdatePermissions = { [UserRole.ADMIN]: { + list: true, delete: true, new: true, show: true, @@ -286,28 +356,15 @@ const featuredUpdatePermissions = { bulkDelete: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { - show: true, - }, - // Add more roles here as needed -}; - -const tokenPermissions = { - [UserRole.ADMIN]: { - delete: true, - new: true, - show: true, - edit: true, - }, - [UserRole.VERIFICATION_FORM_REVIEWER]: { - show: true, - }, - [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -315,6 +372,7 @@ const tokenPermissions = { const donationPermissions = { [UserRole.ADMIN]: { + list: true, delete: true, new: true, show: true, @@ -322,14 +380,17 @@ const donationPermissions = { exportFilterToCsv: true, }, [UserRole.OPERATOR]: { + list: true, show: true, exportFilterToCsv: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, exportFilterToCsv: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -337,6 +398,7 @@ const donationPermissions = { const projectVerificationFormPermissions = { [UserRole.ADMIN]: { + list: true, delete: true, edit: true, show: true, @@ -347,9 +409,11 @@ const projectVerificationFormPermissions = { rejectProjects: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, delete: true, edit: true, show: true, @@ -360,25 +424,29 @@ const projectVerificationFormPermissions = { rejectProjects: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, - verifyProject: true, }, // Add more roles here as needed }; const mainCategoryPermissions = { [UserRole.ADMIN]: { + list: true, new: true, show: true, edit: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -386,17 +454,21 @@ const mainCategoryPermissions = { const categoryPermissions = { [UserRole.ADMIN]: { + list: true, new: true, show: true, edit: true, }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -404,16 +476,109 @@ const categoryPermissions = { const broadcastNotificationPermissions = { [UserRole.ADMIN]: { + list: true, + new: true, + show: true, + }, + [UserRole.OPERATOR]: { + list: true, + show: true, + }, + [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, + show: true, + }, + [UserRole.CAMPAIGN_MANAGER]: { + list: true, + show: true, + }, + // Add more roles here as needed +}; + +const projectFraudPermissions = { + [UserRole.ADMIN]: { + list: true, + show: true, + new: true, + edit: true, + delete: true, + bulkDelete: true, + }, + [UserRole.OPERATOR]: { + list: true, + show: true, + }, + [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, + show: true, + }, + [UserRole.CAMPAIGN_MANAGER]: { + list: true, + show: true, + }, + [UserRole.QF_MANAGER]: { + list: true, + show: true, + new: true, + edit: true, + delete: true, + bulkDelete: true, + }, + // Add more roles here as needed +}; + +const sybilPermissions = { + [UserRole.ADMIN]: { + list: true, + show: true, new: true, + edit: true, + delete: true, + bulkDelete: true, + }, + [UserRole.OPERATOR]: { + list: true, + show: true, + }, + [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, + show: true, + }, + [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, + [UserRole.QF_MANAGER]: { + list: true, + show: true, + new: true, + edit: true, + delete: true, + bulkDelete: true, + }, + // Add more roles here as needed +}; + +// will be modified later on +const recurringDonationPermissions = { + [UserRole.ADMIN]: { + list: true, + show: true, + new: true, + edit: true, + delete: true, + bulkDelete: true, + }, [UserRole.OPERATOR]: { + list: true, show: true, }, [UserRole.VERIFICATION_FORM_REVIEWER]: { + list: true, show: true, }, [UserRole.CAMPAIGN_MANAGER]: { + list: true, show: true, }, // Add more roles here as needed @@ -423,7 +588,7 @@ const hasAccessToResource = (params: { currentAdmin: any; action: string; resourcePermissions: any; -}): Boolean => { +}): boolean => { const { currentAdmin, action, resourcePermissions } = params; if (!currentAdmin) return false; @@ -623,3 +788,33 @@ export const canAccessCampaignAction = ({ currentAdmin }, action: string) => { resourcePermissions: campaignPermissions, }); }; + +export const canAccessProjectFraudAction = ( + { currentAdmin }, + action: string, +) => { + return hasAccessToResource({ + currentAdmin, + action, + resourcePermissions: projectFraudPermissions, + }); +}; + +export const canAccessSybilAction = ({ currentAdmin }, action: string) => { + return hasAccessToResource({ + currentAdmin, + action, + resourcePermissions: sybilPermissions, + }); +}; + +export const canAccessRecurringDonationAction = ( + { currentAdmin }, + action: string, +) => { + return hasAccessToResource({ + currentAdmin, + action, + resourcePermissions: recurringDonationPermissions, + }); +}; diff --git a/src/server/adminJs/tabs/broadcastNorificationTab.test.ts b/src/server/adminJs/tabs/broadcastNorificationTab.test.ts index 3ed5da109..109915378 100644 --- a/src/server/adminJs/tabs/broadcastNorificationTab.test.ts +++ b/src/server/adminJs/tabs/broadcastNorificationTab.test.ts @@ -1,7 +1,7 @@ +import { assert } from 'chai'; import BroadcastNotification, { BROAD_CAST_NOTIFICATION_STATUS, } from '../../../entities/broadcastNotification'; -import { assert } from 'chai'; import { findBroadcastNotificationById } from '../../../repositories/broadcastNotificationRepository'; import { sendBroadcastNotification } from './broadcastNotificationTab'; diff --git a/src/server/adminJs/tabs/broadcastNotificationTab.ts b/src/server/adminJs/tabs/broadcastNotificationTab.ts index bf3990880..3b2177a58 100644 --- a/src/server/adminJs/tabs/broadcastNotificationTab.ts +++ b/src/server/adminJs/tabs/broadcastNotificationTab.ts @@ -1,3 +1,9 @@ +import adminJs from 'adminjs'; +import { + ActionResponse, + After, +} from 'adminjs/src/backend/actions/action.interface'; +import { RecordJSON } from 'adminjs/src/frontend/interfaces/record-json.interface'; import BroadcastNotification, { BROAD_CAST_NOTIFICATION_STATUS, } from '../../../entities/broadcastNotification'; @@ -5,16 +11,10 @@ import { canAccessBroadcastNotificationAction, ResourceActions, } from '../adminJsPermissions'; -import adminJs from 'adminjs'; import { AdminJsContextInterface, AdminJsRequestInterface, } from '../adminJs-types'; -import { - ActionResponse, - After, -} from 'adminjs/src/backend/actions/action.interface'; -import { RecordJSON } from 'adminjs/src/frontend/interfaces/record-json.interface'; import { getNotificationAdapter } from '../../../adapters/adaptersFactory'; import { updateBroadcastNotificationStatus } from '../../../repositories/broadcastNotificationRepository'; import { logger } from '../../../utils/logger'; @@ -24,7 +24,7 @@ export const sendBroadcastNotification = async ( ): Promise> => { const record: RecordJSON = response.record || {}; if (record?.params) { - const { html, id } = record?.params; + const { html, id } = record?.params || {}; try { await getNotificationAdapter().broadcastNotification({ broadCastNotificationId: id, @@ -49,6 +49,20 @@ export const broadcastNotificationTab = { resource: BroadcastNotification, options: { actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessBroadcastNotificationAction( + { currentAdmin }, + ResourceActions.LIST, + ), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessBroadcastNotificationAction( + { currentAdmin }, + ResourceActions.SHOW, + ), + }, delete: { isVisible: false, isAccessible: ({ currentAdmin }) => diff --git a/src/server/adminJs/tabs/campaignsTab.ts b/src/server/adminJs/tabs/campaignsTab.ts index 28d5636fb..4e25c8ede 100644 --- a/src/server/adminJs/tabs/campaignsTab.ts +++ b/src/server/adminJs/tabs/campaignsTab.ts @@ -8,6 +8,14 @@ export const campaignsTab = { resource: Campaign, options: { actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessCampaignAction({ currentAdmin }, ResourceActions.LIST), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessCampaignAction({ currentAdmin }, ResourceActions.SHOW), + }, delete: { isVisible: true, isAccessible: ({ currentAdmin }) => diff --git a/src/server/adminJs/tabs/categoryTab.ts b/src/server/adminJs/tabs/categoryTab.ts index 637882ee3..39cf01341 100644 --- a/src/server/adminJs/tabs/categoryTab.ts +++ b/src/server/adminJs/tabs/categoryTab.ts @@ -8,6 +8,14 @@ export const categoryTab = { resource: Category, options: { actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessCategoryAction({ currentAdmin }, ResourceActions.LIST), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessCategoryAction({ currentAdmin }, ResourceActions.SHOW), + }, delete: { isVisible: false, isAccessible: ({ currentAdmin }) => diff --git a/src/server/adminJs/tabs/donationTab.test.ts b/src/server/adminJs/tabs/donationTab.test.ts index c997bf831..587bbb8d7 100644 --- a/src/server/adminJs/tabs/donationTab.test.ts +++ b/src/server/adminJs/tabs/donationTab.test.ts @@ -1,17 +1,14 @@ +import { assert } from 'chai'; import { createProjectData, saveProjectDirectlyToDb, - SEED_DATA, } from '../../../../test/testUtils'; -import { findUserById } from '../../../repositories/userRepository'; import { NETWORK_IDS } from '../../../provider'; -import { User } from '../../../entities/user'; import { Donation, DONATION_STATUS, DONATION_TYPES, } from '../../../entities/donation'; -import { assert } from 'chai'; import { createDonation } from './donationTab'; describe('createDonation() test cases', createDonationTestCases); @@ -53,7 +50,6 @@ function createDonationTestCases() { ...createProjectData(), walletAddress: sixthProjectAddress, }); - const adminUser = await findUserById(SEED_DATA.ADMIN_USER.id); await createDonation( { query: { @@ -72,12 +68,6 @@ function createDonationTestCases() { // }, }, - { - currentAdmin: adminUser as User, - h: {}, - resource: {}, - records: [], - }, ); const firstDonation = await Donation.findOne({ @@ -168,7 +158,6 @@ function createDonationTestCases() { ...createProjectData(), walletAddress: firstProjectAddress, }); - const adminUser = await findUserById(SEED_DATA.ADMIN_USER.id); await createDonation( { query: { @@ -188,12 +177,6 @@ function createDonationTestCases() { // }, }, - { - currentAdmin: adminUser as User, - h: {}, - resource: {}, - records: [], - }, ); const firstDonation = await Donation.findOne({ diff --git a/src/server/adminJs/tabs/donationTab.ts b/src/server/adminJs/tabs/donationTab.ts index 79e41100b..5e4574756 100644 --- a/src/server/adminJs/tabs/donationTab.ts +++ b/src/server/adminJs/tabs/donationTab.ts @@ -1,3 +1,5 @@ +import { SelectQueryBuilder } from 'typeorm'; +import { ActionContext } from 'adminjs'; import { Donation, DONATION_STATUS, @@ -29,14 +31,11 @@ import { calculateGivbackFactor } from '../../../services/givbackService'; import { findUserByWalletAddress } from '../../../repositories/userRepository'; import { updateTotalDonationsOfProject } from '../../../services/donationService'; import { updateUserTotalDonated } from '../../../services/userService'; -import { NETWORK_IDS, NETWORKS_IDS_TO_NAME } from '../../../provider'; -import { redis } from '../../../redis'; +import { NETWORK_IDS } from '../../../provider'; import { initExportSpreadsheet, addDonationsSheetToSpreadsheet, } from '../../../services/googleSheets'; -import { SelectQueryBuilder } from 'typeorm'; -import { ActionContext } from 'adminjs'; import { extractAdminJsReferrerUrlParams } from '../adminJs'; import { getTwitterDonations } from '../../../services/Idriss/contractDonations'; import { @@ -47,7 +46,6 @@ import { export const createDonation = async ( request: AdminJsRequestInterface, response, - context: AdminJsContextInterface, ) => { let message = messages.DONATION_CREATED_SUCCESSFULLY; const donations: Donation[] = []; @@ -676,6 +674,14 @@ export const donationTab = { }, }, actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessDonationAction({ currentAdmin }, ResourceActions.LIST), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessDonationAction({ currentAdmin }, ResourceActions.SHOW), + }, bulkDelete: { isVisible: false, isAccessible: ({ currentAdmin }) => diff --git a/src/server/adminJs/tabs/featuredUpdateTab.ts b/src/server/adminJs/tabs/featuredUpdateTab.ts index 33fef62bb..0d3f82aa0 100644 --- a/src/server/adminJs/tabs/featuredUpdateTab.ts +++ b/src/server/adminJs/tabs/featuredUpdateTab.ts @@ -30,6 +30,10 @@ export const featuredUpdateTab = { ResourceActions.BULK_DELETE, ), }, + list: { + isAccessible: ({ currentAdmin }) => + canAccessFeaturedUpdateAction({ currentAdmin }, ResourceActions.LIST), + }, show: { isVisible: true, isAccessible: ({ currentAdmin }) => diff --git a/src/server/adminJs/tabs/mainCategoryTab.ts b/src/server/adminJs/tabs/mainCategoryTab.ts index 34e8df74d..2cc7b833c 100644 --- a/src/server/adminJs/tabs/mainCategoryTab.ts +++ b/src/server/adminJs/tabs/mainCategoryTab.ts @@ -8,6 +8,14 @@ export const mainCategoryTab = { resource: MainCategory, options: { actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessMainCategoryAction({ currentAdmin }, ResourceActions.LIST), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessMainCategoryAction({ currentAdmin }, ResourceActions.SHOW), + }, delete: { isVisible: false, isAccessible: ({ currentAdmin }) => diff --git a/src/server/adminJs/tabs/organizationsTab.ts b/src/server/adminJs/tabs/organizationsTab.ts index dfc1822d4..1bc114106 100644 --- a/src/server/adminJs/tabs/organizationsTab.ts +++ b/src/server/adminJs/tabs/organizationsTab.ts @@ -8,6 +8,14 @@ export const organizationsTab = { resource: Organization, options: { actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessOrganizationAction({ currentAdmin }, ResourceActions.LIST), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessOrganizationAction({ currentAdmin }, ResourceActions.SHOW), + }, delete: { isVisible: false, isAccessible: ({ currentAdmin }) => diff --git a/src/server/adminJs/tabs/projectAddressTab.ts b/src/server/adminJs/tabs/projectAddressTab.ts index 6ac6ef9d4..f2d6f7e2d 100644 --- a/src/server/adminJs/tabs/projectAddressTab.ts +++ b/src/server/adminJs/tabs/projectAddressTab.ts @@ -8,6 +8,14 @@ export const projectAddressTab = { resource: ProjectAddress, options: { actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessProjectAddressAction({ currentAdmin }, ResourceActions.LIST), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessProjectAddressAction({ currentAdmin }, ResourceActions.SHOW), + }, new: { isAccessible: ({ currentAdmin }) => canAccessProjectAddressAction({ currentAdmin }, ResourceActions.NEW), diff --git a/src/server/adminJs/tabs/projectFraudTab.test.ts b/src/server/adminJs/tabs/projectFraudTab.test.ts index 2db67b936..9fea0e9a2 100644 --- a/src/server/adminJs/tabs/projectFraudTab.test.ts +++ b/src/server/adminJs/tabs/projectFraudTab.test.ts @@ -1,3 +1,5 @@ +import moment from 'moment'; +import { assert } from 'chai'; import { createProjectData, generateRandomEtheriumAddress, @@ -5,9 +7,6 @@ import { saveUserDirectlyToDb, } from '../../../../test/testUtils'; import { QfRound } from '../../../entities/qfRound'; -import moment from 'moment'; -import { createSybil } from './sybilTab'; -import { assert } from 'chai'; import { createProjectFraud } from './projectFraudTab'; import { errorMessages } from '../../../utils/errorMessages'; diff --git a/src/server/adminJs/tabs/projectFraudTab.ts b/src/server/adminJs/tabs/projectFraudTab.ts index 5bdd7cd6d..7b886f092 100644 --- a/src/server/adminJs/tabs/projectFraudTab.ts +++ b/src/server/adminJs/tabs/projectFraudTab.ts @@ -1,13 +1,10 @@ +import csv from 'csvtojson'; import { - canAccessProjectStatusReasonAction, + canAccessProjectFraudAction, ResourceActions, } from '../adminJsPermissions'; -import { - AdminJsContextInterface, - AdminJsRequestInterface, -} from '../adminJs-types'; +import { AdminJsRequestInterface } from '../adminJs-types'; import { logger } from '../../../utils/logger'; -import csv from 'csvtojson'; import { messages } from '../../../utils/messages'; import { ProjectFraud } from '../../../entities/projectFraud'; import { errorMessages } from '../../../utils/errorMessages'; @@ -15,7 +12,6 @@ import { errorMessages } from '../../../utils/errorMessages'; export const createProjectFraud = async ( request: AdminJsRequestInterface, response, - context?: AdminJsContextInterface, ) => { let message = messages.PROJECT_FRAUD_HAS_BEEN_CREATED_SUCCESSFULLY; logger.debug('createProjectFraud has been called() ', request.payload); @@ -37,7 +33,6 @@ export const createProjectFraud = async ( } slugs.push(obj.slug.toLowerCase()); }); - const uniqueSlugs = [...new Set(slugs)]; // Get projectIds for all slugs const projects = await ProjectFraud.query(` @@ -131,31 +126,19 @@ export const ProjectFraudTab = { handler: createProjectFraud, isAccessible: ({ currentAdmin }) => - canAccessProjectStatusReasonAction( - { currentAdmin }, - ResourceActions.NEW, - ), + canAccessProjectFraudAction({ currentAdmin }, ResourceActions.NEW), }, edit: { isAccessible: ({ currentAdmin }) => - canAccessProjectStatusReasonAction( - { currentAdmin }, - ResourceActions.EDIT, - ), + canAccessProjectFraudAction({ currentAdmin }, ResourceActions.EDIT), }, delete: { isAccessible: ({ currentAdmin }) => - canAccessProjectStatusReasonAction( - { currentAdmin }, - ResourceActions.EDIT, - ), + canAccessProjectFraudAction({ currentAdmin }, ResourceActions.DELETE), }, bulkDelete: { isAccessible: ({ currentAdmin }) => - canAccessProjectStatusReasonAction( - { currentAdmin }, - ResourceActions.EDIT, - ), + canAccessProjectFraudAction({ currentAdmin }, ResourceActions.EDIT), }, }, }, diff --git a/src/server/adminJs/tabs/projectStatusHistoryTab.ts b/src/server/adminJs/tabs/projectStatusHistoryTab.ts index a4c111446..5693ca6f1 100644 --- a/src/server/adminJs/tabs/projectStatusHistoryTab.ts +++ b/src/server/adminJs/tabs/projectStatusHistoryTab.ts @@ -8,6 +8,20 @@ export const projectStatusHistoryTab = { resource: ProjectStatusHistory, options: { actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessProjectStatusHistoryAction( + { currentAdmin }, + ResourceActions.LIST, + ), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessProjectStatusHistoryAction( + { currentAdmin }, + ResourceActions.SHOW, + ), + }, delete: { isVisible: false, isAccessible: ({ currentAdmin }) => diff --git a/src/server/adminJs/tabs/projectStatusReasonTab.ts b/src/server/adminJs/tabs/projectStatusReasonTab.ts index 1bd02cc64..ccfd1c0c7 100644 --- a/src/server/adminJs/tabs/projectStatusReasonTab.ts +++ b/src/server/adminJs/tabs/projectStatusReasonTab.ts @@ -8,6 +8,20 @@ export const projectStatusReasonTab = { resource: ProjectStatusReason, options: { actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessProjectStatusReasonAction( + { currentAdmin }, + ResourceActions.LIST, + ), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessProjectStatusReasonAction( + { currentAdmin }, + ResourceActions.SHOW, + ), + }, new: { isAccessible: ({ currentAdmin }) => canAccessProjectStatusReasonAction( diff --git a/src/server/adminJs/tabs/projectStatusTab.ts b/src/server/adminJs/tabs/projectStatusTab.ts index f120814c5..4145dc92a 100644 --- a/src/server/adminJs/tabs/projectStatusTab.ts +++ b/src/server/adminJs/tabs/projectStatusTab.ts @@ -8,6 +8,14 @@ export const projectStatusTab = { resource: ProjectStatus, options: { actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessProjectStatusAction({ currentAdmin }, ResourceActions.LIST), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessProjectStatusAction({ currentAdmin }, ResourceActions.SHOW), + }, delete: { isVisible: false, isAccessible: ({ currentAdmin }) => diff --git a/src/server/adminJs/tabs/projectUpdateTab.ts b/src/server/adminJs/tabs/projectUpdateTab.ts index a9e510d7d..1882f088d 100644 --- a/src/server/adminJs/tabs/projectUpdateTab.ts +++ b/src/server/adminJs/tabs/projectUpdateTab.ts @@ -1,10 +1,10 @@ +import { ActionResponse, After } from 'adminjs'; import { Project, ProjectUpdate } from '../../../entities/project'; import { canAccessProjectUpdateAction, ResourceActions, } from '../adminJsPermissions'; import { addFeaturedProjectUpdate } from './projectsTab'; -import { ActionResponse, After } from 'adminjs'; export const setProjectsTitleAndSlug: After = async request => { if (Number(request?.records?.length) > 0) { @@ -249,6 +249,8 @@ export const projectUpdateTab = { }, list: { isVisible: true, + isAccessible: ({ currentAdmin }) => + canAccessProjectUpdateAction({ currentAdmin }, ResourceActions.LIST), after: setProjectsTitleAndSlug, }, addFeaturedProjectUpdate: { diff --git a/src/server/adminJs/tabs/projectVerificationTab.ts b/src/server/adminJs/tabs/projectVerificationTab.ts index c1a942ade..2c6a3f4ca 100644 --- a/src/server/adminJs/tabs/projectVerificationTab.ts +++ b/src/server/adminJs/tabs/projectVerificationTab.ts @@ -1,8 +1,13 @@ +import adminJs from 'adminjs'; +import { RecordJSON } from 'adminjs/src/frontend/interfaces/record-json.interface'; +import { + ActionResponse, + After, +} from 'adminjs/src/backend/actions/action.interface'; import { PROJECT_VERIFICATION_STATUSES, ProjectVerificationForm, } from '../../../entities/projectVerificationForm'; -import adminJs from 'adminjs'; import { canAccessProjectVerificationFormAction, ResourceActions, @@ -22,7 +27,6 @@ import { i18n, translationErrorMessagesKeys, } from '../../../utils/errorMessages'; -import { NOTIFICATIONS_EVENT_NAMES } from '../../../analytics/analytics'; import { findProjectById, updateProjectWithVerificationForm, @@ -31,12 +35,7 @@ import { } from '../../../repositories/projectRepository'; import { getNotificationAdapter } from '../../../adapters/adaptersFactory'; import { logger } from '../../../utils/logger'; -import { RecordJSON } from 'adminjs/src/frontend/interfaces/record-json.interface'; import { Project } from '../../../entities/project'; -import { - ActionResponse, - After, -} from 'adminjs/src/backend/actions/action.interface'; import { fillSocialProfileAndQfRounds } from './projectsTab'; export const setCommentEmailAndTimeStamps: After = async ( @@ -72,7 +71,7 @@ export const verifySingleVerificationForm = async ( request: AdminJsRequestInterface, verified: boolean, ) => { - const { records, currentAdmin } = context; + const { currentAdmin } = context; let responseMessage = ''; let responseType = 'success'; const verificationStatus = verified @@ -105,10 +104,6 @@ export const verifySingleVerificationForm = async ( ), ); } - // call repositories - const segmentEvent = verified - ? NOTIFICATIONS_EVENT_NAMES.PROJECT_VERIFIED - : NOTIFICATIONS_EVENT_NAMES.PROJECT_REJECTED; const verificationForm = await verifyForm({ verificationStatus, @@ -164,7 +159,7 @@ export const makeEditableByUser = async ( context: AdminJsContextInterface, request: AdminJsRequestInterface, ) => { - const { records, currentAdmin } = context; + const { currentAdmin } = context; let responseMessage = ''; let responseType = 'success'; const formId = Number(request?.params?.recordId); @@ -571,6 +566,13 @@ export const projectVerificationTab = { isVisible: true, after: setCommentEmailAndTimeStamps, }, + list: { + isAccessible: ({ currentAdmin }) => + canAccessProjectVerificationFormAction( + { currentAdmin }, + ResourceActions.LIST, + ), + }, show: { isVisible: true, isAccessible: ({ currentAdmin }) => diff --git a/src/server/adminJs/tabs/projectsTab.test.ts b/src/server/adminJs/tabs/projectsTab.test.ts index 99afe0c43..508f5eeb1 100644 --- a/src/server/adminJs/tabs/projectsTab.test.ts +++ b/src/server/adminJs/tabs/projectsTab.test.ts @@ -1,3 +1,4 @@ +import { assert } from 'chai'; import { createProjectData, generateRandomEtheriumAddress, @@ -14,7 +15,6 @@ import { RevokeSteps, } from '../../../entities/project'; import { User } from '../../../entities/user'; -import { assert } from 'chai'; import { findOneProjectStatusHistory } from '../../../repositories/projectSatusHistoryRepository'; import { HISTORY_DESCRIPTIONS } from '../../../entities/projectStatusHistory'; import { diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index 677549ee2..fecb8aed3 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -1,3 +1,10 @@ +import adminJs from 'adminjs'; +import { SelectQueryBuilder } from 'typeorm'; +import { + ActionResponse, + After, +} from 'adminjs/src/backend/actions/action.interface'; +import { RecordJSON } from 'adminjs/src/frontend/interfaces/record-json.interface'; import { Project, ProjectUpdate, @@ -5,12 +12,7 @@ import { ReviewStatus, RevokeSteps, } from '../../../entities/project'; -import adminJs, { ActionContext } from 'adminjs'; -import { - canAccessProjectAction, - canAccessQfRoundAction, - ResourceActions, -} from '../adminJsPermissions'; +import { canAccessProjectAction, ResourceActions } from '../adminJsPermissions'; import { findProjectById, findProjectsByIdArray, @@ -24,14 +26,7 @@ import { refreshProjectFuturePowerView, refreshProjectPowerView, } from '../../../repositories/projectPowerViewRepository'; -import { redis } from '../../../redis'; -import { SelectQueryBuilder } from 'typeorm'; import { logger } from '../../../utils/logger'; -import { - ActionResponse, - After, -} from 'adminjs/src/backend/actions/action.interface'; -import { RecordJSON } from 'adminjs/src/frontend/interfaces/record-json.interface'; import { findSocialProfilesByProjectId } from '../../../repositories/socialProfileRepository'; import { findProjectUpdatesByProjectId } from '../../../repositories/projectUpdateRepository'; import { @@ -53,16 +48,14 @@ import { makeFormVerified, } from '../../../repositories/projectVerificationRepository'; import { FeaturedUpdate } from '../../../entities/featuredUpdate'; -import { - findActiveQfRound, - relateManyProjectsToQfRound, -} from '../../../repositories/qfRoundRepository'; +import { findActiveQfRound } from '../../../repositories/qfRoundRepository'; import { User } from '../../../entities/user'; import { refreshProjectDonationSummaryView, refreshProjectEstimatedMatchingView, } from '../../../services/projectViewsService'; import { extractAdminJsReferrerUrlParams } from '../adminJs'; +import { relateManyProjectsToQfRound } from '../../../repositories/qfRoundRepository2'; // add queries depending on which filters were selected export const buildProjectsQuery = ( @@ -306,72 +299,68 @@ export const updateStatusOfProjects = async ( status, ) => { const { records, currentAdmin } = context; - try { - const projectIds = request?.query?.recordIds - ?.split(',') - ?.map(strId => Number(strId)) as number[]; - const projectsBeforeUpdating = await findProjectsByIdArray(projectIds); - const projectStatus = await ProjectStatus.findOne({ - where: { id: status }, - }); - if (projectStatus) { - const updateData: any = { status: projectStatus }; - if (status === ProjStatus.cancelled || status === ProjStatus.deactive) { - updateData.verified = false; - updateData.listed = false; - updateData.reviewStatus = ReviewStatus.NotListed; - } - const projects = await Project.createQueryBuilder('project') - .update(Project, updateData) - .where('project.id IN (:...ids)') - .setParameter('ids', projectIds) - .returning('*') - .updateEntity(true) - .execute(); + const projectIds = request?.query?.recordIds + ?.split(',') + ?.map(strId => Number(strId)) as number[]; + const projectsBeforeUpdating = await findProjectsByIdArray(projectIds); + const projectStatus = await ProjectStatus.findOne({ + where: { id: status }, + }); + if (projectStatus) { + const updateData: any = { status: projectStatus }; + if (status === ProjStatus.cancelled || status === ProjStatus.deactive) { + updateData.verified = false; + updateData.listed = false; + updateData.reviewStatus = ReviewStatus.NotListed; + } + const projects = await Project.createQueryBuilder('project') + .update(Project, updateData) + .where('project.id IN (:...ids)') + .setParameter('ids', projectIds) + .returning('*') + .updateEntity(true) + .execute(); - for (const project of projects.raw) { - if ( - projectsBeforeUpdating.find(p => p.id === project.id)?.statusId === - projectStatus.id - ) { - logger.debug('Changing project status but no changes happened', { - projectId: project.id, - projectStatus, - }); - // if project.listed have not changed, so we should not execute rest of the codes - continue; - } - await Project.addProjectStatusHistoryRecord({ - project, - status: projectStatus, - userId: currentAdmin.id, + for (const project of projects.raw) { + if ( + projectsBeforeUpdating.find(p => p.id === project.id)?.statusId === + projectStatus.id + ) { + logger.debug('Changing project status but no changes happened', { + projectId: project.id, + projectStatus, + }); + // if project.listed have not changed, so we should not execute rest of the codes + continue; + } + await Project.addProjectStatusHistoryRecord({ + project, + status: projectStatus, + userId: currentAdmin.id, + }); + const projectWithAdmin = (await findProjectById(project.id)) as Project; + if (status === ProjStatus.cancelled) { + await getNotificationAdapter().projectCancelled({ + project: projectWithAdmin, + }); + await changeUserBoostingsAfterProjectCancelled({ + projectId: project.id, + }); + } else if (status === ProjStatus.active) { + await getNotificationAdapter().projectReactivated({ + project: projectWithAdmin, + }); + } else if (status === ProjStatus.deactive) { + await getNotificationAdapter().projectDeactivated({ + project: projectWithAdmin, }); - const projectWithAdmin = (await findProjectById(project.id)) as Project; - if (status === ProjStatus.cancelled) { - await getNotificationAdapter().projectCancelled({ - project: projectWithAdmin, - }); - await changeUserBoostingsAfterProjectCancelled({ - projectId: project.id, - }); - } else if (status === ProjStatus.active) { - await getNotificationAdapter().projectReactivated({ - project: projectWithAdmin, - }); - } else if (status === ProjStatus.deactive) { - await getNotificationAdapter().projectDeactivated({ - project: projectWithAdmin, - }); - } } - await Promise.all([ - refreshUserProjectPowerView(), - refreshProjectFuturePowerView(), - refreshProjectPowerView(), - ]); } - } catch (error) { - throw error; + await Promise.all([ + refreshUserProjectPowerView(), + refreshProjectFuturePowerView(), + refreshProjectPowerView(), + ]); } return { redirectUrl: '/admin/resources/Project', @@ -390,27 +379,23 @@ export const addProjectsToQfRound = async ( request: AdminJsRequestInterface, add: boolean = true, ) => { - const { records, currentAdmin } = context; + const { records } = context; let message = messages.PROJECTS_RELATED_TO_ACTIVE_QF_ROUND_SUCCESSFULLY; - try { - const projectIds = request?.query?.recordIds - ?.split(',') - ?.map(strId => Number(strId)) as number[]; - const activeQfRound = await findActiveQfRound(); - if (activeQfRound) { - await relateManyProjectsToQfRound({ - projectIds, - qfRoundId: activeQfRound.id, - add, - }); + const projectIds = request?.query?.recordIds + ?.split(',') + ?.map(strId => Number(strId)) as number[]; + const qfRound = await findActiveQfRound(); + if (qfRound) { + await relateManyProjectsToQfRound({ + projectIds, + qfRound, + add, + }); - await refreshProjectEstimatedMatchingView(); - await refreshProjectDonationSummaryView(); - } else { - message = messages.THERE_IS_NOT_ANY_ACTIVE_QF_ROUND; - } - } catch (error) { - throw error; + await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); + } else { + message = messages.THERE_IS_NOT_ANY_ACTIVE_QF_ROUND; } return { redirectUrl: '/admin/resources/Project', @@ -431,23 +416,19 @@ export const addSingleProjectToQfRound = async ( ) => { const { record, currentAdmin } = context; let message = messages.PROJECTS_RELATED_TO_ACTIVE_QF_ROUND_SUCCESSFULLY; - try { - const projectId = Number(request?.params?.recordId); - const activeQfRound = await findActiveQfRound(); - if (activeQfRound) { - await relateManyProjectsToQfRound({ - projectIds: [projectId], - qfRoundId: activeQfRound.id, - add, - }); + const projectId = Number(request?.params?.recordId); + const qfRound = await findActiveQfRound(); + if (qfRound) { + await relateManyProjectsToQfRound({ + projectIds: [projectId], + qfRound, + add, + }); - await refreshProjectEstimatedMatchingView(); - await refreshProjectDonationSummaryView(); - } else { - message = messages.THERE_IS_NOT_ANY_ACTIVE_QF_ROUND; - } - } catch (error) { - throw error; + await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); + } else { + message = messages.THERE_IS_NOT_ANY_ACTIVE_QF_ROUND; } return { record: record.toJSON(currentAdmin), @@ -458,11 +439,9 @@ export const addSingleProjectToQfRound = async ( }; }; -export const fillSocialProfileAndQfRounds: After = async ( - response, - request, - context, -) => { +export const fillSocialProfileAndQfRounds: After< + ActionResponse +> = async response => { const record: RecordJSON = response.record || {}; // both cases for projectVerificationForms and projects' ids const projectId = record.params.projectId || record.params.id; @@ -910,11 +889,7 @@ export const projectsTab = { edit: { isAccessible: ({ currentAdmin }) => canAccessProjectAction({ currentAdmin }, ResourceActions.EDIT), - before: async ( - request: AdminJsRequestInterface, - response, - context: AdminJsContextInterface, - ) => { + before: async (request: AdminJsRequestInterface) => { const { verified, reviewStatus } = request.payload; const statusChanges: string[] = []; if (request?.payload?.id) { @@ -1250,7 +1225,7 @@ export const projectsTab = { actionType: 'record', isVisible: true, isAccessible: ({ currentAdmin }) => - canAccessQfRoundAction( + canAccessProjectAction( { currentAdmin }, ResourceActions.ADD_PROJECT_TO_QF_ROUND, ), @@ -1265,7 +1240,7 @@ export const projectsTab = { actionType: 'record', isVisible: true, isAccessible: ({ currentAdmin }) => - canAccessQfRoundAction( + canAccessProjectAction( { currentAdmin }, ResourceActions.ADD_PROJECT_TO_QF_ROUND, ), @@ -1281,7 +1256,7 @@ export const projectsTab = { actionType: 'bulk', isVisible: true, isAccessible: ({ currentAdmin }) => - canAccessQfRoundAction( + canAccessProjectAction( { currentAdmin }, ResourceActions.ADD_PROJECT_TO_QF_ROUND, ), @@ -1294,7 +1269,7 @@ export const projectsTab = { actionType: 'bulk', isVisible: true, isAccessible: ({ currentAdmin }) => - canAccessQfRoundAction( + canAccessProjectAction( { currentAdmin }, ResourceActions.ADD_PROJECT_TO_QF_ROUND, ), diff --git a/src/server/adminJs/tabs/qfRoundHistoryTab.ts b/src/server/adminJs/tabs/qfRoundHistoryTab.ts index 58fb3343e..8a1da694a 100644 --- a/src/server/adminJs/tabs/qfRoundHistoryTab.ts +++ b/src/server/adminJs/tabs/qfRoundHistoryTab.ts @@ -1,5 +1,4 @@ import { - canAccessQfRoundAction, canAccessQfRoundHistoryAction, ResourceActions, } from '../adminJsPermissions'; @@ -13,7 +12,6 @@ import { import { fillQfRoundHistory } from '../../../repositories/qfRoundHistoryRepository'; import { insertDonationsFromQfRoundHistory } from '../../../services/donationService'; import { refreshProjectDonationSummaryView } from '../../../services/projectViewsService'; -import { refreshUserProjectPowerView } from '../../../repositories/userProjectPowerViewRepository'; export const updateQfRoundHistory = async ( _request: AdminJsRequestInterface, @@ -155,7 +153,7 @@ export const qfRoundHistoryTab = { isAccessible: ({ currentAdmin }) => canAccessQfRoundHistoryAction( { currentAdmin }, - ResourceActions.UPDATE_QF_ROUND_HISTORIES, + ResourceActions.RELATE_DONATIONS_WITH_DISTRIBUTED_FUNDS, ), handler: CreateRelatedDonationsForQfRoundHistoryRecords, component: false, diff --git a/src/server/adminJs/tabs/qfRoundTab.ts b/src/server/adminJs/tabs/qfRoundTab.ts index 4cfdb96df..d937a1bf5 100644 --- a/src/server/adminJs/tabs/qfRoundTab.ts +++ b/src/server/adminJs/tabs/qfRoundTab.ts @@ -1,9 +1,11 @@ -import { QfRound } from '../../../entities/qfRound'; -import { canAccessQfRoundAction, ResourceActions } from '../adminJsPermissions'; import { ActionResponse, After, } from 'adminjs/src/backend/actions/action.interface'; +import adminJs, { ValidationError } from 'adminjs'; +import { RecordJSON } from 'adminjs/src/frontend/interfaces/record-json.interface'; +import { QfRound } from '../../../entities/qfRound'; +import { canAccessQfRoundAction, ResourceActions } from '../adminJsPermissions'; import { getQfRoundActualDonationDetails, refreshProjectActualMatchingView, @@ -14,17 +16,17 @@ import { AdminJsContextInterface, AdminJsRequestInterface, } from '../adminJs-types'; -import adminJs, { ValidationError } from 'adminjs'; import { isQfRoundHasEnded } from '../../../services/qfRoundService'; import { findQfRoundById, getRelatedProjectsOfQfRound, } from '../../../repositories/qfRoundRepository'; -import { RecordJSON } from 'adminjs/src/frontend/interfaces/record-json.interface'; import { NETWORK_IDS } from '../../../provider'; import { logger } from '../../../utils/logger'; import { messages } from '../../../utils/messages'; import { addQfRoundDonationsSheetToSpreadsheet } from '../../../services/googleSheets'; +import { errorMessages } from '../../../utils/errorMessages'; +import { relateManyProjectsToQfRound } from '../../../repositories/qfRoundRepository2'; export const refreshMaterializedViews = async ( response, @@ -43,8 +45,6 @@ export const fillProjects: After = async ( const record: RecordJSON = response.record || {}; const qfRoundId = record.params.qfRoundId || record.params.id; const projects = await getRelatedProjectsOfQfRound(qfRoundId); - - const adminJsBaseUrl = process.env.SERVER_URL; response.record = { ...record, params: { @@ -60,27 +60,32 @@ const returnAllQfRoundDonationAnalysis = async ( request: AdminJsRequestInterface, ) => { const { record, currentAdmin } = context; + const qfRoundId = Number(request?.params?.recordId); + let type = 'success'; + logger.debug( + 'returnAllQfRoundDonationAnalysis() has been called, qfRoundId', + qfRoundId, + ); + let message = messages.QF_ROUND_DATA_UPLOAD_IN_GOOGLE_SHEET_SUCCESSFULLY; try { - const qfRoundId = Number(request?.params?.recordId); - logger.debug('qfRoundId', qfRoundId); - - const qfRoundDonationsRows = await getQfRoundActualDonationDetails( - qfRoundId, - ); + const qfRoundDonationsRows = + await getQfRoundActualDonationDetails(qfRoundId); logger.debug('qfRoundDonationsRows', qfRoundDonationsRows); await addQfRoundDonationsSheetToSpreadsheet({ rows: qfRoundDonationsRows, qfRoundId, }); - // TODO Upload to google sheet - } catch (error) { - throw error; + } catch (e) { + logger.error('returnAllQfRoundDonationAnalysis() error', e); + message = e.message; + type = 'danger'; } + return { record: record.toJSON(currentAdmin), notice: { - message: messages.QF_ROUND_DATA_UPLOAD_IN_GOOGLE_SHEET_SUCCESSFULLY, - type: 'success', + message, + type, }, }; }; @@ -89,6 +94,23 @@ export const qfRoundTab = { resource: QfRound, options: { properties: { + addProjectIdsList: { + type: 'textarea', + // projectIds separated By comma + isVisible: { + filter: false, + list: false, + show: false, + new: false, + edit: true, + }, + }, + title: { + isVisible: true, + }, + description: { + isVisible: true, + }, name: { isVisible: true, }, @@ -130,7 +152,7 @@ export const qfRoundTab = { value: NETWORK_IDS.MORDOR_ETC_TESTNET, label: 'MORDOR ETC TESTNET', }, - { value: NETWORK_IDS.OPTIMISM_GOERLI, label: 'OPTIMISM GOERLI' }, + { value: NETWORK_IDS.OPTIMISM_SEPOLIA, label: 'OPTIMISM SEPOLIA' }, { value: NETWORK_IDS.CELO, label: 'CELO' }, { value: NETWORK_IDS.CELO_ALFAJORES, @@ -198,21 +220,36 @@ export const qfRoundTab = { canAccessQfRoundAction({ currentAdmin }, ResourceActions.EDIT), before: async ( request: AdminJsRequestInterface, - response, + _response, _context: AdminJsContextInterface, ) => { // https://docs.adminjs.co/basics/action#using-before-and-after-hooks if (request?.payload?.id) { const qfRoundId = Number(request.payload.id); const qfRound = await findQfRoundById(qfRoundId); - if (!qfRound || isQfRoundHasEnded({ endDate: qfRound!.endDate })) { + if (!qfRound) { throw new ValidationError({ endDate: { - message: - 'The endDate has passed so qfRound cannot be edited.', + message: errorMessages.QF_ROUND_NOT_FOUND, }, }); } + if (isQfRoundHasEnded({ endDate: qfRound!.endDate })) { + // When qf round is ended we should not be able to edit begin date and end date + // https://github.com/Giveth/giveth-dapps-v2/issues/3864 + request.payload.endDate = qfRound.endDate; + request.payload.beginDate = qfRound.beginDate; + request.payload.isActive = qfRound.isActive; + } else if ( + qfRound.isActive && + request?.payload?.addProjectIdsList?.split(',')?.length > 0 + ) { + await relateManyProjectsToQfRound({ + projectIds: request.payload.addProjectIdsList.split(','), + qfRound, + add: true, + }); + } } return request; }, @@ -223,6 +260,11 @@ export const qfRoundTab = { // https://docs.adminjs.co/basics/action#record-type-actions actionType: 'record', isVisible: true, + isAccessible: ({ currentAdmin }) => + canAccessQfRoundAction( + { currentAdmin }, + ResourceActions.RETURN_ALL_DONATIONS_DATA, + ), handler: async (request, response, context) => { return returnAllQfRoundDonationAnalysis(context, request); }, diff --git a/src/server/adminJs/tabs/recurringDonationTab.ts b/src/server/adminJs/tabs/recurringDonationTab.ts index d85fb47e4..8816895c9 100644 --- a/src/server/adminJs/tabs/recurringDonationTab.ts +++ b/src/server/adminJs/tabs/recurringDonationTab.ts @@ -3,20 +3,28 @@ import { RecurringDonation } from '../../../entities/recurringDonation'; export const RecurringDonationTab = { resource: RecurringDonation, - options: {}, - - actions: { - new: { - isVisible: false, - }, - edit: { - isVisible: false, - }, - delete: { - isVisible: false, - }, - bulkDelete: { - isVisible: false, + options: { + actions: { + list: { + isAccessible: ({ currentAdmin }) => + currentAdmin && currentAdmin.role !== 'qfManager', + }, + show: { + isAccessible: ({ currentAdmin }) => + currentAdmin && currentAdmin.role !== 'qfManager', + }, + new: { + isVisible: false, + }, + edit: { + isVisible: false, + }, + delete: { + isVisible: false, + }, + bulkDelete: { + isVisible: false, + }, }, }, }; diff --git a/src/server/adminJs/tabs/sybilTab.test.ts b/src/server/adminJs/tabs/sybilTab.test.ts index 5f86f61f5..ceff58156 100644 --- a/src/server/adminJs/tabs/sybilTab.test.ts +++ b/src/server/adminJs/tabs/sybilTab.test.ts @@ -1,11 +1,11 @@ +import moment from 'moment'; +import { assert } from 'chai'; import { generateRandomEtheriumAddress, saveUserDirectlyToDb, } from '../../../../test/testUtils'; import { QfRound } from '../../../entities/qfRound'; -import moment from 'moment'; import { createSybil } from './sybilTab'; -import { assert } from 'chai'; import { errorMessages } from '../../../utils/errorMessages'; import { Sybil } from '../../../entities/sybil'; diff --git a/src/server/adminJs/tabs/sybilTab.ts b/src/server/adminJs/tabs/sybilTab.ts index 66f32b773..4373f6f45 100644 --- a/src/server/adminJs/tabs/sybilTab.ts +++ b/src/server/adminJs/tabs/sybilTab.ts @@ -1,14 +1,8 @@ +import csv from 'csvtojson'; import { Sybil } from '../../../entities/sybil'; -import { - canAccessProjectStatusReasonAction, - ResourceActions, -} from '../adminJsPermissions'; -import { - AdminJsContextInterface, - AdminJsRequestInterface, -} from '../adminJs-types'; +import { canAccessSybilAction, ResourceActions } from '../adminJsPermissions'; +import { AdminJsRequestInterface } from '../adminJs-types'; import { logger } from '../../../utils/logger'; -import csv from 'csvtojson'; import { messages } from '../../../utils/messages'; import { errorMessages } from '../../../utils/errorMessages'; import { findUserByWalletAddress } from '../../../repositories/userRepository'; @@ -16,7 +10,6 @@ import { findUserByWalletAddress } from '../../../repositories/userRepository'; export const createSybil = async ( request: AdminJsRequestInterface, response, - context?: AdminJsContextInterface, ) => { let message = messages.SYBIL_HAS_BEEN_CREATED_SUCCESSFULLY; logger.debug('createSybil has been called() ', request.payload); @@ -154,31 +147,19 @@ export const SybilTab = { handler: createSybil, isAccessible: ({ currentAdmin }) => - canAccessProjectStatusReasonAction( - { currentAdmin }, - ResourceActions.NEW, - ), + canAccessSybilAction({ currentAdmin }, ResourceActions.NEW), }, edit: { isAccessible: ({ currentAdmin }) => - canAccessProjectStatusReasonAction( - { currentAdmin }, - ResourceActions.EDIT, - ), + canAccessSybilAction({ currentAdmin }, ResourceActions.EDIT), }, delete: { isAccessible: ({ currentAdmin }) => - canAccessProjectStatusReasonAction( - { currentAdmin }, - ResourceActions.EDIT, - ), + canAccessSybilAction({ currentAdmin }, ResourceActions.DELETE), }, bulkDelete: { isAccessible: ({ currentAdmin }) => - canAccessProjectStatusReasonAction( - { currentAdmin }, - ResourceActions.EDIT, - ), + canAccessSybilAction({ currentAdmin }, ResourceActions.BULK_DELETE), }, }, }, diff --git a/src/server/adminJs/tabs/thirdPartProjectImportTab.ts b/src/server/adminJs/tabs/thirdPartProjectImportTab.ts index 410ee70ec..d867fbf96 100644 --- a/src/server/adminJs/tabs/thirdPartProjectImportTab.ts +++ b/src/server/adminJs/tabs/thirdPartProjectImportTab.ts @@ -89,6 +89,20 @@ export const thirdPartProjectImportTab = { }, }, actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessThirdPartyProjectImportAction( + { currentAdmin }, + ResourceActions.LIST, + ), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessThirdPartyProjectImportAction( + { currentAdmin }, + ResourceActions.SHOW, + ), + }, bulkDelete: { isVisible: false, isAccessible: ({ currentAdmin }) => diff --git a/src/server/adminJs/tabs/thirdPartyProjectImport.test.ts b/src/server/adminJs/tabs/thirdPartyProjectImport.test.ts index b7cd8cb3c..1dd397f6c 100644 --- a/src/server/adminJs/tabs/thirdPartyProjectImport.test.ts +++ b/src/server/adminJs/tabs/thirdPartyProjectImport.test.ts @@ -1,3 +1,5 @@ +import { assert } from 'chai'; +import sinon from 'sinon'; import { findUserById } from '../../../repositories/userRepository'; import { generateRandomEtheriumAddress, @@ -7,10 +9,8 @@ import * as ChangeAPI from '../../../services/changeAPI/nonProfits'; import { errorMessages } from '../../../utils/errorMessages'; import { User } from '../../../entities/user'; import { Project } from '../../../entities/project'; -import { assert } from 'chai'; import { ProjectAddress } from '../../../entities/projectAddress'; import { importThirdPartyProject } from './thirdPartProjectImportTab'; -import sinon from 'sinon'; describe( 'importThirdPartyProject() test cases', diff --git a/src/server/adminJs/tabs/tokenTab.test.ts b/src/server/adminJs/tabs/tokenTab.test.ts index c1095b3ae..48f63c2b5 100644 --- a/src/server/adminJs/tabs/tokenTab.test.ts +++ b/src/server/adminJs/tabs/tokenTab.test.ts @@ -1,5 +1,5 @@ -import { generateRandomEvmTxHash } from '../../../../test/testUtils'; import { assert } from 'chai'; +import { generateRandomEvmTxHash } from '../../../../test/testUtils'; import { findTokenByTokenAddress } from '../../../repositories/tokenRepository'; import { Organization, @@ -66,7 +66,7 @@ function generateOrganizationListTestCases() { // this includes all organizations option it('should return 15 permutations option when 4 organizations are present', async () => { let totalPermutations = 0; - const [_, n] = await Organization.createQueryBuilder('organization') + const [, n] = await Organization.createQueryBuilder('organization') .orderBy('organization.id') .getManyAndCount(); // there is no take 0 elements case @@ -94,7 +94,7 @@ function generateOrganizationListTestCases() { }); await organization.save(); - const [_, n] = await Organization.createQueryBuilder('organization') + const [, n] = await Organization.createQueryBuilder('organization') .orderBy('organization.id') .getManyAndCount(); // there is no take 0 elements case diff --git a/src/server/adminJs/tabs/tokenTab.ts b/src/server/adminJs/tabs/tokenTab.ts index 1e6551d0c..b6a83f959 100644 --- a/src/server/adminJs/tabs/tokenTab.ts +++ b/src/server/adminJs/tabs/tokenTab.ts @@ -1,6 +1,7 @@ +import adminJs from 'adminjs'; +import { RecordJSON } from 'adminjs/src/frontend/interfaces/record-json.interface'; import { Token } from '../../../entities/token'; import { NETWORK_IDS } from '../../../provider'; -import adminJs from 'adminjs'; import { canAccessTokenAction, ResourceActions } from '../adminJsPermissions'; import { AdminJsRequestInterface } from '../adminJs-types'; import { Organization } from '../../../entities/organization'; @@ -45,7 +46,7 @@ export const permuteOrganizations = ( }; export const generateOrganizationList = async () => { - const organizationsList: {}[] = []; + const organizationsList: NonNullable[] = []; const [organizations, organizationCount] = await Organization.createQueryBuilder('organization') .orderBy('organization.id') @@ -80,8 +81,6 @@ export const linkOrganizations = async (request: AdminJsRequestInterface) => { // default handler updates the other params, we only care about orgs if (!request.record.params.organizations) return request; - let message = `Token created successfully`; - let type = 'success'; const { organizations, id } = request.record.params; try { const token = await findTokenByTokenId(id); @@ -105,8 +104,6 @@ export const linkOrganizations = async (request: AdminJsRequestInterface) => { await token!.save(); } catch (e) { logger.error('error creating token', e.message); - message = e.message; - type = 'danger'; } return request; @@ -114,7 +111,7 @@ export const linkOrganizations = async (request: AdminJsRequestInterface) => { export const createToken = async ( request: AdminJsRequestInterface, - response, + context, ) => { let message = `Token created successfully`; let type = 'success'; @@ -158,16 +155,24 @@ export const createToken = async ( type = 'danger'; } - response.send({ - redirectUrl: '/admin/resources/Token', - record: {}, + const record: RecordJSON = { + baseError: null, + id: request?.params?.recordId || '', + title: '', + bulkActions: [], + errors: {}, + params: (context as any)?.record?.params, + populated: (context as any)?.record?.populated, + recordActions: [], + }; + + return { + redirectUrl: '/admin/resources/Token/actions/new', + record, notice: { message, type, }, - }); - return { - record: newToken, }; }; @@ -184,7 +189,7 @@ export const generateTokenTab = async () => { { value: NETWORK_IDS.GOERLI, label: 'GOERLI' }, { value: NETWORK_IDS.POLYGON, label: 'POLYGON' }, { value: NETWORK_IDS.OPTIMISTIC, label: 'OPTIMISTIC' }, - { value: NETWORK_IDS.OPTIMISM_GOERLI, label: 'OPTIMISM GOERLI' }, + { value: NETWORK_IDS.OPTIMISM_SEPOLIA, label: 'OPTIMISM SEPOLIA' }, { value: NETWORK_IDS.CELO, label: 'CELO' }, { value: NETWORK_IDS.CELO_ALFAJORES, @@ -248,6 +253,14 @@ export const generateTokenTab = async () => { }, }, actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessTokenAction({ currentAdmin }, ResourceActions.LIST), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessTokenAction({ currentAdmin }, ResourceActions.SHOW), + }, bulkDelete: { isVisible: false, isAccessible: ({ currentAdmin }) => @@ -289,8 +302,8 @@ export const generateTokenTab = async () => { new: { isAccessible: ({ currentAdmin }) => canAccessTokenAction({ currentAdmin }, ResourceActions.NEW), - handler: createToken, - // component: false + handler: async (req, _res, context) => createToken(req, context), + // component: false, }, }, }, diff --git a/src/server/adminJs/tabs/usersTab.ts b/src/server/adminJs/tabs/usersTab.ts index d200e1a40..60d75a380 100644 --- a/src/server/adminJs/tabs/usersTab.ts +++ b/src/server/adminJs/tabs/usersTab.ts @@ -1,7 +1,7 @@ import { User } from '../../../entities/user'; import { canAccessUserAction, ResourceActions } from '../adminJsPermissions'; import { logger } from '../../../utils/logger'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const bcrypt = require('bcrypt'); export const usersTab = { @@ -26,6 +26,14 @@ export const usersTab = { }, }, actions: { + list: { + isAccessible: ({ currentAdmin }) => + canAccessUserAction({ currentAdmin }, ResourceActions.LIST), + }, + show: { + isAccessible: ({ currentAdmin }) => + canAccessUserAction({ currentAdmin }, ResourceActions.SHOW), + }, delete: { isVisible: false, isAccessible: ({ currentAdmin }) => diff --git a/src/server/bootstrap.ts b/src/server/bootstrap.ts index 7a300ba58..7eb8bc1a8 100644 --- a/src/server/bootstrap.ts +++ b/src/server/bootstrap.ts @@ -1,20 +1,25 @@ // @ts-check -import config from '../config'; +import http from 'http'; import { rateLimit } from 'express-rate-limit'; import { RedisStore } from 'rate-limit-redis'; import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { ApolloServerPluginSchemaReporting } from '@apollo/server/plugin/schemaReporting'; import { ApolloServerPluginLandingPageGraphQLPlayground } from '@apollo/server-plugin-landing-page-graphql-playground'; -import express, { json, Request, Response } from 'express'; -import { handleStripeWebhook } from '../utils/stripe'; -import createSchema from './createSchema'; -import { getResolvers } from '../resolvers/resolvers'; +import express, { json, Request } from 'express'; import { Container } from 'typedi'; -import { RegisterResolver } from '../user/register/RegisterResolver'; -import { ConfirmUserResolver } from '../user/ConfirmUserResolver'; import { Resource } from '@adminjs/typeorm'; import { validate } from 'class-validator'; +import { ModuleThread, Pool, spawn, Worker } from 'threads'; +import { DataSource } from 'typeorm'; +import cors from 'cors'; +import bodyParser from 'body-parser'; +import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js'; +import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled'; +import { ApolloServerErrorCode } from '@apollo/server/errors'; +import config from '../config'; +import { handleStripeWebhook } from '../utils/stripe'; +import createSchema from './createSchema'; import SentryLogger from '../sentryLogger'; import { runCheckPendingDonationsCronJob } from '../services/cronJobs/syncDonationsWithNetwork'; @@ -47,18 +52,10 @@ import { import { runFillPowerSnapshotBalanceCronJob } from '../services/cronJobs/fillSnapshotBalances'; import { runUpdatePowerRoundCronJob } from '../services/cronJobs/updatePowerRoundJob'; import { onramperWebhookHandler } from '../services/onramper/webhookHandler'; -import { ModuleThread, Pool, spawn, Worker } from 'threads'; -import { DataSource } from 'typeorm'; import { AppDataSource, CronDataSource } from '../orm'; -import http from 'http'; -import cors from 'cors'; -import bodyParser from 'body-parser'; import { ApolloContext } from '../types/ApolloContext'; import { ProjectResolverWorker } from '../workers/projectsResolverWorker'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js'; -import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled'; -import { ApolloServerErrorCode } from '@apollo/server/errors'; import { runInstantBoostingUpdateCronJob } from '../services/cronJobs/instantBoostingUpdateJob'; import { refreshProjectDonationSummaryView, @@ -72,6 +69,9 @@ import { runSyncLostDonations } from '../services/cronJobs/importLostDonationsJo import { runSyncBackupServiceDonations } from '../services/cronJobs/backupDonationImportJob'; import { runUpdateRecurringDonationStream } from '../services/cronJobs/updateStreamOldRecurringDonationsJob'; import { runDraftDonationMatchWorkerJob } from '../services/cronJobs/draftDonationMatchingJob'; +import { runCheckUserSuperTokenBalancesJob } from '../services/cronJobs/checkUserSuperTokenBalancesJob'; +import { runCheckPendingRecurringDonationsCronJob } from '../services/cronJobs/syncRecurringDonationsWithNetwork'; +import { runDraftRecurringDonationMatchWorkerJob } from '../services/cronJobs/draftRecurringDonationMatchingJob'; Resource.validate = validate; @@ -89,21 +89,13 @@ export async function bootstrap() { await AppDataSource.initialize(); await CronDataSource.initialize(); Container.set(DataSource, AppDataSource.getDataSource()); - const resolvers = getResolvers(); - - if (config.get('REGISTER_USERNAME_PASSWORD') === 'true') { - resolvers.push.apply(resolvers, [RegisterResolver, ConfirmUserResolver]); - } - - // Actually we should use await AppDataSource.initialize(); but it throw errors I think because some changes - // are needed in using typeorm repositories, so currently I kept this const dropSchema = config.get('DROP_DATABASE') === 'true'; if (dropSchema) { - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('Drop database....'); await AppDataSource.getDataSource().synchronize(dropSchema); - // tslint:disable-next-line:no-console + // eslint-disable-next-line no-console console.log('Drop done.'); try { await dropDbCronExtension(); @@ -219,7 +211,7 @@ export async function bootstrap() { }), windowMs: 60 * 1000, // 1 minutes max: Number(process.env.ALLOWED_REQUESTS_PER_MINUTE), // limit each IP to 40 requests per windowMs - skip: (req: Request, res: Response) => { + skip: (req: Request) => { const vercelKey = process.env.VERCEL_KEY; if (vercelKey && req.headers.vercel_key === vercelKey) { // Skip rate-limit for Vercel requests because our front is SSR @@ -294,7 +286,7 @@ export async function bootstrap() { bodyParser.raw({ type: 'application/json' }), handleStripeWebhook, ); - app.get('/health', (req, res, next) => { + app.get('/health', (_req, res) => { res.send('Hi every thing seems ok'); }); app.post('/fiat_webhook', onramperWebhookHandler); @@ -323,6 +315,7 @@ export async function bootstrap() { } runCheckPendingDonationsCronJob(); + runCheckPendingRecurringDonationsCronJob(); runNotifyMissingDonationsCronJob(); runCheckPendingProjectListingCronJob(); runUpdateDonationsWithoutValueUsdPrices(); @@ -351,6 +344,10 @@ export async function bootstrap() { runDraftDonationMatchWorkerJob(); } + if (process.env.ENABLE_DRAFT_RECURRING_DONATION === 'true') { + runDraftRecurringDonationMatchWorkerJob(); + } + if (process.env.FILL_POWER_SNAPSHOT_BALANCE_SERVICE_ACTIVE === 'true') { runFillPowerSnapshotBalanceCronJob(); } @@ -376,6 +373,7 @@ export async function bootstrap() { } if (process.env.ENABLE_UPDATE_RECURRING_DONATION_STREAM === 'true') { runUpdateRecurringDonationStream(); + runCheckUserSuperTokenBalancesJob(); } await runCheckActiveStatusOfQfRounds(); await runUpdateProjectCampaignsCacheJob(); diff --git a/src/server/createSchema.ts b/src/server/createSchema.ts index d80675d9f..88cd44004 100644 --- a/src/server/createSchema.ts +++ b/src/server/createSchema.ts @@ -1,9 +1,9 @@ import * as TypeGraphQL from 'type-graphql'; -import { getResolvers } from '../resolvers/resolvers'; import { Container } from 'typedi'; -import { userCheck } from '../auth/userCheck'; import { GraphQLSchema } from 'graphql'; import { NonEmptyArray } from 'type-graphql'; +import { userCheck } from '../auth/userCheck'; +import { getResolvers } from '../resolvers/resolvers'; import config from '../config'; const createSchema = async (): Promise => { @@ -18,7 +18,7 @@ const createSchema = async (): Promise => { const environment = config.get('ENVIRONMENT') as string; // build TypeGraphQL executable schema const schema = await TypeGraphQL.buildSchema({ - resolvers: getResolvers() as NonEmptyArray, + resolvers: getResolvers() as NonEmptyArray<() => NonNullable>, container: Container, authChecker: userCheck, validate: { diff --git a/src/services/Idriss/contractDonations.ts b/src/services/Idriss/contractDonations.ts index c997bd559..8beb8f53e 100644 --- a/src/services/Idriss/contractDonations.ts +++ b/src/services/Idriss/contractDonations.ts @@ -1,15 +1,9 @@ -// import ethers.. - import { ethers } from 'ethers'; -import { - getLatestBlockNumberFromDonations, - isTransactionHashStored, -} from '../../repositories/donationRepository'; +import moment from 'moment'; +import axios from 'axios'; +import { isTransactionHashStored } from '../../repositories/donationRepository'; import { DONATION_ORIGINS, Donation } from '../../entities/donation'; -import { - findProjectByWalletAddress, - findProjectByWalletAddressAndNetwork, -} from '../../repositories/projectRepository'; +import { findProjectByWalletAddressAndNetwork } from '../../repositories/projectRepository'; import { NETWORK_IDS } from '../../provider'; import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; import { ProjStatus } from '../../entities/project'; @@ -27,9 +21,7 @@ import { } from '../../repositories/userRepository'; import { logger } from '../../utils/logger'; import { getGitcoinAdapter } from '../../adapters/adaptersFactory'; -import moment from 'moment'; import { calculateGivbackFactor } from '../givbackService'; -import axios from 'axios'; import { updateUserTotalDonated, updateUserTotalReceived, @@ -209,7 +201,7 @@ export const createIdrissTwitterDonation = async ( } } - const donation = await Donation.create({ + const donation = Donation.create({ amount: Number(idrissDonation.amount), transactionId: idrissDonation?.txHash?.toLowerCase(), isFiat: false, diff --git a/src/services/actualMatchingFundView.test.ts b/src/services/actualMatchingFundView.test.ts index db7f73fca..01ca48391 100644 --- a/src/services/actualMatchingFundView.test.ts +++ b/src/services/actualMatchingFundView.test.ts @@ -1,6 +1,7 @@ +import moment from 'moment'; +import { assert } from 'chai'; import { QfRound } from '../entities/qfRound'; import { Project } from '../entities/project'; -import moment from 'moment'; import { createDonationData, createProjectData, @@ -9,8 +10,6 @@ import { saveProjectDirectlyToDb, saveUserDirectlyToDb, } from '../../test/testUtils'; -import { getProjectDonationsSqrtRootSum } from '../repositories/qfRoundRepository'; -import { assert, expect } from 'chai'; import { refreshProjectActualMatchingView, refreshProjectDonationSummaryView, @@ -21,7 +20,6 @@ import { NETWORK_IDS } from '../provider'; import { Sybil } from '../entities/sybil'; import { ProjectFraud } from '../entities/projectFraud'; import { DONATION_STATUS } from '../entities/donation'; -import { findProjectById } from '../repositories/projectRepository'; describe('getActualMatchingFund test cases', getActualMatchingFundTests); @@ -73,7 +71,7 @@ function getActualMatchingFundTests() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); user.passportScore = qfRound.minimumPassportScore; await user.save(); - const donation = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( { ...createDonationData(), status: 'verified', @@ -107,7 +105,7 @@ function getActualMatchingFundTests() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); user.passportScore = qfRound.minimumPassportScore; await user.save(); - const verifiedProject = await saveProjectDirectlyToDb({ + await saveProjectDirectlyToDb({ ...createProjectData(), walletAddress: user.walletAddress as string, listed: true, @@ -144,161 +142,10 @@ function getActualMatchingFundTests() { // qfRound has 4 networks so we just recipient addresses for those networks assert.equal(actualMatchingFund?.networkAddresses?.split(',').length, 4); }); - // THIS TESTS NO LONGER ARE VALID - // it('Confirms donations from recipients of non-verified projects are included', async () => { - // const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - // user.passportScore = qfRound.minimumPassportScore; - // await user.save(); - // const nonVerified = await saveProjectDirectlyToDb({ - // ...createProjectData(), - // walletAddress: user.walletAddress as string, - // listed: true, - // verified: false, - // }); - // const donation = await saveDonationDirectlyToDb( - // { - // ...createDonationData(), - // status: 'verified', - // valueUsd: 100, - // qfRoundId: qfRound.id, - // qfRoundUserScore: user.passportScore, - // }, - // user.id, - // project.id, - // ); - // await refreshProjectActualMatchingView(); - - // const actualMatchingFund = await ProjectActualMatchingView.findOne({ - // where: { - // projectId: project.id, - // qfRoundId: qfRound.id, - // }, - // }); - // assert.equal(actualMatchingFund?.projectId, project.id); - // assert.equal(actualMatchingFund?.donationIdsBeforeAnalysis.length, 1); - // assert.equal(actualMatchingFund?.donationIdsBeforeAnalysis[0], donation.id); - // assert.equal(actualMatchingFund?.donationIdsAfterAnalysis.length, 1); - // assert.equal(actualMatchingFund?.allUsdReceived, donation.valueUsd); - // assert.equal( - // actualMatchingFund?.allUsdReceivedAfterSybilsAnalysis, - // donation.valueUsd, - // ); - // assert.equal(actualMatchingFund?.uniqueQualifiedDonors, 1); - // assert.equal(actualMatchingFund?.totalDonors, 1); - - // // qfRound has 4 networks so we just recipient addresses for those networks - // assert.equal(actualMatchingFund?.networkAddresses?.split(',').length, 4); - // }); - // it('Confirms donations from recipients of non-verified projects that are in another qfRound are included', async () => { - // const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - // user.passportScore = qfRound.minimumPassportScore; - // await user.save(); - // const projectInAnotherQfRound = await saveProjectDirectlyToDb({ - // ...createProjectData(), - // walletAddress: user.walletAddress as string, - // listed: true, - // verified: false, - // }); - // const qfRound2 = QfRound.create({ - // isActive: true, - // name: 'test', - // allocatedFund: 100, - // minimumPassportScore: 8, - // minimumValidUsdValue: 1, - // slug: new Date().getTime().toString(), - // eligibleNetworks: [ - // NETWORK_IDS.XDAI, - // NETWORK_IDS.OPTIMISTIC, - // NETWORK_IDS.POLYGON, - // NETWORK_IDS.MAIN_NET, - // ], - // beginDate: new Date(), - // endDate: moment().add(10, 'days').toDate(), - // }); - - // projectInAnotherQfRound.qfRounds = [qfRound2]; - // await projectInAnotherQfRound.save(); - // const donation = await saveDonationDirectlyToDb( - // { - // ...createDonationData(), - // status: 'verified', - // valueUsd: 100, - // qfRoundId: qfRound.id, - // qfRoundUserScore: user.passportScore, - // }, - // user.id, - // project.id, - // ); - // await refreshProjectActualMatchingView(); - - // const actualMatchingFund = await ProjectActualMatchingView.findOne({ - // where: { - // projectId: project.id, - // qfRoundId: qfRound.id, - // }, - // }); - // assert.equal(actualMatchingFund?.projectId, project.id); - // assert.equal(actualMatchingFund?.donationIdsBeforeAnalysis.length, 1); - // assert.equal(actualMatchingFund?.donationIdsBeforeAnalysis[0], donation.id); - // assert.equal(actualMatchingFund?.donationIdsAfterAnalysis.length, 1); - // assert.equal(actualMatchingFund?.allUsdReceived, donation.valueUsd); - // assert.equal( - // actualMatchingFund?.allUsdReceivedAfterSybilsAnalysis, - // donation.valueUsd, - // ); - // assert.equal(actualMatchingFund?.uniqueQualifiedDonors, 1); - // assert.equal(actualMatchingFund?.totalDonors, 1); - - // // qfRound has 4 networks so we just recipient addresses for those networks - // assert.equal(actualMatchingFund?.networkAddresses?.split(',').length, 4); - // }); - it('Confirms donations from recipients of non-verified projects that are in that qfRound are excluded', async () => { - const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - user.passportScore = qfRound.minimumPassportScore; - await user.save(); - const projectInQfRound = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: user.walletAddress as string, - listed: true, - verified: false, - }); - projectInQfRound.qfRounds = [qfRound]; - await projectInQfRound.save(); - const donation = await saveDonationDirectlyToDb( - { - ...createDonationData(), - status: 'verified', - valueUsd: 100, - qfRoundId: qfRound.id, - qfRoundUserScore: user.passportScore, - }, - user.id, - project.id, - ); - await refreshProjectActualMatchingView(); - - const actualMatchingFund = await ProjectActualMatchingView.findOne({ - where: { - projectId: project.id, - qfRoundId: qfRound.id, - }, - }); - assert.equal(actualMatchingFund?.projectId, project.id); - assert.equal(actualMatchingFund?.donationIdsBeforeAnalysis.length, 1); - assert.equal(actualMatchingFund?.donationIdsBeforeAnalysis[0], donation.id); - assert.isNotOk(actualMatchingFund?.donationIdsAfterAnalysis); - assert.equal(actualMatchingFund?.allUsdReceived, donation.valueUsd); - assert.equal(actualMatchingFund?.allUsdReceivedAfterSybilsAnalysis, 0); - assert.equal(actualMatchingFund?.uniqueQualifiedDonors, 0); - assert.equal(actualMatchingFund?.totalDonors, 1); - - // qfRound has 4 networks so we just recipient addresses for those networks - assert.equal(actualMatchingFund?.networkAddresses?.split(',').length, 4); - }); it('Validates correct aggregation of multiple donations to a project', async () => { const valuesUsd = [4, 25, 100, 1024]; await Promise.all( - valuesUsd.map(async (valueUsd, index) => { + valuesUsd.map(async valueUsd => { const user = await saveUserDirectlyToDb( generateRandomEtheriumAddress(), ); @@ -517,7 +364,7 @@ function getActualMatchingFundTests() { await project.save(); user.passportScore = qfRound.minimumPassportScore; await user.save(); - const donation = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( { ...createDonationData(), status: 'verified', @@ -553,7 +400,7 @@ function getActualMatchingFundTests() { await project.save(); user.passportScore = qfRound.minimumPassportScore; await user.save(); - const donation = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( { ...createDonationData(), status: 'verified', diff --git a/src/services/authorizationService.test.ts b/src/services/authorizationService.test.ts index 34f589693..f79329a79 100644 --- a/src/services/authorizationService.test.ts +++ b/src/services/authorizationService.test.ts @@ -1,19 +1,19 @@ import { assert } from 'chai'; +import Axios from 'axios'; +import { ethers } from 'ethers'; import { generateRandomEtheriumAddress, generateTestAccessToken, saveUserDirectlyToDb, } from '../../test/testUtils'; import { User } from '../entities/user'; -import Axios from 'axios'; import { authorizationHandler } from './authorizationServices'; import config from '../config'; -import { ethers } from 'ethers'; import { findUserByWalletAddress } from '../repositories/userRepository'; describe('authorizationHandler() test cases', authorizationHandlerTestCases); -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const siwe = require('siwe'); const domain = 'localhost'; diff --git a/src/services/authorizationServices.ts b/src/services/authorizationServices.ts index 49fac286d..5f6520ebf 100644 --- a/src/services/authorizationServices.ts +++ b/src/services/authorizationServices.ts @@ -1,10 +1,6 @@ import Axios from 'axios'; import * as jwt from 'jsonwebtoken'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { ApolloContext } from '../types/ApolloContext'; import SentryLogger from '../sentryLogger'; import { logger } from '../utils/logger'; diff --git a/src/services/campaignService.ts b/src/services/campaignService.ts index d593c6997..fd22e2947 100644 --- a/src/services/campaignService.ts +++ b/src/services/campaignService.ts @@ -1,3 +1,4 @@ +import { ModuleThread, Pool } from 'threads'; import { Campaign, CampaignType } from '../entities/campaign'; import { FilterProjectQueryInputParams, @@ -5,9 +6,7 @@ import { } from '../repositories/projectRepository'; import { FilterField, Project, SortingField } from '../entities/project'; import { findUserReactionsByProjectIds } from '../repositories/reactionRepository'; -import { ModuleThread, Pool } from 'threads'; import { ProjectResolverWorker } from '../workers/projectsResolverWorker'; -import { QueryBuilder } from 'typeorm/query-builder/QueryBuilder'; import { findAllActiveCampaigns } from '../repositories/campaignRepository'; import { logger } from '../utils/logger'; import { getRedisObject, setObjectInRedis } from '../redis'; @@ -103,6 +102,7 @@ export const fillCampaignProjects = async (params: { ); let projects: Project[]; let totalCount: number; + // eslint-disable-next-line prefer-const [projects, totalCount] = await projectsQuery .cache(projectsQueryCacheKey, projectFiltersCacheDuration) .getManyAndCount(); diff --git a/src/services/chains/evm/draftDonationService.test.ts b/src/services/chains/evm/draftDonationService.test.ts index 72369a527..7753d5855 100644 --- a/src/services/chains/evm/draftDonationService.test.ts +++ b/src/services/chains/evm/draftDonationService.test.ts @@ -17,10 +17,10 @@ import { DONATION_STATUS, Donation, } from '../../../entities/donation'; -import { Project } from '../../../entities/project'; +import { Project, ProjectUpdate } from '../../../entities/project'; import { User } from '../../../entities/user'; -describe('draftDonationMatching', draftDonationMatchingTests); +describe.skip('draftDonationMatching', draftDonationMatchingTests); const RandomAddress1 = '0xf3ddeb5022a6f06b61488b48c90315087ca2beef'; const RandomAddress2 = '0xc42a4791735ae1253c50c6226832e37ede3669f5'; @@ -65,6 +65,7 @@ function draftDonationMatchingTests() { }); if (projectAddress) { await ProjectAddress.delete({ address: RandomAddress2 }); + await ProjectUpdate.delete({ projectId: projectAddress.projectId }); await Project.delete(projectAddress.projectId); } diff --git a/src/services/chains/evm/draftDonationService.ts b/src/services/chains/evm/draftDonationService.ts index 0c5d94299..b99e29c3c 100644 --- a/src/services/chains/evm/draftDonationService.ts +++ b/src/services/chains/evm/draftDonationService.ts @@ -1,11 +1,12 @@ import _ from 'lodash'; +import { ethers } from 'ethers'; +import { ModuleThread, Pool, spawn, Worker } from 'threads'; import { DRAFT_DONATION_STATUS, DraftDonation, } from '../../../entities/draftDonation'; import { getNetworkNativeToken } from '../../../provider'; import { getListOfTransactionsByAddress } from './transactionService'; -import { ethers } from 'ethers'; import { closeTo } from '..'; import { findTokenByNetworkAndAddress } from '../../../utils/tokenUtils'; import { ITxInfo } from '../../../types/etherscan'; @@ -13,7 +14,6 @@ import { DONATION_ORIGINS, Donation } from '../../../entities/donation'; import { DonationResolver } from '../../../resolvers/donationResolver'; import { ApolloContext } from '../../../types/ApolloContext'; import { logger } from '../../../utils/logger'; -import { ModuleThread, Pool, spawn, Worker } from 'threads'; import { DraftDonationWorker } from '../../../workers/draftDonationMatchWorker'; const transferErc20CallData = (to: string, amount: number, decimals = 18) => { diff --git a/src/services/chains/evm/draftRecurringDonationService.test.ts b/src/services/chains/evm/draftRecurringDonationService.test.ts new file mode 100644 index 000000000..48ca6e082 --- /dev/null +++ b/src/services/chains/evm/draftRecurringDonationService.test.ts @@ -0,0 +1,234 @@ +import { expect } from 'chai'; +import { + saveProjectDirectlyToDb, + createProjectData, + saveUserDirectlyToDb, + generateRandomEtheriumAddress, + generateRandomEvmTxHash, + saveRecurringDonationDirectlyToDb, +} from '../../../../test/testUtils'; +import { NETWORK_IDS } from '../../../provider'; +import { Project } from '../../../entities/project'; +import { User } from '../../../entities/user'; +import { + DraftRecurringDonation, + RECURRING_DONATION_ORIGINS, +} from '../../../entities/draftRecurringDonation'; +import { AnchorContractAddress } from '../../../entities/anchorContractAddress'; +import { + RECURRING_DONATION_STATUS, + RecurringDonation, +} from '../../../entities/recurringDonation'; +import { addNewAnchorAddress } from '../../../repositories/anchorContractAddressRepository'; +import { matchDraftRecurringDonations } from './draftRecurringDonationService'; + +describe('matchDraftRecurringDonations', matchDraftRecurringDonationsTests); + +function matchDraftRecurringDonationsTests() { + let project1: Project; + let anchorContractAddress1: AnchorContractAddress; + let user1: User; + + beforeEach(async () => { + project1 = await saveProjectDirectlyToDb(createProjectData()); + + user1 = await User.create({ + walletAddress: generateRandomEtheriumAddress(), + loginType: 'wallet', + firstName: 'first name', + }).save(); + + anchorContractAddress1 = await addNewAnchorAddress({ + project: project1, + owner: user1, + creator: user1, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + txHash: generateRandomEvmTxHash(), + }); + }); + + afterEach(async () => { + await DraftRecurringDonation.delete({}); + await RecurringDonation.delete({}); + await AnchorContractAddress.delete({}); + }); + + it('should create a recurring donation based on the draft donation OP Sepholia #1', async () => { + // https://sepolia-optimism.etherscan.io/tx/0x516567c51c3506afe1291f7055fa0e858cc2ca9ed4079625c747fe92bd125a10 + const user = await saveUserDirectlyToDb( + '0x871Cd6353B803CECeB090Bb827Ecb2F361Db81AB', + ); + const txHash = + '0x516567c51c3506afe1291f7055fa0e858cc2ca9ed4079625c747fe92bd125a10'; + anchorContractAddress1.address = + '0x1190f5ac0f509d8f3f4b662bf17437d37d64527c'; + anchorContractAddress1.isActive = true; + await anchorContractAddress1.save(); + + const draftRecurringDonation = await DraftRecurringDonation.create({ + projectId: project1!.id, + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + currency: 'ETH', + donorId: user!.id, + flowRate: '285225986', + }).save(); + const oneSecEarlierThanTx = new Date(1711283035000); + draftRecurringDonation.createdAt = oneSecEarlierThanTx; + await draftRecurringDonation.save(); + + expect(draftRecurringDonation).to.be.ok; + + await matchDraftRecurringDonations([draftRecurringDonation!]); + + const recurringDonation = await RecurringDonation.findOne({ + where: { + txHash, + }, + }); + + const updatedDraftDonation = await DraftRecurringDonation.findOne({ + where: { + id: draftRecurringDonation.id, + }, + }); + + expect(recurringDonation).to.be.ok; + + expect(recurringDonation?.txHash).to.be.equal(txHash); + expect(recurringDonation?.status).to.equal( + RECURRING_DONATION_STATUS.PENDING, + ); + expect(recurringDonation?.origin).to.equal( + RECURRING_DONATION_ORIGINS.DRAFT_RECURRING_DONATION_MATCHING, + ); + expect(updatedDraftDonation?.matchedRecurringDonationId).to.equal( + recurringDonation?.id, + ); + }); + + it('should create a recurring donation based on the draft donation OP Sepholia #1, update existing', async () => { + // https://sepolia-optimism.etherscan.io/tx/0x516567c51c3506afe1291f7055fa0e858cc2ca9ed4079625c747fe92bd125a10 + const user = await saveUserDirectlyToDb( + '0x871Cd6353B803CECeB090Bb827Ecb2F361Db81AB', + ); + const txHash = + '0x516567c51c3506afe1291f7055fa0e858cc2ca9ed4079625c747fe92bd125a10'; + anchorContractAddress1.address = + '0x1190f5ac0f509d8f3f4b662bf17437d37d64527c'; + anchorContractAddress1.isActive = true; + await anchorContractAddress1.save(); + + const existingRecurringDonation = await saveRecurringDonationDirectlyToDb({ + donationData: { + txHash: generateRandomEvmTxHash(), + projectId: project1!.id, + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + currency: 'ETH', + donorId: user!.id, + flowRate: '11111', + status: RECURRING_DONATION_STATUS.ACTIVE, + }, + }); + + const draftRecurringDonation = await DraftRecurringDonation.create({ + projectId: project1!.id, + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + matchedRecurringDonationId: existingRecurringDonation.id, + isForUpdate: true, + currency: 'ETH', + donorId: user!.id, + flowRate: '285225986', + }).save(); + + const oneSecEarlierThanTx = new Date(1711283035000); + draftRecurringDonation.createdAt = oneSecEarlierThanTx; + await draftRecurringDonation.save(); + + expect(draftRecurringDonation).to.be.ok; + + await matchDraftRecurringDonations([draftRecurringDonation!]); + + const recurringDonation = await RecurringDonation.findOne({ + where: { + txHash, + }, + }); + + const updatedDraftDonation = await DraftRecurringDonation.findOne({ + where: { + id: draftRecurringDonation.id, + }, + }); + + expect(recurringDonation).to.be.ok; + + expect(recurringDonation?.txHash).to.be.equal(txHash); + expect(recurringDonation?.flowRate).to.be.equal( + draftRecurringDonation.flowRate, + ); + expect(recurringDonation?.status).to.equal( + RECURRING_DONATION_STATUS.PENDING, + ); + expect(recurringDonation?.origin).to.equal( + RECURRING_DONATION_ORIGINS.DRAFT_RECURRING_DONATION_MATCHING, + ); + expect(updatedDraftDonation?.matchedRecurringDonationId).to.equal( + recurringDonation?.id, + ); + }); + + it('should create a recurring donation based on the draft donation OP Sepholia #2 batch', async () => { + // https://sepolia-optimism.etherscan.io/tx/0x1833603bc894448b54cf9c03483fa361508fa101abcfa6c3b6ef51425cab533f + const user = await saveUserDirectlyToDb( + '0xa1179f64638adb613ddaac32d918eb6beb824104', + ); + const txHash = + '0x1833603bc894448b54cf9c03483fa361508fa101abcfa6c3b6ef51425cab533f'; + anchorContractAddress1.address = + '0xe6375bc298aEB29D173B2AB359D492439A43b268'; + anchorContractAddress1.isActive = true; + await anchorContractAddress1.save(); + + const draftRecurringDonation = await DraftRecurringDonation.create({ + projectId: project1!.id, + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + currency: 'ETH', + donorId: user!.id, + flowRate: '152207001', + }).save(); + const oneSecEarlierThanTx = new Date(1711264598000); + draftRecurringDonation.createdAt = oneSecEarlierThanTx; + draftRecurringDonation.isBatch = true; + await draftRecurringDonation.save(); + + expect(draftRecurringDonation).to.be.ok; + + await matchDraftRecurringDonations([draftRecurringDonation!]); + + const recurringDonation = await RecurringDonation.findOne({ + where: { + txHash, + }, + }); + + const updatedDraftDonation = await DraftRecurringDonation.findOne({ + where: { + id: draftRecurringDonation.id, + }, + }); + + expect(recurringDonation).to.be.ok; + + expect(recurringDonation?.txHash).to.be.equal(txHash); + expect(recurringDonation?.status).to.equal( + RECURRING_DONATION_STATUS.PENDING, + ); + expect(recurringDonation?.origin).to.equal( + RECURRING_DONATION_ORIGINS.DRAFT_RECURRING_DONATION_MATCHING, + ); + expect(updatedDraftDonation?.matchedRecurringDonationId).to.equal( + recurringDonation?.id, + ); + }); +} diff --git a/src/services/chains/evm/draftRecurringDonationService.ts b/src/services/chains/evm/draftRecurringDonationService.ts new file mode 100644 index 000000000..1c5333619 --- /dev/null +++ b/src/services/chains/evm/draftRecurringDonationService.ts @@ -0,0 +1,211 @@ +import { ModuleThread, Pool, spawn, Worker } from 'threads'; +import { WorkerModule } from 'threads/dist/types/worker'; +import { DRAFT_DONATION_STATUS } from '../../../entities/draftDonation'; +import { ApolloContext } from '../../../types/ApolloContext'; +import { logger } from '../../../utils/logger'; +import { + DRAFT_RECURRING_DONATION_STATUS, + DraftRecurringDonation, + RECURRING_DONATION_ORIGINS, +} from '../../../entities/draftRecurringDonation'; +import { + RECURRING_DONATION_STATUS, + RecurringDonation, +} from '../../../entities/recurringDonation'; +import { RecurringDonationResolver } from '../../../resolvers/recurringDonationResolver'; +import { findUserById } from '../../../repositories/userRepository'; +import { findActiveAnchorAddress } from '../../../repositories/anchorContractAddressRepository'; +import { findRecurringDonationById } from '../../../repositories/recurringDonationRepository'; +import { getSuperFluidAdapter } from '../../../adapters/adaptersFactory'; +import { FlowUpdatedEvent } from '../../../adapters/superFluid/superFluidAdapterInterface'; +import { convertTimeStampToSeconds } from '../../../utils/utils'; + +type DraftRecurringDonationWorkerFunctions = 'matchDraftRecurringDonations'; +export type DraftRecurringDonationWorker = + WorkerModule; + +export async function matchDraftRecurringDonations( + draftRecurringDonations: DraftRecurringDonation[], +) { + logger.debug( + 'matchDraftRecurringDonations() has been called draftDonation.length', + draftRecurringDonations.length, + ); + for (const draftRecurringDonation of draftRecurringDonations) { + try { + const anchorContractAddress = await findActiveAnchorAddress({ + networkId: draftRecurringDonation.networkId, + projectId: draftRecurringDonation.projectId, + }); + const donor = await findUserById(draftRecurringDonation.donorId); + const superFluidAdapter = getSuperFluidAdapter(); + + const getFlowParams = { + flowRate: draftRecurringDonation.flowRate, + receiver: anchorContractAddress?.address?.toLowerCase() as string, + sender: donor?.walletAddress?.toLowerCase() as string, + timestamp_gt: convertTimeStampToSeconds( + draftRecurringDonation.createdAt.getTime(), + ), + }; + const flow = + await superFluidAdapter.getFlowByReceiverSenderFlowRate(getFlowParams); + if (flow) { + logger.debug('matchDraftRecurringDonations flow: ', flow); + await submitMatchedDraftRecurringDonation(draftRecurringDonation, flow); + } else { + logger.error('matchDraftRecurringDonations flow is undefined', flow); + } + } catch (e) { + logger.error('error validating draftRecurringDonation', { + draftRecurringDonationId: + draftRecurringDonation.matchedRecurringDonationId, + flowRate: draftRecurringDonation?.flowRate, + }); + } + } +} + +async function submitMatchedDraftRecurringDonation( + draftRecurringDonation: DraftRecurringDonation, + tx: FlowUpdatedEvent, +) { + logger.debug( + 'submitMatchedDraftRecurringDonation() has been called', + draftRecurringDonation, + tx, + ); + // Check whether a donation with same networkId and txHash already exists + const existingRecurringDonation = await RecurringDonation.findOne({ + where: { + networkId: draftRecurringDonation.networkId, + txHash: tx.transactionHash, + projectId: draftRecurringDonation.projectId, + }, + }); + + if (existingRecurringDonation) { + // Check whether the donation has not been saved during matching procedure + await draftRecurringDonation.reload(); + if (draftRecurringDonation.status === DRAFT_DONATION_STATUS.PENDING) { + draftRecurringDonation.status = DRAFT_DONATION_STATUS.FAILED; + draftRecurringDonation.errorMessage = `Recurring donation with same networkId and txHash with ID ${existingRecurringDonation.id} already exists`; + await draftRecurringDonation.save(); + } + return; + } + + const recurringDonationResolver = new RecurringDonationResolver(); + + const { + flowRate, + networkId, + anonymous, + currency, + projectId, + isBatch, + matchedRecurringDonationId, + isForUpdate, + } = draftRecurringDonation; + const txHash = tx.transactionHash; + try { + logger.debug( + `Creating donation for draftDonation with ID ${draftRecurringDonation.id}`, + ); + let recurringDonation; + if (isForUpdate) { + const oldRecurringDonation = await findRecurringDonationById( + matchedRecurringDonationId!, + ); + recurringDonation = + await recurringDonationResolver.updateRecurringDonationParams( + { + req: { user: { userId: draftRecurringDonation.donorId }, auth: {} }, + } as ApolloContext, + projectId, + networkId, + currency, + + txHash, + flowRate, + anonymous, + oldRecurringDonation?.isArchived, + ); + } else { + recurringDonation = + await recurringDonationResolver.createRecurringDonation( + { + req: { user: { userId: draftRecurringDonation.donorId }, auth: {} }, + } as ApolloContext, + projectId, + networkId, + txHash, + currency, + flowRate, + anonymous, + isBatch, + ); + } + + await RecurringDonation.update(Number(recurringDonation.id), { + origin: RECURRING_DONATION_ORIGINS.DRAFT_RECURRING_DONATION_MATCHING, + status: RECURRING_DONATION_STATUS.PENDING, + }); + + await DraftRecurringDonation.update(draftRecurringDonation.id, { + matchedRecurringDonationId: recurringDonation.id, + }); + + logger.debug( + `Recurring donation with ID ${recurringDonation.id} has been created for draftRecurringDonation with ID ${draftRecurringDonation.id}`, + ); + // donation resolver does it + // draftDonation.status = DRAFT_DONATION_STATUS.MATCHED; + // draftDonation.matchedDonationId = Number(donationId); + } catch (e) { + logger.error( + `Error on creating donation for draftDonation with ID ${draftRecurringDonation.id}`, + e, + ); + draftRecurringDonation.status = DRAFT_RECURRING_DONATION_STATUS.FAILED; + draftRecurringDonation.errorMessage = e.message; + await draftRecurringDonation.save(); + } +} + +let workerIsIdle = true; +let pool: Pool>; + +export async function runDraftRecurringDonationMatchWorker() { + if (!workerIsIdle) { + logger.debug('Draft recurring donation matching worker is already running'); + return; + } + workerIsIdle = false; + + if (!pool) { + pool = Pool( + () => + spawn( + new Worker('./../../../workers/draftRecurringDonationMatchWorker'), + ), + { + name: 'draftRecurringDonationMatchWorker', + concurrency: 4, + size: 2, + }, + ); + } + try { + await pool.queue(draftRecurringDonationWorker => + draftRecurringDonationWorker.matchDraftRecurringDonations(), + ); + await pool.settled(true); + } catch (e) { + logger.error( + `error in calling draft recurring donation match worker: ${e.message}`, + ); + } finally { + workerIsIdle = true; + } +} diff --git a/src/services/chains/evm/transactionService.test.ts b/src/services/chains/evm/transactionService.test.ts index 0e40cc622..6bf2387f4 100644 --- a/src/services/chains/evm/transactionService.test.ts +++ b/src/services/chains/evm/transactionService.test.ts @@ -1,14 +1,19 @@ import { assert } from 'chai'; import 'mocha'; -import { getDisperseTransactions } from './transactionService'; +import { + getDisperseTransactions, + getEvmTransactionTimestamp, +} from './transactionService'; import { assertThrowsAsync } from '../../../../test/testUtils'; -import { errorMessages } from '../../../utils/errorMessages'; import { NETWORK_IDS } from '../../../provider'; -import moment from 'moment'; describe( 'getDisperseTransactions test cases', getDisperseTransactionsTestCases, ); +describe( + 'getEvmTransactionTimestamp test cases', + getEvmTransactionTimestampTestCases, +); function getDisperseTransactionsTestCases() { it('Should return transactions, for disperseEther on xdai', async () => { @@ -90,3 +95,24 @@ function getDisperseTransactionsTestCases() { // assert.equal(transactions[3].currency, 'USDC'); // }); } + +function getEvmTransactionTimestampTestCases() { + it('Should return the transaction time from the blockchain', async () => { + // https://blockscout.com/xdai/mainnet/tx/0x42c0f15029557ec35e61515a89366297fc239a334e3ba22fab15a3f1d04ad53f + const transactionTime = await getEvmTransactionTimestamp({ + txHash: + '0x42c0f15029557ec35e61515a89366297fc239a334e3ba22fab15a3f1d04ad53f', + networkId: NETWORK_IDS.XDAI, + }); + assert.equal(transactionTime, 1702091620); + }); + + it('Should throw error if the transaction is not found', async () => { + await assertThrowsAsync(async () => { + await getEvmTransactionTimestamp({ + txHash: '0x', + networkId: NETWORK_IDS.XDAI, + }); + }, 'Transaction not found'); + }); +} diff --git a/src/services/chains/evm/transactionService.ts b/src/services/chains/evm/transactionService.ts index 2c7ff085d..aa3f3ad32 100644 --- a/src/services/chains/evm/transactionService.ts +++ b/src/services/chains/evm/transactionService.ts @@ -1,13 +1,14 @@ import abiDecoder from 'abi-decoder'; +import axios from 'axios'; import { findTokenByNetworkAndAddress, findTokenByNetworkAndSymbol, } from '../../../utils/tokenUtils'; import { + errorMessages, i18n, translationErrorMessagesKeys, } from '../../../utils/errorMessages'; -import axios from 'axios'; import { erc20ABI } from '../../../assets/erc20ABI'; import { disperseABI } from '../../../assets/disperseABI'; import { @@ -21,10 +22,9 @@ import { gnosisSafeL2ABI } from '../../../assets/gnosisSafeL2ABI'; import { NetworkTransactionInfo, TransactionDetailInput } from '../index'; import { normalizeAmount } from '../../../utils/utils'; import { ONE_HOUR, validateTransactionWithInputData } from '../index'; -import _ from 'lodash'; import { ITxInfo } from '../../../types/etherscan'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const ethers = require('ethers'); abiDecoder.addABI(erc20ABI); abiDecoder.addABI(gnosisSafeL2ABI); @@ -180,6 +180,8 @@ export async function getListOfTransactionsByAddress(input: { logger.debug( 'NODE RPC request count - getTransactionDetailForTokenTransfer provider.getTransaction fromAddress:', address, + networkId, + offset, ); const result = await axios.get(getBlockExplorerApiUrl(networkId), { params: { @@ -191,6 +193,23 @@ export async function getListOfTransactionsByAddress(input: { sort: 'desc', }, }); + + if (result?.data?.status === '0') { + // https://docs.gnosisscan.io/support/common-error-messages + + /** + * sample of these errors + { + "status": "0", + "message": "Query Timeout occured. Please select a smaller result dataset", + "result": null + } + */ + throw new Error( + result.data?.message || + `Error while fetching transactions networkId: ${networkId}`, + ); + } const userRecentTransactions = result.data.result.filter(tx => { return tx.from.toLowerCase() === input.address.toLowerCase(); }); @@ -201,6 +220,30 @@ export async function getListOfTransactionsByAddress(input: { }; } +export async function getEvmTransactionTimestamp(input: { + txHash: string; + networkId: number; +}): Promise { + try { + const { txHash, networkId } = input; + logger.debug( + 'NODE RPC request count - getTransactionTimeFromBlockchain provider.getTransaction txHash:', + input.txHash, + ); + const transaction = await getProvider(networkId).getTransaction(txHash); + if (!transaction) { + throw new Error(errorMessages.TRANSACTION_NOT_FOUND); + } + const block = await getProvider(networkId).getBlock( + transaction.blockNumber as number, + ); + return block.timestamp as number; + } catch (e) { + logger.error('getTransactionTimeFromBlockchain error', e); + throw new Error(errorMessages.TRANSACTION_NOT_FOUND); + } +} + async function getTransactionDetailForNormalTransfer( input: TransactionDetailInput, ): Promise { @@ -248,7 +291,7 @@ async function getTransactionDetailForNormalTransfer( const events = decodedLogs[0].events; transactionTo = events[0]?.value?.toLowerCase(); - transactionFrom = decodedLogs[0]?.address!; + transactionFrom = decodedLogs[0]?.address; amount = normalizeAmount(events[1]?.value, token.decimals); if (!transactionTo || !transactionFrom) { diff --git a/src/services/chains/index.test.ts b/src/services/chains/index.test.ts index f995eef98..b96c298f4 100644 --- a/src/services/chains/index.test.ts +++ b/src/services/chains/index.test.ts @@ -1,10 +1,10 @@ -import { NETWORK_IDS } from '../../provider'; import { assert } from 'chai'; +import moment from 'moment'; +import { NETWORK_IDS } from '../../provider'; import { assertThrowsAsync } from '../../../test/testUtils'; import { errorMessages } from '../../utils/errorMessages'; -import moment from 'moment'; -import { getTransactionInfoFromNetwork } from './index'; import { ChainType } from '../../types/network'; +import { getTransactionInfoFromNetwork } from './index'; const ONE_DAY = 60 * 60 * 24; @@ -88,6 +88,7 @@ function getTransactionDetailTestCases() { }); it('should return transaction detail for DAI transfer on ethereum classic', async () => { // https://etc.blockscout.com/tx/0x48e0c03ed99996fac3a7ecaaf05a1582a9191d8e37b6ebdbdd630b83350faf63 + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision const amount = 4492.059297640078891631; const transactionInfo = await getTransactionInfoFromNetwork({ txHash: @@ -424,97 +425,6 @@ function getTransactionDetailTestCases() { ); }); - // it('should return error when transaction amount is different with donation amount', async () => { - // // https://etherscan.io/tx/0x5b80133493a5be96385f00ce22a69c224e66fa1fc52b3b4c33e9057f5e873f49 - // const amount = 1730; - // const badFunc = async () => { - // await getTransactionInfoFromNetwork({ - // txHash: - // '0x5b80133493a5be96385f00ce22a69c224e66fa1fc52b3b4c33e9057f5e873f49', - // symbol: 'DAI', - // networkId: NETWORK_IDS.MAIN_NET, - // fromAddress: '0x5ac583feb2b1f288c0a51d6cdca2e8c814bfe93b', - // toAddress: '0x2Ea846Dc38C6b6451909F1E7ff2bF613a96DC1F3', - // amount, - // nonce: 4, - // timestamp: 1624772582, - // }); - // }; - // await assertThrowsAsync( - // badFunc, - // errorMessages.TRANSACTION_AMOUNT_IS_DIFFERENT_WITH_SENT_AMOUNT, - // ); - // }); - - // Getting 503 in github actions, so I had to comment this - - // it('should return transaction detail for DAI token transfer on mainnet when transaction is invalid but speedup', - // async () => { - // // https://etherscan.io/tx/0x5b80133493a5be96385f00ce22a69c224e66fa1fc52b3b4c33e9057f5e873f49 - // const amount = 1760; - // const txHash = - // '0x5b80133493a5be96385f00ce22a69c224e66fa1fc52b3b4c33e9057f5e871229'; - // const transactionInfo = await getTransactionInfoFromNetwork({ - // txHash, - // symbol: 'DAI', - // networkId: NETWORK_IDS.MAIN_NET, - // fromAddress: '0x5ac583feb2b1f288c0a51d6cdca2e8c814bfe93b', - // toAddress: '0x2Ea846Dc38C6b6451909F1E7ff2bF613a96DC1F3', - // amount, - // nonce: 4, - // timestamp: 1624772582, - // }); - // assert.isOk(transactionInfo); - // assert.equal(transactionInfo.currency, 'DAI'); - // assert.equal(transactionInfo.amount, amount); - // assert.notEqual(transactionInfo.hash, txHash); - // }); - - // TODO: Rewrite this test for goerli or delete - // it('should return transaction detail for UNI token transfer on ropsten', async () => { - // // https://ropsten.etherscan.io/tx/0xba3c2627c9d3dd963455648b4f9d7239e8b5c80d0aa85ac354d2b762d99e4441 - // const amount = 0.01; - // const transactionInfo = await getTransactionInfoFromNetwork({ - // txHash: - // '0xba3c2627c9d3dd963455648b4f9d7239e8b5c80d0aa85ac354d2b762d99e4441', - // symbol: 'UNI', - // networkId: NETWORK_IDS.ROPSTEN, - // fromAddress: '0x826976d7c600d45fb8287ca1d7c76fc8eb732030', - // toAddress: '0x8f951903c9360345b4e1b536c7f5ae8f88a64e79', - // amount, - // timestamp: 1615739937, - // }); - // assert.isOk(transactionInfo); - // assert.equal(transactionInfo.currency, 'UNI'); - // assert.equal(transactionInfo.amount, amount); - // }); - - // TODO: Rewrite for goerli or delete - // it('should return transaction when transactionHash is wrong because of speedup on ropsten', async () => { - // // https://ropsten.etherscan.io/tx/0xd65478445fa41679fc5fd2a171f56a71a2f006a2246d4b408be97a251e330da7 - // const amount = 0.001; - // const txHash = - // '0xd65478445fa41679fc5fd2a171f56a71a2f006a2246d4b408be97a251e331234'; - // const transactionInfo = await getTransactionInfoFromNetwork({ - // txHash, - // symbol: 'ETH', - // networkId: NETWORK_IDS.ROPSTEN, - // fromAddress: '0xb20a327c9b4da091f454b1ce0e2e4dc5c128b5b4', - // toAddress: '0x5d28fe1e9f895464aab52287d85ebff32b351674', - // amount, - // nonce: 70, - // timestamp: 1621072452, - // }); - // assert.isOk(transactionInfo); - // assert.equal(transactionInfo.currency, 'ETH'); - // assert.equal(transactionInfo.amount, amount); - // assert.notEqual(transactionInfo.hash, txHash); - // assert.equal( - // transactionInfo.hash, - // '0xd65478445fa41679fc5fd2a171f56a71a2f006a2246d4b408be97a251e330da7', - // ); - // }); - it('should return transaction detail for normal transfer on polygon', async () => { // https://polygonscan.com/tx/0x16f122ad45705dfa41bb323c3164b6d840cbb0e9fa8b8e58bd7435370f8bbfc8 @@ -534,6 +444,25 @@ function getTransactionDetailTestCases() { assert.equal(transactionInfo.amount, amount); }); + it('should return transaction detail for normal transfer on optimism-sepolia', async () => { + // https://sepolia-optimism.etherscan.io/tx/0x1b4e9489154a499cd7d0bd7a097e80758e671a32f98559be3b732553afb00809 + + const amount = 0.01; + const transactionInfo = await getTransactionInfoFromNetwork({ + txHash: + '0x1b4e9489154a499cd7d0bd7a097e80758e671a32f98559be3b732553afb00809', + symbol: 'ETH', + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + fromAddress: '0x625bcc1142e97796173104a6e817ee46c593b3c5', + toAddress: '0x73f9b3f48ebc96ac55cb76c11053b068669a8a67', + amount, + timestamp: 1708954960, + }); + assert.isOk(transactionInfo); + assert.equal(transactionInfo.currency, 'ETH'); + assert.equal(transactionInfo.amount, amount); + }); + it('should return transaction detail for normal transfer on CELO', async () => { // https://celoscan.io/tx/0xa2a282cf6a7dec8b166aa52ac3d00fcd15a370d414615e29a168cfbb592e3637 diff --git a/src/services/chains/solana/transactionService.ts b/src/services/chains/solana/transactionService.ts index ccb9e5921..8c92325c1 100644 --- a/src/services/chains/solana/transactionService.ts +++ b/src/services/chains/solana/transactionService.ts @@ -1,5 +1,5 @@ -import { logger } from '../../../utils/logger'; import SolanaWeb3, { ParsedInstruction } from '@solana/web3.js'; +import { logger } from '../../../utils/logger'; import { NetworkTransactionInfo, TransactionDetailInput, @@ -10,10 +10,7 @@ import { i18n, translationErrorMessagesKeys, } from '../../../utils/errorMessages'; -import { - findTokenByNetworkAndAddress, - findTokenByNetworkAndSymbol, -} from '../../../utils/tokenUtils'; +import { findTokenByNetworkAndSymbol } from '../../../utils/tokenUtils'; import { NETWORK_IDS } from '../../../provider'; const solanaProviders = new Map(); diff --git a/src/services/chainvineReferralService.ts b/src/services/chainvineReferralService.ts index 3628f6ed9..68424961a 100644 --- a/src/services/chainvineReferralService.ts +++ b/src/services/chainvineReferralService.ts @@ -1,7 +1,7 @@ +import moment from 'moment'; import { getChainvineAdapter } from '../adapters/adaptersFactory'; import { getRoundNumberByDate } from '../utils/powerBoostingUtils'; import { isFirstTimeDonor } from '../repositories/userRepository'; -import moment from 'moment'; import { logger } from '../utils/logger'; import { findReferredEventByUserId } from '../repositories/referredEventRepository'; diff --git a/src/services/changeAPI/nonProfits.ts b/src/services/changeAPI/nonProfits.ts index 16bfefe12..77541d062 100644 --- a/src/services/changeAPI/nonProfits.ts +++ b/src/services/changeAPI/nonProfits.ts @@ -1,4 +1,4 @@ -import Axios, { AxiosResponse } from 'axios'; +import Axios from 'axios'; import slugify from 'slugify'; import config from '../../config'; import { Organization, ORGANIZATION_LABELS } from '../../entities/organization'; @@ -9,11 +9,7 @@ import { ReviewStatus, } from '../../entities/project'; import { ProjectStatus } from '../../entities/projectStatus'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; import { logger } from '../../utils/logger'; import { getAppropriateSlug, getQualityScore } from '../projectService'; import { findUserById } from '../../repositories/userRepository'; @@ -58,7 +54,7 @@ interface ChangeNonProfit { // exact title returns 1 element export const getChangeNonProfitByNameOrIEN = async ( - nonProfit: String, + nonProfit: string, ): Promise => { try { const result = await Axios.get(changeApiNonProfitUrl, { diff --git a/src/services/cronJobs/backupDonationImport.test.ts b/src/services/cronJobs/backupDonationImport.test.ts index b88fd23a2..1fecba3d7 100644 --- a/src/services/cronJobs/backupDonationImport.test.ts +++ b/src/services/cronJobs/backupDonationImport.test.ts @@ -1,16 +1,14 @@ +import { assert } from 'chai'; import { createBackupDonation } from './backupDonationImportJob'; import { assertThrowsAsync, createProjectData, generateRandomEtheriumAddress, generateRandomEvmTxHash, - generateTestAccessToken, - graphqlUrl, saveProjectDirectlyToDb, } from '../../../test/testUtils'; import { User } from '../../entities/user'; import { NETWORK_IDS } from '../../provider'; -import { assert } from 'chai'; import { DONATION_STATUS } from '../../entities/donation'; import { findTokenByNetworkAndSymbol } from '../../utils/tokenUtils'; diff --git a/src/services/cronJobs/backupDonationImportJob.ts b/src/services/cronJobs/backupDonationImportJob.ts index e603ebf75..8071025f6 100644 --- a/src/services/cronJobs/backupDonationImportJob.ts +++ b/src/services/cronJobs/backupDonationImportJob.ts @@ -1,7 +1,7 @@ +import { schedule } from 'node-cron'; import config from '../../config'; import { logger } from '../../utils/logger'; -import { schedule } from 'node-cron'; import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; import { findUserByWalletAddress } from '../../repositories/userRepository'; import { Donation } from '../../entities/donation'; diff --git a/src/services/cronJobs/checkActiveStatusQfRounds.ts b/src/services/cronJobs/checkActiveStatusQfRounds.ts index b4cb529df..c47957fad 100644 --- a/src/services/cronJobs/checkActiveStatusQfRounds.ts +++ b/src/services/cronJobs/checkActiveStatusQfRounds.ts @@ -1,7 +1,7 @@ +import { schedule } from 'node-cron'; import config from '../../config'; import { logger } from '../../utils/logger'; -import { schedule } from 'node-cron'; -import { isTestEnv, sleep } from '../../utils/utils'; +import { isTestEnv } from '../../utils/utils'; import { deactivateExpiredQfRounds, getExpiredActiveQfRounds, diff --git a/src/services/cronJobs/checkProjectVerificationStatus.test.ts b/src/services/cronJobs/checkProjectVerificationStatus.test.ts index 35e1b6563..9929effbf 100644 --- a/src/services/cronJobs/checkProjectVerificationStatus.test.ts +++ b/src/services/cronJobs/checkProjectVerificationStatus.test.ts @@ -1,19 +1,14 @@ -import { Project, RevokeSteps } from '../../entities/project'; -import { - ProjectStatusHistory, - HISTORY_DESCRIPTIONS, -} from '../../entities/projectStatusHistory'; import { assert } from 'chai'; +import { RevokeSteps } from '../../entities/project'; + import { checkProjectVerificationStatus } from './checkProjectVerificationStatus'; import { createProjectData, saveProjectDirectlyToDb, } from '../../../test/testUtils'; import { findProjectById } from '../../repositories/projectRepository'; -import { createProjectVerificationForm } from '../../repositories/projectVerificationRepository'; -import { PROJECT_VERIFICATION_STATUSES } from '../../entities/projectVerificationForm'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const moment = require('moment'); describe( @@ -24,43 +19,15 @@ describe( // set days to 60 for test env // main projectupdate also counts towards updates not only normal updates function checkProjectVerificationStatusTestCases() { - it('should send a reminder when project update is more than 30 days old', async () => { - const remindableProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - verified: true, - updatedAt: moment().subtract(31, 'days').endOf('day').toDate(), - projectUpdateCreationDate: moment().subtract(31, 'days').endOf('day'), - }); - const nonRevokableProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - verified: true, - }); - await checkProjectVerificationStatus(); - - const reminderProjectUpdated = await findProjectById(remindableProject.id); - const nonRevokableProjectUpdated = await findProjectById( - nonRevokableProject.id, - ); - - assert.isTrue(reminderProjectUpdated?.verified); - assert.equal( - reminderProjectUpdated!.verificationStatus, - RevokeSteps.Reminder, - ); - assert.isTrue(nonRevokableProjectUpdated!.verified); - }); - it('should send a warning when project update is more than 60 days old', async () => { + it('should send a warning when project update is more than 45 days old', async () => { const warnableProject = await saveProjectDirectlyToDb({ ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), verified: true, - updatedAt: moment().subtract(61, 'days').endOf('day').toDate(), - projectUpdateCreationDate: moment().subtract(61, 'days').endOf('day'), + updatedAt: moment().subtract(46, 'days').endOf('day').toDate(), + projectUpdateCreationDate: moment().subtract(46, 'days').endOf('day'), + verificationStatus: RevokeSteps.Reminder, }); await checkProjectVerificationStatus(); @@ -73,8 +40,8 @@ function checkProjectVerificationStatusTestCases() { RevokeSteps.Warning, ); }); - it('should send last warning if project was warned and 30 days past', async () => { - const lastWarningProject = await saveProjectDirectlyToDb({ + it('should send a last chance warning when project update is more than 90 days old', async () => { + const warnableProject = await saveProjectDirectlyToDb({ ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), @@ -86,18 +53,16 @@ function checkProjectVerificationStatusTestCases() { await checkProjectVerificationStatus(); - const lastWarningProjectUpdated = await findProjectById( - lastWarningProject.id, - ); + const warnableProjectUpdate = await findProjectById(warnableProject.id); - assert.isTrue(lastWarningProjectUpdated!.verified); + assert.isTrue(warnableProjectUpdate!.verified); assert.equal( - lastWarningProjectUpdated!.verificationStatus, + warnableProjectUpdate!.verificationStatus, RevokeSteps.LastChance, ); }); - it('should revoke project verification after last chance time frame expired', async () => { - const revokableProject = await saveProjectDirectlyToDb({ + it('should change project verificationStatus to upForRevoking after last chance time frame expired', async () => { + const lastWarningProject = await saveProjectDirectlyToDb({ ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), @@ -107,126 +72,149 @@ function checkProjectVerificationStatusTestCases() { verificationStatus: RevokeSteps.LastChance, }); - const projectVerificationForm = await createProjectVerificationForm({ - projectId: revokableProject.id, - userId: Number(revokableProject.admin), - }); - - projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.VERIFIED; - await projectVerificationForm.save(); - await checkProjectVerificationStatus(); - const revokableProjectUpdated = await Project.createQueryBuilder('project') - .leftJoinAndSelect( - 'project.projectVerificationForm', - 'projectVerificationForm', - ) - .where('project.id = :id', { id: revokableProject.id }) - .getOne(); - - assert.isFalse(revokableProjectUpdated!.verified); - assert.equal( - revokableProjectUpdated!.verificationStatus, - RevokeSteps.Revoked, - ); - - const revokableProjectHistory = - await ProjectStatusHistory.createQueryBuilder('project_status_history') - .where('project_status_history.projectId = :projectId', { - projectId: revokableProjectUpdated!.id, - }) - .getOne(); - - assert.isNotEmpty(revokableProjectHistory); - assert.equal( - revokableProjectHistory!.description, - HISTORY_DESCRIPTIONS.CHANGED_TO_UNVERIFIED_BY_CRONJOB, - ); - - // set project verification as draft - assert.notEqual( - projectVerificationForm.status, - revokableProjectUpdated?.projectVerificationForm?.status, + const lastWarningProjectUpdated = await findProjectById( + lastWarningProject.id, ); - }); - it('should warn projects that update already expired when feature release', async () => { - const expiredProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - verified: true, - updatedAt: moment().subtract(105, 'days').endOf('day').toDate(), - projectUpdateCreationDate: moment().subtract(105, 'days').endOf('day'), - }); - - await checkProjectVerificationStatus(); - - const expiredProjectUpdated = await findProjectById(expiredProject.id); - assert.isTrue(expiredProjectUpdated!.verified); + assert.isTrue(lastWarningProjectUpdated!.verified); assert.equal( - expiredProjectUpdated!.verificationStatus, + lastWarningProjectUpdated!.verificationStatus, RevokeSteps.UpForRevoking, ); }); - it('should revoke project verification after expired projects time frame is over', async () => { - const expiredRevokableProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - verified: true, - updatedAt: moment().subtract(300, 'days').endOf('day').toDate(), - projectUpdateCreationDate: moment().subtract(300, 'days').endOf('day'), - verificationStatus: RevokeSteps.UpForRevoking, - }); - - const projectVerificationForm = await createProjectVerificationForm({ - projectId: expiredRevokableProject.id, - userId: Number(expiredRevokableProject.admin), - }); - - projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.VERIFIED; - await projectVerificationForm.save(); - - // setup an old date in the test.env (last year), so this is instantly revoked - - await checkProjectVerificationStatus(); - - const expiredRevokableProjectUpdated = await Project.createQueryBuilder( - 'project', - ) - .leftJoinAndSelect( - 'project.projectVerificationForm', - 'projectVerificationForm', - ) - .where('project.id = :id', { id: expiredRevokableProject.id }) - .getOne(); - - assert.isFalse(expiredRevokableProjectUpdated!.verified); - assert.equal( - expiredRevokableProjectUpdated!.verificationStatus, - RevokeSteps.Revoked, - ); - - const expiredProjectHistory = await ProjectStatusHistory.createQueryBuilder( - 'project_status_history', - ) - .where('project_status_history.projectId = :projectId', { - projectId: expiredRevokableProjectUpdated!.id, - }) - .getOne(); - - assert.isNotEmpty(expiredProjectHistory); - assert.equal( - expiredProjectHistory!.description, - HISTORY_DESCRIPTIONS.CHANGED_TO_UNVERIFIED_BY_CRONJOB, - ); - - // set project verification as draft - assert.notEqual( - projectVerificationForm.status, - expiredRevokableProjectUpdated?.projectVerificationForm?.status, - ); - }); + // it('should revoke project verification after last chance time frame expired', async () => { + // const revokableProject = await saveProjectDirectlyToDb({ + // ...createProjectData(), + // title: String(new Date().getTime()), + // slug: String(new Date().getTime()), + // verified: true, + // updatedAt: moment().subtract(105, 'days').endOf('day').toDate(), + // projectUpdateCreationDate: moment().subtract(105, 'days').endOf('day'), + // verificationStatus: RevokeSteps.LastChance, + // }); + // + // const projectVerificationForm = await createProjectVerificationForm({ + // projectId: revokableProject.id, + // userId: Number(revokableProject.admin), + // }); + // + // projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.VERIFIED; + // await projectVerificationForm.save(); + // + // await checkProjectVerificationStatus(); + // + // const revokableProjectUpdated = await Project.createQueryBuilder('project') + // .leftJoinAndSelect( + // 'project.projectVerificationForm', + // 'projectVerificationForm', + // ) + // .where('project.id = :id', { id: revokableProject.id }) + // .getOne(); + // + // assert.isFalse(revokableProjectUpdated!.verified); + // assert.equal( + // revokableProjectUpdated!.verificationStatus, + // RevokeSteps.Revoked, + // ); + // + // const revokableProjectHistory = + // await ProjectStatusHistory.createQueryBuilder('project_status_history') + // .where('project_status_history.projectId = :projectId', { + // projectId: revokableProjectUpdated!.id, + // }) + // .getOne(); + // + // assert.isNotEmpty(revokableProjectHistory); + // assert.equal( + // revokableProjectHistory!.description, + // HISTORY_DESCRIPTIONS.CHANGED_TO_UNVERIFIED_BY_CRONJOB, + // ); + // + // // set project verification as draft + // assert.notEqual( + // projectVerificationForm.status, + // revokableProjectUpdated?.projectVerificationForm?.status, + // ); + // }); + // it('should warn projects that update already expired when feature release', async () => { + // const expiredProject = await saveProjectDirectlyToDb({ + // ...createProjectData(), + // title: String(new Date().getTime()), + // slug: String(new Date().getTime()), + // verified: true, + // updatedAt: moment().subtract(105, 'days').endOf('day').toDate(), + // projectUpdateCreationDate: moment().subtract(105, 'days').endOf('day'), + // }); + // + // await checkProjectVerificationStatus(); + // + // const expiredProjectUpdated = await findProjectById(expiredProject.id); + // + // assert.isTrue(expiredProjectUpdated!.verified); + // assert.equal( + // expiredProjectUpdated!.verificationStatus, + // RevokeSteps.UpForRevoking, + // ); + // }); + // it('should revoke project verification after expired projects time frame is over', async () => { + // const expiredRevokableProject = await saveProjectDirectlyToDb({ + // ...createProjectData(), + // title: String(new Date().getTime()), + // slug: String(new Date().getTime()), + // verified: true, + // updatedAt: moment().subtract(300, 'days').endOf('day').toDate(), + // projectUpdateCreationDate: moment().subtract(300, 'days').endOf('day'), + // verificationStatus: RevokeSteps.UpForRevoking, + // }); + // + // const projectVerificationForm = await createProjectVerificationForm({ + // projectId: expiredRevokableProject.id, + // userId: Number(expiredRevokableProject.admin), + // }); + // + // projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.VERIFIED; + // await projectVerificationForm.save(); + // + // // setup an old date in the test.env (last year), so this is instantly revoked + // + // await checkProjectVerificationStatus(); + // + // const expiredRevokableProjectUpdated = await Project.createQueryBuilder( + // 'project', + // ) + // .leftJoinAndSelect( + // 'project.projectVerificationForm', + // 'projectVerificationForm', + // ) + // .where('project.id = :id', { id: expiredRevokableProject.id }) + // .getOne(); + // + // assert.isFalse(expiredRevokableProjectUpdated!.verified); + // assert.equal( + // expiredRevokableProjectUpdated!.verificationStatus, + // RevokeSteps.Revoked, + // ); + // + // const expiredProjectHistory = await ProjectStatusHistory.createQueryBuilder( + // 'project_status_history', + // ) + // .where('project_status_history.projectId = :projectId', { + // projectId: expiredRevokableProjectUpdated!.id, + // }) + // .getOne(); + // + // assert.isNotEmpty(expiredProjectHistory); + // assert.equal( + // expiredProjectHistory!.description, + // HISTORY_DESCRIPTIONS.CHANGED_TO_UNVERIFIED_BY_CRONJOB, + // ); + // + // // set project verification as draft + // assert.notEqual( + // projectVerificationForm.status, + // expiredRevokableProjectUpdated?.projectVerificationForm?.status, + // ); + // }); } diff --git a/src/services/cronJobs/checkProjectVerificationStatus.ts b/src/services/cronJobs/checkProjectVerificationStatus.ts index 447a5d01f..9351f8a5c 100644 --- a/src/services/cronJobs/checkProjectVerificationStatus.ts +++ b/src/services/cronJobs/checkProjectVerificationStatus.ts @@ -1,10 +1,9 @@ import { schedule } from 'node-cron'; +import moment = require('moment'); import { Project, RevokeSteps } from '../../entities/project'; import { HISTORY_DESCRIPTIONS } from '../../entities/projectStatusHistory'; -import { User } from '../../entities/user'; import config from '../../config'; import { logger } from '../../utils/logger'; -import moment = require('moment'); import { projectsWithoutUpdateAfterTimeFrame } from '../../repositories/projectRepository'; import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; @@ -17,20 +16,13 @@ import { refreshProjectPowerView, } from '../../repositories/projectPowerViewRepository'; -// Every 3 months if no project verification was added, the project -// Verification status will be revoked -// Every other month an email will be sent to notify owners to do updates const cronJobTime = (config.get( 'CHECK_PROJECT_VERIFICATION_STATUS_CRONJOB_EXPRESSION', ) as string) || '0 0 * * 0'; -const projectUpdatesReminderDays = Number( - config.get('PROJECT_UPDATES_VERIFIED_REMINDER_DAYS') || 30, -); - const projectUpdatesWarningDays = Number( - config.get('PROJECT_UPDATES_VERIFIED_WARNING_DAYS') || 60, + config.get('PROJECT_UPDATES_VERIFIED_WARNING_DAYS') || 45, ); const projectUpdatesLastWarningDays = Number( @@ -41,19 +33,6 @@ const projectUpdatesRevokeVerificationDays = Number( config.get('PROJECT_UPDATES_VERIFIED_REVOKE_DAYS') || 104, ); -const projectUpdatesExpiredRevokeAdditionalDays = Number( - config.get('PROJECT_UPDATES_EXPIRED_ADDITIONAL_REVOKE_DAYS') || 30, -); - -const projectUpdatesFirstRevokeBatchDate = String( - config.get('PROJECT_UPDATES_FIRST_REVOKE_BATCH_DATE') || '2022-10-22', -); - -const maxDaysForSendingUpdateReminder = moment() - .subtract(projectUpdatesReminderDays, 'days') - .endOf('day') - .toDate(); - const maxDaysForSendingUpdateWarning = moment() .subtract(projectUpdatesWarningDays, 'days') .endOf('day') @@ -77,13 +56,19 @@ export const runCheckProjectVerificationStatus = () => { }; export const checkProjectVerificationStatus = async () => { - // all projects with last update created at 30+ days + // all projects with last update created at +45 days const projects = await projectsWithoutUpdateAfterTimeFrame( - maxDaysForSendingUpdateReminder, + maxDaysForSendingUpdateWarning, ); - logger.debug('checkProjectVerificationStatus()', { - maxDaysForSendingUpdateReminder, - foundProjectsCount: projects?.length, + logger.debug('checkProjectVerificationStatus() has been called', { + maxDaysForSendingUpdateWarning, + foundProjectsCount: projects.length, + projects: projects.map(p => { + return { + slug: p.slug, + verificationStatus: p.verificationStatus, + }; + }), }); for (const project of projects) { @@ -109,63 +94,46 @@ export const checkProjectVerificationStatus = async () => { }; const remindUpdatesOrRevokeVerification = async (project: Project) => { + // We don't revoke verification badge for any projects. + const latestUpdate = + project.projectUpdates?.[0].createdAt || project.updatedAt; logger.debug('remindUpdatesOrRevokeVerification() has been called', { projectId: project.id, projectSlug: project.slug, projectVerificationStatus: project.verificationStatus, + latestUpdate, }); - // Projects up for revoking when 30 days are done after feature release + const { verificationStatus } = project; + let newVerificationStatus = verificationStatus?.slice(); if ( - new Date() >= new Date(projectUpdatesFirstRevokeBatchDate) && - project.verificationStatus === RevokeSteps.UpForRevoking - ) { - project.verificationStatus = RevokeSteps.Revoked; - project.verified = false; - } else if ( - // Projects that already expired verification are given a last chance - // for this feature - project.updatedAt <= maxDaysForSendingUpdateLastWarning && - project.verificationStatus === null - ) { - project.verificationStatus = RevokeSteps.UpForRevoking; - } else if ( - // Projects that had the last chance and failed to add an update are revoked - project.updatedAt <= maxDaysForRevokingBadge && - project.verificationStatus === RevokeSteps.LastChance + (!verificationStatus || verificationStatus === RevokeSteps.Reminder) && + latestUpdate <= maxDaysForSendingUpdateWarning ) { - project.verificationStatus = RevokeSteps.Revoked; - project.verified = false; + newVerificationStatus = RevokeSteps.Warning; } else if ( - // projects that were warned are sent a last chance warning - project.updatedAt <= maxDaysForSendingUpdateLastWarning && - project.updatedAt > maxDaysForRevokingBadge && - project.verificationStatus === RevokeSteps.Warning + latestUpdate <= maxDaysForSendingUpdateLastWarning && + verificationStatus === RevokeSteps.Warning ) { - project.verificationStatus = RevokeSteps.LastChance; + newVerificationStatus = RevokeSteps.LastChance; } else if ( - // After reminder at 60/75 days - project.updatedAt <= maxDaysForSendingUpdateWarning && - project.updatedAt > maxDaysForSendingUpdateLastWarning && - project.verificationStatus !== RevokeSteps.Warning + latestUpdate <= maxDaysForRevokingBadge && + verificationStatus === RevokeSteps.LastChance ) { - project.verificationStatus = RevokeSteps.Warning; - } else if ( - // First email for reminding to add an update - project.updatedAt <= maxDaysForSendingUpdateReminder && - project.updatedAt > maxDaysForSendingUpdateWarning && - project.verificationStatus !== RevokeSteps.Reminder - ) { - project.verificationStatus = RevokeSteps.Reminder; + newVerificationStatus = RevokeSteps.UpForRevoking; } - await project.save(); - logger.debug('remindUpdatesOrRevokeVerification() save project', { - projectId: project.id, - slug: project.slug, - verificationStatus: project.verificationStatus, - }); + if (project.verificationStatus !== newVerificationStatus) { + project.verificationStatus = newVerificationStatus?.slice(); + await project.save(); + await sendProperNotification(project, project.verificationStatus as string); + logger.debug('remindUpdatesOrRevokeVerification() save project', { + projectId: project.id, + slug: project.slug, + verificationStatus: project.verificationStatus, + }); + } - // draft the verification form to allow reapply + // draft the verification form to allow to reapply if ( project.projectVerificationForm && project.verificationStatus === RevokeSteps.Revoked @@ -184,9 +152,6 @@ const remindUpdatesOrRevokeVerification = async (project: Project) => { }); } - const user = await User.findOne({ where: { id: Number(project.admin) } }); - - await sendProperNotification(project, project.verificationStatus as string); await sleep(300); }; @@ -200,18 +165,19 @@ const sendProperNotification = ( verificationStatus: project.verificationStatus, }); switch (projectVerificationStatus) { - case RevokeSteps.Reminder: - return getNotificationAdapter().projectBadgeRevokeReminder({ project }); + // case RevokeSteps.Reminder: + // return getNotificationAdapter().projectBadgeRevokeReminder({ project }); case RevokeSteps.Warning: return getNotificationAdapter().projectBadgeRevokeWarning({ project }); case RevokeSteps.LastChance: return getNotificationAdapter().projectBadgeRevokeLastWarning({ project, }); - case RevokeSteps.Revoked: - return getNotificationAdapter().projectBadgeRevoked({ project }); case RevokeSteps.UpForRevoking: - return getNotificationAdapter().projectBadgeUpForRevoking({ project }); + // No email or notification for UpForRevoking + return; + // case RevokeSteps.Revoked: + // return getNotificationAdapter().projectBadgeRevoked({ project }); default: throw new Error( diff --git a/src/services/cronJobs/checkUserSuperTokenBalancesJob.ts b/src/services/cronJobs/checkUserSuperTokenBalancesJob.ts new file mode 100644 index 000000000..ae7a3bba2 --- /dev/null +++ b/src/services/cronJobs/checkUserSuperTokenBalancesJob.ts @@ -0,0 +1,29 @@ +import { schedule } from 'node-cron'; +import config from '../../config'; +import { logger } from '../../utils/logger'; +import { + processRecurringDonationBalancesJobs, + runCheckUserSuperTokenBalances, +} from './checkUserSuperTokenBalancesQueue'; + +const cronJobTime = + (config.get('CHECK_USERS_SUPER_TOKEN_BALANCES_CRONJOB_TIME') as string) || + '0 0 * * *'; // one day at 00:00 + +export const runCheckUserSuperTokenBalancesJob = () => { + logger.debug( + 'runCheckUserSuperTokenBalancesJob() has been called, cronJobTime', + cronJobTime, + ); + processRecurringDonationBalancesJobs(); + + schedule(cronJobTime, async () => { + logger.debug('runCheckUserSuperTokenBalancesJob() has been started'); + try { + await runCheckUserSuperTokenBalances(); + } catch (error) { + logger.error('runCheckUserSuperTokenBalancesJob() error', error); + } + logger.debug('runCheckUserSuperTokenBalancesJob() has been finished'); + }); +}; diff --git a/src/services/cronJobs/checkUserSuperTokenBalancesQueue.ts b/src/services/cronJobs/checkUserSuperTokenBalancesQueue.ts new file mode 100644 index 000000000..44406ee98 --- /dev/null +++ b/src/services/cronJobs/checkUserSuperTokenBalancesQueue.ts @@ -0,0 +1,168 @@ +import Bull from 'bull'; +import { + getNotificationAdapter, + getSuperFluidAdapter, +} from '../../adapters/adaptersFactory'; +import config from '../../config'; +import { RecurringDonation } from '../../entities/recurringDonation'; +import { redisConfig } from '../../redis'; +import { findUserById } from '../../repositories/userRepository'; +import { logger } from '../../utils/logger'; +import { + findActiveRecurringDonations, + findRecurringDonationById, +} from '../../repositories/recurringDonationRepository'; +import { getCurrentDateFormatted } from '../../utils/utils'; +import { getNetworkNameById, superTokens } from '../../provider'; +import { NOTIFICATIONS_EVENT_NAMES } from '../../analytics/analytics'; + +const runCheckUserSuperTokenBalancesQueue = new Bull( + 'user-token-balances-stream-queue', + { + redis: redisConfig, + }, +); + +const numberOfUpdateRecurringDonationsStreamConcurrentJob = + Number(config.get('NUMBER_OF_CHECK_USER_SUPER_TOKEN_BALANCES_JOB')) || 1; + +const TWO_MINUTES = 1000 * 60 * 2; +setInterval(async () => { + const superTokenBalancesQueueCount = + await runCheckUserSuperTokenBalancesQueue.count(); + logger.debug(`Check User token Balances queues count:`, { + superTokenBalancesQueueCount, + }); +}, TWO_MINUTES); + +export const runCheckUserSuperTokenBalances = async () => { + logger.debug('runCheckUserSuperTokenBalances() has been called'); + + const recurringDonations = await findActiveRecurringDonations(); + logger.debug('Active recurring donations length', recurringDonations.length); + recurringDonations.forEach(recurringDonation => { + logger.debug('Add pending recurringDonation to queue', { + recurringDonationId: recurringDonation.id, + }); + runCheckUserSuperTokenBalancesQueue.add( + { + recurringDonationId: recurringDonation.id, + }, + { + // Because we want to run this job once per day so we need to add the date to the job id + jobId: `update-recurring-donations-stream-queue-${getCurrentDateFormatted()}-${ + recurringDonation.id + }`, + removeOnComplete: true, + removeOnFail: true, + }, + ); + }); +}; + +export function processRecurringDonationBalancesJobs() { + logger.debug('processRecurringDonationBalancesJobs() has been called'); + runCheckUserSuperTokenBalancesQueue.process( + numberOfUpdateRecurringDonationsStreamConcurrentJob, + async (job, done) => { + const { recurringDonationId } = job.data; + logger.debug('job processing', { jobData: job.data }); + try { + await checkRecurringDonationBalances({ recurringDonationId }); + done(); + } catch (e) { + logger.error('processRecurringDonationBalancesJobs error', e); + done(); + } + }, + ); +} + +export const checkRecurringDonationBalances = async (params: { + recurringDonationId: number; +}) => { + const recurringDonation = await findRecurringDonationById( + params.recurringDonationId, + ); + logger.debug( + `checkRecurringDonationBalances() has been called for id ${params.recurringDonationId}`, + recurringDonation, + ); + if (!recurringDonation) return; + await validateDonorSuperTokenBalance(recurringDonation); +}; + +const weekInSec = 60 * 60 * 24 * 7; +const monthInSec = 60 * 60 * 24 * 30; +export const validateDonorSuperTokenBalance = async ( + recurringDonation: RecurringDonation, +) => { + const superFluidAdapter = getSuperFluidAdapter(); + const user = await findUserById(recurringDonation.donorId); + + if (!user) return; + + const accountBalances = await superFluidAdapter.accountBalance( + user.walletAddress!, + ); + + logger.debug( + `validateDonorSuperTokenBalance for recurringDonation id ${recurringDonation.id}`, + { accountBalances, userId: user.id }, + ); + + if (!accountBalances || accountBalances.length === 0) return; + + for (const tokenBalance of accountBalances) { + const { maybeCriticalAtTimestamp, token } = tokenBalance; + if (!user!.email) continue; + const tokenSymbol = superTokens.find(t => t.id === token.id) + ?.underlyingToken.symbol; + // We shouldn't notify the user if the token is not the same as the recurring donation + if (tokenSymbol !== recurringDonation.currency) continue; + const nowInSec = Number((Date.now() / 1000).toFixed()); + const balanceLongerThanMonth = + Math.abs(nowInSec - maybeCriticalAtTimestamp) > monthInSec; + if (balanceLongerThanMonth) { + if (user.streamBalanceWarning) { + user.streamBalanceWarning[tokenSymbol] = null; + await user.save(); + } + continue; + } + const balanceLongerThanWeek = + Math.abs(nowInSec - maybeCriticalAtTimestamp) > weekInSec; + + const depletedBalance = + maybeCriticalAtTimestamp === 0 || !maybeCriticalAtTimestamp; + const eventName = depletedBalance + ? NOTIFICATIONS_EVENT_NAMES.SUPER_TOKENS_BALANCE_DEPLETED + : balanceLongerThanWeek + ? NOTIFICATIONS_EVENT_NAMES.SUPER_TOKENS_BALANCE_MONTH + : NOTIFICATIONS_EVENT_NAMES.SUPER_TOKENS_BALANCE_WEEK; + + // If the balance warning is the same, we've already sent the notification + if ( + user.streamBalanceWarning && + user.streamBalanceWarning[tokenSymbol] === eventName + ) + continue; + if (user.streamBalanceWarning) { + user.streamBalanceWarning[tokenSymbol] = eventName; + } else { + user.streamBalanceWarning = { + [tokenSymbol]: eventName, + }; + } + await user.save(); + // Notify user their super token is running out + await getNotificationAdapter().userSuperTokensCritical({ + user, + eventName, + tokenSymbol: tokenSymbol!, + isEnded: recurringDonation.finished, + project: recurringDonation.project, + networkName: getNetworkNameById(recurringDonation.networkId), + }); + } +}; diff --git a/src/services/cronJobs/draftRecurringDonationMatchingJob.ts b/src/services/cronJobs/draftRecurringDonationMatchingJob.ts new file mode 100644 index 000000000..c99e96acc --- /dev/null +++ b/src/services/cronJobs/draftRecurringDonationMatchingJob.ts @@ -0,0 +1,38 @@ +import { schedule } from 'node-cron'; +import config from '../../config'; +import { logger } from '../../utils/logger'; +import { + DRAFT_RECURRING_DONATION_STATUS, + DraftRecurringDonation, +} from '../../entities/draftRecurringDonation'; +import { runDraftRecurringDonationMatchWorker } from '../chains/evm/draftRecurringDonationService'; +import { deleteExpiredDraftRecurringDonations } from '../../repositories/draftRecurringDonationRepository'; + +const cronJobTime = + (config.get('MATCH_DRAFT_DONATION_CRONJOB_EXPRESSION') as string) || + '0 */5 * * *'; + +const TWO_MINUTES = 1000 * 60 * 2; + +// Queue for filling snapshot balances + +// Periodically log the queue count + +export const runDraftRecurringDonationMatchWorkerJob = () => { + logger.debug('runDraftRecurringDonationMatchWorkerJob', cronJobTime); + + schedule(cronJobTime, async () => { + const hours = Number( + process.env.DRAFT_RECURRING_DONATION_MATCH_EXPIRATION_HOURS || 48, + ); + await deleteExpiredDraftRecurringDonations(hours); + await runDraftRecurringDonationMatchWorker(); + }); + + setInterval(async () => { + const count = await DraftRecurringDonation.countBy({ + status: DRAFT_RECURRING_DONATION_STATUS.PENDING, + }); + logger.debug('Pending Draft Recurring Donations count:', { count }); + }, TWO_MINUTES); +}; diff --git a/src/services/cronJobs/fillSnapshotBalances.test.ts b/src/services/cronJobs/fillSnapshotBalances.test.ts index 51697b44d..af8ee9915 100644 --- a/src/services/cronJobs/fillSnapshotBalances.test.ts +++ b/src/services/cronJobs/fillSnapshotBalances.test.ts @@ -1,9 +1,10 @@ +import { assert } from 'chai'; +import sinon from 'sinon'; import { addFillPowerSnapshotBalanceJobsToQueue, processFillPowerSnapshotJobs, } from './fillSnapshotBalances'; import { getPowerBoostingSnapshotWithoutBalance } from '../../repositories/powerSnapshotRepository'; -import { assert } from 'chai'; import { createProjectData, generateRandomEtheriumAddress, @@ -15,8 +16,6 @@ import { PowerSnapshot } from '../../entities/powerSnapshot'; import { PowerBoostingSnapshot } from '../../entities/powerBoostingSnapshot'; import { AppDataSource } from '../../orm'; import { PowerBalanceSnapshot } from '../../entities/powerBalanceSnapshot'; -import { logger } from '../../utils/logger'; -import sinon from 'sinon'; import { getPowerBalanceAggregatorAdapter } from '../../adapters/adaptersFactory'; import { convertTimeStampToSeconds } from '../../utils/utils'; diff --git a/src/services/cronJobs/fillSnapshotBalances.ts b/src/services/cronJobs/fillSnapshotBalances.ts index 969e9c068..97e3ef39c 100644 --- a/src/services/cronJobs/fillSnapshotBalances.ts +++ b/src/services/cronJobs/fillSnapshotBalances.ts @@ -1,15 +1,15 @@ import Bull from 'bull'; +import { schedule } from 'node-cron'; +import _ from 'lodash'; import { redisConfig } from '../../redis'; import { logger } from '../../utils/logger'; import config from '../../config'; -import { schedule } from 'node-cron'; import { getPowerBalanceAggregatorAdapter } from '../../adapters/adaptersFactory'; import { getPowerBoostingSnapshotWithoutBalance, GetPowerBoostingSnapshotWithoutBalanceOutput, } from '../../repositories/powerSnapshotRepository'; import { addOrUpdatePowerSnapshotBalances } from '../../repositories/powerBalanceSnapshotRepository'; -import _ from 'lodash'; // Constants const FILL_SNAPSHOT_BALANCE_QUEUE_NAME = 'fill-snapshot-balance-aggregator'; diff --git a/src/services/cronJobs/importLostDonationsJob.test.ts b/src/services/cronJobs/importLostDonationsJob.test.ts index 56d3046a0..921fb9299 100644 --- a/src/services/cronJobs/importLostDonationsJob.test.ts +++ b/src/services/cronJobs/importLostDonationsJob.test.ts @@ -1,12 +1,4 @@ import { assert } from 'chai'; -import { - createProjectData, - generateRandomEtheriumAddress, - saveProjectDirectlyToDb, - saveUserDirectlyToDb, -} from '../../../test/testUtils'; -import { importLostDonations } from './importLostDonationsJob'; -import { Donation } from '../../entities/donation'; describe('importLostDonations() test cases', importLostDonationsTestCases); diff --git a/src/services/cronJobs/importLostDonationsJob.ts b/src/services/cronJobs/importLostDonationsJob.ts index 122833ead..b6add9257 100644 --- a/src/services/cronJobs/importLostDonationsJob.ts +++ b/src/services/cronJobs/importLostDonationsJob.ts @@ -1,8 +1,9 @@ -import config from '../../config'; import abiDecoder from 'abi-decoder'; +import { schedule } from 'node-cron'; +import moment from 'moment'; +import config from '../../config'; import { Donation } from '../../entities/donation'; import { logger } from '../../utils/logger'; -import { schedule } from 'node-cron'; import { NetworkTransactionInfo } from '../chains'; import { getProvider, NETWORKS_IDS_TO_NAME } from '../../provider'; import { erc20ABI } from '../../assets/erc20ABI'; @@ -10,7 +11,6 @@ import { User } from '../../entities/user'; import { Token } from '../../entities/token'; import { Project } from '../../entities/project'; import { calculateGivbackFactor } from '../givbackService'; -import moment from 'moment'; import { getUserDonationStats, updateUserTotalDonated, @@ -25,8 +25,9 @@ import { CoingeckoPriceAdapter } from '../../adapters/price/CoingeckoPriceAdapte import { QfRound } from '../../entities/qfRound'; import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; import { getNotificationAdapter } from '../../adapters/adaptersFactory'; +import { getOrttoPersonAttributes } from '../../adapters/notifications/NotificationCenterAdapter'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const ethers = require('ethers'); abiDecoder.addABI(erc20ABI); @@ -251,7 +252,7 @@ export const importLostDonations = async () => { const donationStats = await getUserDonationStats(dbUser.id); - await getNotificationAdapter().updateOrttoUser({ + const orttoPerson = getOrttoPersonAttributes({ userId: dbUser.id.toString(), firstName: dbUser?.firstName, lastName: dbUser?.lastName, @@ -260,9 +261,10 @@ export const importLostDonations = async () => { donationsCount: donationStats?.donationsCount, lastDonationDate: donationStats?.lastDonationDate, GIVbacksRound: dbDonation.powerRound, - QFRound: dbDonation.qfRound?.name, + QFDonor: dbDonation.qfRound?.name, donationChain: NETWORKS_IDS_TO_NAME[dbDonation.transactionNetworkId], }); + await getNotificationAdapter().updateOrttoPeople([orttoPerson]); } catch (e) { logger.error('importLostDonations() error'); continue; diff --git a/src/services/cronJobs/instantBoostingUpdateJob.ts b/src/services/cronJobs/instantBoostingUpdateJob.ts index d1a74d7fd..660954105 100644 --- a/src/services/cronJobs/instantBoostingUpdateJob.ts +++ b/src/services/cronJobs/instantBoostingUpdateJob.ts @@ -1,6 +1,6 @@ +import { schedule } from 'node-cron'; import config from '../../config'; import { logger } from '../../utils/logger'; -import { schedule } from 'node-cron'; import { updateInstantBoosting } from '../instantBoostingServices'; const cronJobTime = diff --git a/src/services/cronJobs/notifyDonationsWithSegment.test.ts b/src/services/cronJobs/notifyDonationsWithSegment.test.ts index 231d98d5b..90afbea45 100644 --- a/src/services/cronJobs/notifyDonationsWithSegment.test.ts +++ b/src/services/cronJobs/notifyDonationsWithSegment.test.ts @@ -1,11 +1,11 @@ import { assert } from 'chai'; import 'mocha'; +import sinon from 'sinon'; import { createDonationData, saveDonationDirectlyToDb, SEED_DATA, } from '../../../test/testUtils'; -import sinon from 'sinon'; import { Donation, DONATION_STATUS } from '../../entities/donation'; import { notifyMissingDonationsWithSegment } from './notifyDonationsWithSegment'; import * as utils from '../../utils/utils'; diff --git a/src/services/cronJobs/notifyDonationsWithSegment.ts b/src/services/cronJobs/notifyDonationsWithSegment.ts index 5d4d3726d..bd05aba5c 100644 --- a/src/services/cronJobs/notifyDonationsWithSegment.ts +++ b/src/services/cronJobs/notifyDonationsWithSegment.ts @@ -1,8 +1,6 @@ +import { schedule } from 'node-cron'; import { Donation, DONATION_STATUS } from '../../entities/donation'; import { logger } from '../../utils/logger'; -import { schedule } from 'node-cron'; -import { Project } from '../../entities/project'; -import { User } from '../../entities/user'; import { sleep } from '../../utils/utils'; import config from '../../config'; import { sendNotificationForDonation } from '../donationService'; @@ -40,49 +38,3 @@ export const notifyMissingDonationsWithSegment = async () => { await sleep(1000); } }; - -interface SegmentDonationInterFace { - slug?: string | null; - title: string; - amount: number; - transactionId: string; - toWalletAddress: string; - fromWalletAddress: string; - donationValueUsd: number; - donationValueEth: number; - verified: boolean; - projectOwnerId: number; - transactionNetworkId: number; - currency: string; - projectWalletAddress?: string | null; - createdAt: Date; - email?: string | null; - firstName?: string | null; - anonymous: boolean; -} - -const segmentDonationAttributes = ( - project: Project, - donation: Donation, - user: User, -): SegmentDonationInterFace => { - return { - slug: project.slug, - title: project.title, - amount: donation.amount, - transactionId: donation.transactionId.toLowerCase(), - toWalletAddress: donation.toWalletAddress.toLowerCase(), - fromWalletAddress: donation.fromWalletAddress.toLowerCase(), - donationValueUsd: donation.valueUsd, - donationValueEth: donation.valueEth, - verified: project.verified, - projectOwnerId: Number(project.admin), - transactionNetworkId: donation.transactionNetworkId, - currency: donation.currency, - projectWalletAddress: project.walletAddress, - createdAt: donation.createdAt, - email: user != null ? user.email : '', - firstName: user != null ? user.firstName : '', - anonymous: donation.anonymous, - }; -}; diff --git a/src/services/cronJobs/syncDonationsWithNetwork.ts b/src/services/cronJobs/syncDonationsWithNetwork.ts index 7ff03b7aa..02c1af491 100644 --- a/src/services/cronJobs/syncDonationsWithNetwork.ts +++ b/src/services/cronJobs/syncDonationsWithNetwork.ts @@ -1,9 +1,4 @@ -import { Donation, DONATION_STATUS } from '../../entities/donation'; -import { errorMessages } from '../../utils/errorMessages'; import { schedule } from 'node-cron'; - -// @ts-ignore -// everything I used had problem so I had to add ts-ignore https://github.com/OptimalBits/bull/issues/1772 import Bull from 'bull'; import config from '../../config'; import { redisConfig } from '../../redis'; diff --git a/src/services/cronJobs/syncProjectsRequiredForListing.test.ts b/src/services/cronJobs/syncProjectsRequiredForListing.test.ts index 74b80df11..b36c864b0 100644 --- a/src/services/cronJobs/syncProjectsRequiredForListing.test.ts +++ b/src/services/cronJobs/syncProjectsRequiredForListing.test.ts @@ -6,7 +6,7 @@ import { import { Project, ProjStatus, ReviewStatus } from '../../entities/project'; import { updateProjectListing } from './syncProjectsRequiredForListing'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const moment = require('moment'); describe('updateProjectListing() test cases', updateProjectListingTestCases); diff --git a/src/services/cronJobs/syncProjectsRequiredForListing.ts b/src/services/cronJobs/syncProjectsRequiredForListing.ts index d095d40e2..b72127144 100644 --- a/src/services/cronJobs/syncProjectsRequiredForListing.ts +++ b/src/services/cronJobs/syncProjectsRequiredForListing.ts @@ -1,9 +1,7 @@ -import { Project } from '../../entities/project'; import { schedule } from 'node-cron'; -import { getRepository } from 'typeorm'; +import { Project } from '../../entities/project'; import config from '../../config'; -import { NOTIFICATIONS_EVENT_NAMES } from '../../analytics/analytics'; import { logger } from '../../utils/logger'; import { getNotificationAdapter } from '../../adapters/adaptersFactory'; import { makeProjectListed } from '../../repositories/projectRepository'; diff --git a/src/services/cronJobs/syncRecurringDonationsWithNetwork.ts b/src/services/cronJobs/syncRecurringDonationsWithNetwork.ts new file mode 100644 index 000000000..44d0d6cac --- /dev/null +++ b/src/services/cronJobs/syncRecurringDonationsWithNetwork.ts @@ -0,0 +1,90 @@ +import { schedule } from 'node-cron'; +import Bull from 'bull'; +import config from '../../config'; +import { redisConfig } from '../../redis'; +import { logger } from '../../utils/logger'; +import { getPendingRecurringDonationsIds } from '../../repositories/recurringDonationRepository'; +import { updateRecurringDonationStatusWithNetwork } from '../recurringDonationService'; + +const verifyRecurringDonationsQueue = new Bull( + 'verify-recurring-donations-queue', + { + redis: redisConfig, + }, +); +const TWO_MINUTES = 1000 * 60 * 2; +setInterval(async () => { + const verifyDonationsQueueCount = await verifyRecurringDonationsQueue.count(); + logger.debug(`Verify recurring donations job queues count:`, { + verifyDonationsQueueCount, + }); +}, TWO_MINUTES); + +const numberOfVerifyDonationConcurrentJob = + Number(config.get('NUMBER_OF_VERIFY_RECURRING_CONCURRENT_JOB')) || 1; + +const cronJobTime = + (config.get('VERIFY_RECURRING_CRONJOB_EXPRESSION') as string) || + '0 0 * * * *'; + +export const runCheckPendingRecurringDonationsCronJob = () => { + logger.debug( + 'runCheckPendingRecurringDonationsCronJob() has been called, cronJobTime', + cronJobTime, + ); + processVerifyRecurringDonationsJobs(); + + // https://github.com/node-cron/node-cron#cron-syntax + schedule(cronJobTime, async () => { + await addJobToCheckPendingRecurringDonationsWithNetwork(); + }); + addJobToCheckPendingRecurringDonationsWithNetwork(); +}; + +const addJobToCheckPendingRecurringDonationsWithNetwork = async () => { + logger.debug( + 'addJobToCheckPendingRecurringDonationsWithNetwork() has been called', + ); + + const recurringDonations = await getPendingRecurringDonationsIds(); + logger.debug( + 'Pending recurringDonations to be check', + recurringDonations.length, + ); + recurringDonations.forEach(donation => { + logger.debug('Add pending recurring donation to queue', { + donationId: donation.id, + }); + verifyRecurringDonationsQueue.add( + { + donationId: donation.id, + }, + { + jobId: `verify-recurring-donation-id-${donation.id}`, + removeOnComplete: true, + removeOnFail: true, + }, + ); + }); +}; + +function processVerifyRecurringDonationsJobs() { + logger.debug('processVerifyRecurringDonationsJobs() has been called'); + verifyRecurringDonationsQueue.process( + numberOfVerifyDonationConcurrentJob, + async (job, done) => { + const { donationId } = job.data; + logger.debug('job processing', { jobData: job.data }); + try { + await updateRecurringDonationStatusWithNetwork({ donationId }); + done(); + } catch (e) { + logger.error( + 'processVerifyRecurringDonationsJobs >> updateRecurringDonationStatusWithNetwork error', + e, + ); + done(); + } + }, + ); +} diff --git a/src/services/cronJobs/updatePowerRoundJob.ts b/src/services/cronJobs/updatePowerRoundJob.ts index 7ed77cec4..d36f94c5d 100644 --- a/src/services/cronJobs/updatePowerRoundJob.ts +++ b/src/services/cronJobs/updatePowerRoundJob.ts @@ -1,6 +1,6 @@ +import { schedule } from 'node-cron'; import config from '../../config'; import { logger } from '../../utils/logger'; -import { schedule } from 'node-cron'; import { getPowerRound, setPowerRound, @@ -14,7 +14,6 @@ import { import { refreshUserProjectPowerView } from '../../repositories/userProjectPowerViewRepository'; import { copyProjectRanksToPreviousRoundRankTable, - deleteAllPreviousRoundRanks, projectsThatTheirRanksHaveChanged, } from '../../repositories/previousRoundRankRepository'; import { getNotificationAdapter } from '../../adapters/adaptersFactory'; diff --git a/src/services/cronJobs/updateProjectCampaignsCacheJob.ts b/src/services/cronJobs/updateProjectCampaignsCacheJob.ts index 6bdaf6cab..a69e4bfe8 100644 --- a/src/services/cronJobs/updateProjectCampaignsCacheJob.ts +++ b/src/services/cronJobs/updateProjectCampaignsCacheJob.ts @@ -1,9 +1,6 @@ +import { schedule } from 'node-cron'; import config from '../../config'; import { logger } from '../../utils/logger'; -import { schedule } from 'node-cron'; -import { isTestEnv } from '../../utils/utils'; -import { ModuleThread, Pool, spawn, Worker } from 'threads'; -import { CacheProjectCampaignsWorker } from '../../workers/cacheProjectCampaignsWorker'; import { cacheProjectCampaigns } from '../campaignService'; // every 10 minutes @@ -11,11 +8,11 @@ const cronJobTime = (config.get('CACHE_PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION') as string) || '0 */5 * * * *'; -const projectsFiltersThreadPool: Pool< - ModuleThread -> = Pool( - () => spawn(new Worker('../../workers/cacheProjectCampaignsWorker')), // create the worker, -); +// const projectsFiltersThreadPool: Pool< +// ModuleThread +// > = Pool( +// () => spawn(new Worker('../../workers/cacheProjectCampaignsWorker')), // create the worker, +// ); export const runUpdateProjectCampaignsCacheJob = async () => { logger.debug( 'runUpdateProjectCampaignsCacheJob() has been called, cronJobTime', diff --git a/src/services/cronJobs/updateStreamOldRecurringDonationsJob.ts b/src/services/cronJobs/updateStreamOldRecurringDonationsJob.ts index cd979cbcc..d0f7bf878 100644 --- a/src/services/cronJobs/updateStreamOldRecurringDonationsJob.ts +++ b/src/services/cronJobs/updateStreamOldRecurringDonationsJob.ts @@ -1,6 +1,6 @@ +import { schedule } from 'node-cron'; import config from '../../config'; import { logger } from '../../utils/logger'; -import { schedule } from 'node-cron'; import { processRecurringDonationStreamJobs, updateRecurringDonationsStream, diff --git a/src/services/donationService.test.ts b/src/services/donationService.test.ts index 60892acec..88b3abbf7 100644 --- a/src/services/donationService.test.ts +++ b/src/services/donationService.test.ts @@ -1,4 +1,6 @@ import { assert, expect } from 'chai'; +import { CHAIN_ID } from '@giveth/monoswap/dist/src/sdk/sdkFactory'; +import moment from 'moment'; import { isTokenAcceptableForProject, updateOldStableCoinDonationsPrice, @@ -14,7 +16,6 @@ import { createProjectData, DONATION_SEED_DATA, generateRandomEtheriumAddress, - generateRandomEvmTxHash, saveDonationDirectlyToDb, saveProjectDirectlyToDb, saveUserDirectlyToDb, @@ -27,13 +28,8 @@ import { Donation, DONATION_STATUS } from '../entities/donation'; import { errorMessages } from '../utils/errorMessages'; import { findDonationById } from '../repositories/donationRepository'; import { findProjectById } from '../repositories/projectRepository'; -import { CHAIN_ID } from '@giveth/monoswap/dist/src/sdk/sdkFactory'; -import { - findUserById, - findUserByWalletAddress, -} from '../repositories/userRepository'; +import { findUserByWalletAddress } from '../repositories/userRepository'; import { QfRound } from '../entities/qfRound'; -import moment from 'moment'; import { fillQfRoundHistory, getQfRoundHistoriesThatDontHaveRelatedDonations, @@ -41,7 +37,6 @@ import { } from '../repositories/qfRoundHistoryRepository'; import { User } from '../entities/user'; import { QfRoundHistory } from '../entities/qfRoundHistory'; -import { ChainType } from '../types/network'; describe('isProjectAcceptToken test cases', isProjectAcceptTokenTestCases); describe( @@ -396,6 +391,50 @@ function syncDonationStatusWithBlockchainNetworkTestCases() { assert.isTrue(updateDonation.segmentNotified); }); + it('should verify a Optimism Sepolia donation', async () => { + // https://sepolia-optimism.etherscan.io/tx/0x1b4e9489154a499cd7d0bd7a097e80758e671a32f98559be3b732553afb00809 + const amount = 0.01; + + const transactionInfo = { + txHash: + '0x1b4e9489154a499cd7d0bd7a097e80758e671a32f98559be3b732553afb00809', + currency: 'ETH', + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + fromAddress: '0x625bcc1142e97796173104a6e817ee46c593b3c5', + toAddress: '0x73f9b3f48ebc96ac55cb76c11053b068669a8a67', + amount, + timestamp: 1708954960, + }; + const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const donation = await saveDonationDirectlyToDb( + { + amount: transactionInfo.amount, + transactionNetworkId: transactionInfo.networkId, + transactionId: transactionInfo.txHash, + currency: transactionInfo.currency, + fromWalletAddress: transactionInfo.fromAddress, + toWalletAddress: transactionInfo.toAddress, + valueUsd: 20.73, + anonymous: false, + createdAt: new Date(transactionInfo.timestamp), + status: DONATION_STATUS.PENDING, + }, + user.id, + project.id, + ); + const updateDonation = await syncDonationStatusWithBlockchainNetwork({ + donationId: donation.id, + }); + assert.isOk(updateDonation); + assert.equal(updateDonation.id, donation.id); + assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); + assert.isTrue(updateDonation.segmentNotified); + }); + // it('should verify a mainnet donation', async () => { // // https://etherscan.io/tx/0x37765af1a7924fb6ee22c83668e55719c9ecb1b79928bd4b208c42dfff44da3a // const transactionInfo = { @@ -854,10 +893,8 @@ function fillOldStableCoinDonationsPriceTestCases() { await updateDonationPricesAndValues( donation, project, - null, - token, + { symbol: token }, CHAIN_ID.POLYGON, - amount, ); donation = (await findDonationById(donation.id))!; @@ -886,10 +923,8 @@ function fillOldStableCoinDonationsPriceTestCases() { await updateDonationPricesAndValues( donation, project, - null, - token, + { symbol: token }, CHAIN_ID.CELO, - amount, ); donation = (await findDonationById(donation.id))!; expect(donation.valueUsd).to.gt(0); @@ -926,10 +961,8 @@ function fillOldStableCoinDonationsPriceTestCases() { await updateDonationPricesAndValues( donation, project, - token, - currency, + token!, CHAIN_ID.MAINNET, - amount, ); donation = (await findDonationById(donation.id))!; expect(donation.valueUsd).to.gt(0); @@ -967,10 +1000,8 @@ function fillOldStableCoinDonationsPriceTestCases() { await updateDonationPricesAndValues( donation, project, - token, - currency, + token!, CHAIN_ID.MAINNET, - amount, ); donation = (await findDonationById(donation.id))!; expect(donation.valueUsd).to.gt(0); @@ -999,10 +1030,8 @@ function fillOldStableCoinDonationsPriceTestCases() { await updateDonationPricesAndValues( donation, project, - null, - token, + { symbol: token }, CHAIN_ID.ALFAJORES, - amount, ); donation = (await findDonationById(donation.id))!; @@ -1046,8 +1075,8 @@ function insertDonationsFromQfRoundHistoryTestCases() { }); it('should return correct value for single project', async () => { - // First call it to make sure there isnt any thing in DB to make conflicts in our test cases - await insertDonationsFromQfRoundHistory(); + // First call it to make sure there isn't any thing in DB to make conflicts in our test cases + await QfRoundHistory.clear(); const usersDonations: number[][] = [ [1, 3], // 4 @@ -1099,7 +1128,9 @@ function insertDonationsFromQfRoundHistoryTestCases() { qfRoundId: qfRound.id, }); assert.isNotNull(qfRoundHistory); - qfRoundHistory!.distributedFundTxHash = generateRandomEvmTxHash(); + // https://blockscout.com/xdai/mainnet/tx/0x42c0f15029557ec35e61515a89366297fc239a334e3ba22fab15a3f1d04ad53f + qfRoundHistory!.distributedFundTxHash = + '0x42c0f15029557ec35e61515a89366297fc239a334e3ba22fab15a3f1d04ad53f'; qfRoundHistory!.distributedFundNetwork = '100'; qfRoundHistory!.matchingFundAmount = 1000; qfRoundHistory!.matchingFundCurrency = 'DAI'; @@ -1159,6 +1190,7 @@ function insertDonationsFromQfRoundHistoryTestCases() { donations[0].transactionId, qfRoundHistory?.distributedFundTxHash, ); + assert.equal(donations[0].createdAt.getTime(), 1702091620); const updatedProject = await findProjectById(firstProject.id); assert.equal( diff --git a/src/services/donationService.ts b/src/services/donationService.ts index 6fd930fe8..b657b7d12 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -1,3 +1,4 @@ +import { getTokenPrices } from '@giveth/monoswap'; import { Project } from '../entities/project'; import { Token } from '../entities/token'; import { Donation, DONATION_STATUS } from '../entities/donation'; @@ -14,7 +15,7 @@ import { } from '../utils/errorMessages'; import { findProjectById } from '../repositories/projectRepository'; import { convertExponentialNumber } from '../utils/utils'; -import { fetchGivHistoricPrice, fetchGivPrice } from './givPriceService'; +import { fetchGivHistoricPrice } from './givPriceService'; import { findDonationById, findStableCoinDonationsWithoutPrice, @@ -24,7 +25,6 @@ import { getNotificationAdapter, } from '../adapters/adaptersFactory'; import { calculateGivbackFactor } from './givbackService'; -import { getTokenPrices } from '@giveth/monoswap'; import SentryLogger from '../sentryLogger'; import { getUserDonationStats, @@ -35,30 +35,23 @@ import { refreshProjectDonationSummaryView, refreshProjectEstimatedMatchingView, } from './projectViewsService'; -import { MonoswapPriceAdapter } from '../adapters/price/MonoswapPriceAdapter'; -import { CryptoComparePriceAdapter } from '../adapters/price/CryptoComparePriceAdapter'; -import { - COINGECKO_TOKEN_IDS, - CoingeckoPriceAdapter, -} from '../adapters/price/CoingeckoPriceAdapter'; import { AppDataSource } from '../orm'; import { getQfRoundHistoriesThatDontHaveRelatedDonations } from '../repositories/qfRoundHistoryRepository'; import { getPowerRound } from '../repositories/powerRoundRepository'; import { fetchSafeTransactionHash } from './safeServices'; -import { ChainType } from '../types/network'; -import { NETWORK_IDS, NETWORKS_IDS_TO_NAME } from '../provider'; +import { NETWORKS_IDS_TO_NAME } from '../provider'; import { getTransactionInfoFromNetwork } from './chains'; -import { fetchMpEthPrice } from './mpEthPriceService'; +import { getEvmTransactionTimestamp } from './chains/evm/transactionService'; +import { getOrttoPersonAttributes } from '../adapters/notifications/NotificationCenterAdapter'; +import { CustomToken, getTokenPrice } from './priceService'; export const TRANSAK_COMPLETED_STATUS = 'COMPLETED'; export const updateDonationPricesAndValues = async ( donation: Donation, project: Project, - token: Token | null, - currency: string, + token: CustomToken, priceChainId: number, - amount: string | number, ) => { logger.debug('updateDonationPricesAndValues() has been called', { donationId: donation.id, @@ -67,41 +60,9 @@ export const updateDonationPricesAndValues = async ( priceChainId, }); try { - if (token?.isStableCoin) { - donation.priceUsd = 1; - donation.valueUsd = Number(amount); - // } else if (currency === 'mpETH') { - // const mpEthPriceInUsd = await fetchMpEthPrice(); - // donation.priceUsd = toFixNumber(mpEthPriceInUsd, 4); - // donation.valueUsd = toFixNumber(donation.amount * mpEthPriceInUsd, 4); - } else if (currency === 'GIV') { - const { givPriceInUsd } = await fetchGivPrice(); - donation.priceUsd = toFixNumber(givPriceInUsd, 4); - donation.valueUsd = toFixNumber(donation.amount * givPriceInUsd, 4); - } else if (token?.cryptoCompareId) { - const priceUsd = await new CryptoComparePriceAdapter().getTokenPrice({ - symbol: token.cryptoCompareId, - networkId: priceChainId, - }); - donation.priceUsd = toFixNumber(priceUsd, 4); - donation.valueUsd = toFixNumber(donation.amount * priceUsd, 4); - } else if (token?.coingeckoId) { - const priceUsd = await new CoingeckoPriceAdapter().getTokenPrice({ - symbol: token.coingeckoId, - networkId: priceChainId, - }); - donation.priceUsd = toFixNumber(priceUsd, 4); - donation.valueUsd = toFixNumber(donation.amount * priceUsd, 4); - } else { - const priceUsd = await new MonoswapPriceAdapter().getTokenPrice({ - symbol: currency, - networkId: priceChainId, - }); - if (priceUsd) { - donation.priceUsd = Number(priceUsd); - donation.valueUsd = toFixNumber(Number(amount) * donation.priceUsd, 4); - } - } + const tokenPrice = await getTokenPrice(priceChainId, token); + donation.priceUsd = toFixNumber(tokenPrice, 4); + donation.valueUsd = toFixNumber(donation.amount * tokenPrice, 4); } catch (e) { logger.error('Error in getting price from donation', { error: e, @@ -237,6 +198,9 @@ 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); } @@ -411,7 +375,7 @@ export const syncDonationStatusWithBlockchainNetwork = async (params: { const donationStats = await getUserDonationStats(donation.userId); const donor = await findUserById(donation.userId); - await getNotificationAdapter().updateOrttoUser({ + const orttoPerson = getOrttoPersonAttributes({ userId: donation.userId.toString(), firstName: donor?.firstName, lastName: donor?.lastName, @@ -420,9 +384,10 @@ export const syncDonationStatusWithBlockchainNetwork = async (params: { donationsCount: donationStats?.donationsCount, lastDonationDate: donationStats?.lastDonationDate, GIVbacksRound: donation.powerRound + 1, // powerRound is 1 behind givbacks round - QFRound: donation.qfRound?.name, + QFDonor: donation.qfRound?.name, donationChain: NETWORKS_IDS_TO_NAME[donation.transactionNetworkId], }); + await getNotificationAdapter().updateOrttoPeople([orttoPerson]); // send chainvine the referral as last step to not interrupt previous if (donation.referrerWallet && donation.isReferrerGivbackEligible) { @@ -523,6 +488,18 @@ export const insertDonationsFromQfRoundHistory = async (): Promise => { `insertDonationsFromQfRoundHistory Filling ${qfRoundHistories.length} qfRoundHistory info ...`, ); + for (const qfRoundHistory of qfRoundHistories) { + if (qfRoundHistory.distributedFundTxDate) { + continue; + } + // get transaction time from blockchain + const txTimestamp = await getEvmTransactionTimestamp({ + txHash: qfRoundHistory.distributedFundTxHash, + networkId: Number(qfRoundHistory.distributedFundNetwork), + }); + qfRoundHistory.distributedFundTxDate = new Date(txTimestamp); + await qfRoundHistory.save(); + } const matchingFundFromAddress = (process.env.MATCHING_FUND_DONATIONS_FROM_ADDRESS as string) || donationDotEthAddress; @@ -566,7 +543,7 @@ export const insertDonationsFromQfRoundHistory = async (): Promise => { q."qfRoundId", true, ${user.id}, - NOW() + q."distributedFundTxDate" FROM "qf_round_history" q INNER JOIN "project" p ON q."projectId" = p."id" diff --git a/src/services/givPriceService.ts b/src/services/givPriceService.ts index cf8bb42bd..e11cc8274 100644 --- a/src/services/givPriceService.ts +++ b/src/services/givPriceService.ts @@ -1,4 +1,4 @@ -import Axios, { AxiosResponse } from 'axios'; +import Axios from 'axios'; import axiosRetry from 'axios-retry'; import { logger } from '../utils/logger'; import { NETWORK_IDS } from '../provider'; diff --git a/src/services/googleSheets.ts b/src/services/googleSheets.ts index 54f806bc0..64ef44457 100644 --- a/src/services/googleSheets.ts +++ b/src/services/googleSheets.ts @@ -3,7 +3,7 @@ import config from '../config'; import { logger } from '../utils/logger'; import { ReviewStatus } from '../entities/project'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const moment = require('moment'); interface ProjectExport { @@ -47,6 +47,8 @@ export interface QfRoundDonationRow { donationIdsAfterAnalysis: string; totalValuesOfUserDonationsAfterAnalysis: string; uniqueUserIdsAfterAnalysis: string; + + projectOwnerEmail?: string; } interface DonationExport { @@ -174,6 +176,7 @@ export const addQfRoundDonationsSheetToSpreadsheet = async (params: { 'donationIdsAfterAnalysis', 'totalValuesOfUserDonationsAfterAnalysis', 'uniqueUserIdsAfterAnalysis', + 'projectOwnerEmail', ]; const { rows, qfRoundId } = params; diff --git a/src/services/instantBoostingServices.test.ts b/src/services/instantBoostingServices.test.ts index f0ce8f405..fc2d093c3 100644 --- a/src/services/instantBoostingServices.test.ts +++ b/src/services/instantBoostingServices.test.ts @@ -1,8 +1,8 @@ +import { expect } from 'chai'; import { PowerBoosting } from '../entities/powerBoosting'; import { InstantPowerBalance } from '../entities/instantPowerBalance'; import { updateInstantPowerBalances } from './instantBoostingServices'; import { InstantPowerFetchState } from '../entities/instantPowerFetchState'; -import { expect } from 'chai'; import { getMaxFetchedUpdatedAtTimestamp } from '../repositories/instantBoostingRepository'; import { insertSinglePowerBoosting } from '../repositories/powerBoostingRepository'; import { @@ -10,8 +10,6 @@ import { saveProjectDirectlyToDb, saveUserDirectlyToDb, } from '../../test/testUtils'; -import { mockPowerBalanceAggregator } from '../adapters/adaptersFactory'; -import { BalanceResponse } from '../types/GivPowerBalanceAggregator'; describe( 'updateInstancePowerBalances test cases', @@ -23,11 +21,11 @@ const SampleStagingGivPowerUsers = [ '0x05a1ff0a32bc24265bcb39499d0c5d9a6cb2011c', ]; -const getLastUpdatedUsers = async (): Promise => { - return mockPowerBalanceAggregator.getBalancesUpdatedAfterDate({ - date: 0, - }); -}; +// const getLastUpdatedUsers = async (): Promise => { +// return mockPowerBalanceAggregator.getBalancesUpdatedAfterDate({ +// date: 0, +// }); +// }; function updateInstancePowerBalancesTestCase() { beforeEach(async () => { diff --git a/src/services/instantBoostingServices.ts b/src/services/instantBoostingServices.ts index 34ef04dda..87463d001 100644 --- a/src/services/instantBoostingServices.ts +++ b/src/services/instantBoostingServices.ts @@ -59,6 +59,7 @@ const fetchUpdatedInstantPowerBalances = async ( // logger.debug(`Latest synced block: ${latestSyncedBlock}`); let counter = 0; + // eslint-disable-next-line no-constant-condition while (true) { const balances = await givPowerBalanceAggregator.getBalancesUpdatedAfterDate({ @@ -134,6 +135,7 @@ const fillMissingInstantPowerBalances = async ( // let allUsersWithoutBalance: { id: number; walletAddress: string }[] = []; let counter = 0; + // eslint-disable-next-line no-constant-condition while (true) { const result = await getUsersBoostedWithoutInstanceBalance(100, counter); if (result.length === 0) break; diff --git a/src/services/mpEthPriceService.ts b/src/services/mpEthPriceService.ts index 3025946af..5c3524aa2 100644 --- a/src/services/mpEthPriceService.ts +++ b/src/services/mpEthPriceService.ts @@ -1,6 +1,5 @@ +import Axios from 'axios'; import { logger } from '../utils/logger'; -import Axios, { AxiosResponse } from 'axios'; -import axiosRetry from 'axios-retry'; const mpEthSubgraphUrl = process.env.MPETH_GRAPHQL_PRICES_URL as string; diff --git a/src/services/onramper/donationService.ts b/src/services/onramper/donationService.ts index b3016da0b..443011b72 100644 --- a/src/services/onramper/donationService.ts +++ b/src/services/onramper/donationService.ts @@ -6,11 +6,9 @@ import { findProjectRecipientAddressByNetworkId } from '../../repositories/proje import { findProjectById } from '../../repositories/projectRepository'; import { findUserById } from '../../repositories/userRepository'; import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; -import { errorMessages } from '../../utils/errorMessages'; import { logger } from '../../utils/logger'; import { isTokenAcceptableForProject, - updateDonationPricesAndValues, updateTotalDonationsOfProject, } from '../donationService'; import { OnRamperFiatTransaction, OnRamperMetadata } from './fiatTransaction'; @@ -102,7 +100,7 @@ export const createFiatDonationFromOnramper = async ( const ethMainnetAddress = '0x0000000000000000000000000000000000000000'; // FromWalletAddress is not the donor wallet, but the Onramper Address - donation = await Donation.create({ + donation = Donation.create({ amount: Number(fiatTransaction.payload.outAmount), transactionId: fiatTransaction.payload.txHash!.toLowerCase(), isFiat: true, @@ -133,14 +131,14 @@ export const createFiatDonationFromOnramper = async ( await donation.save(); - await updateDonationPricesAndValues( - donation, - project, - null, - fiatTransaction.payload.outCurrency, - priceChainId, - fiatTransaction.payload.outAmount, - ); + // await updateDonationPricesAndValues( + // donation, + // project, + // null, + // fiatTransaction.payload.outCurrency, + // priceChainId, + // fiatTransaction.payload.outAmount, + // ); // After updating, recalculate user total donated and owner total received if (donorUser) { diff --git a/src/services/onramper/webhookHandler.test.ts b/src/services/onramper/webhookHandler.test.ts index a1cdaa4c8..fa59f144d 100644 --- a/src/services/onramper/webhookHandler.test.ts +++ b/src/services/onramper/webhookHandler.test.ts @@ -32,15 +32,11 @@ const transactionId = function onramperWebhookHandlerTestCases() { it('should return error 403 if the hmac-sha256 signature is invalid', async () => { try { - const result = await axios.post( - `${serverBaseAddress}/fiat_webhook`, - payload, - { - headers: { - 'x-onramper-webhook-signature': 'xxxxxx', - }, + await axios.post(`${serverBaseAddress}/fiat_webhook`, payload, { + headers: { + 'x-onramper-webhook-signature': 'xxxxxx', }, - ); + }); } catch (e) { const status = e.response.status; assert.equal(status, 403); @@ -48,10 +44,7 @@ function onramperWebhookHandlerTestCases() { }); it('should return error if the hmac-sha256 signature header is missing', async () => { try { - const result = await axios.post( - `${serverBaseAddress}/fiat_webhook`, - payload, - ); + await axios.post(`${serverBaseAddress}/fiat_webhook`, payload); } catch (e) { const status = e.response.status; assert.equal(status, 403); diff --git a/src/services/onramper/webhookHandler.ts b/src/services/onramper/webhookHandler.ts index 8ad07512f..b7b85a197 100644 --- a/src/services/onramper/webhookHandler.ts +++ b/src/services/onramper/webhookHandler.ts @@ -4,7 +4,7 @@ import { logger } from '../../utils/logger'; import { OnRamperFiatTransaction } from './fiatTransaction'; import { i18n } from '../../utils/errorMessages'; -// tslint:disable:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const sha256 = require('js-sha256'); const onramperSecret = process.env.ONRAMPER_SECRET as string; diff --git a/src/services/poignArt/api.test.ts b/src/services/poignArt/api.test.ts index d8ccd297f..e376b4774 100644 --- a/src/services/poignArt/api.test.ts +++ b/src/services/poignArt/api.test.ts @@ -1,7 +1,7 @@ -import { getPoignArtWithdrawals } from './api'; -import { assert } from 'chai'; -import { generateRandomEtheriumAddress } from '../../../test/testUtils'; -import { convertTimeStampToSeconds } from '../../utils/utils'; +// import { getPoignArtWithdrawals } from './api'; +// import { assert } from 'chai'; +// import { generateRandomEtheriumAddress } from '../../../test/testUtils'; +// import { convertTimeStampToSeconds } from '../../utils/utils'; // As we get deployment `dan13ram/poignart-rinkeby` does not exist, I commented these test cases // describe( @@ -9,30 +9,30 @@ import { convertTimeStampToSeconds } from '../../utils/utils'; // getPoignArtWithdrawalsTestCases, // ); -function getPoignArtWithdrawalsTestCases() { - const unchainWalletAddress = process.env - .POIGN_ART_RECIPIENT_ADDRESS as string; - it('should return result for unchain address', async () => { - const withdrawals = await getPoignArtWithdrawals({ - recipient: unchainWalletAddress, - startTimestamp: 0, - }); - assert.isTrue(withdrawals.length > 0); - assert.equal(withdrawals[0].recipient, unchainWalletAddress.toLowerCase()); - }); - it('should return empty array for random walletAddress', async () => { - const withdrawals = await getPoignArtWithdrawals({ - recipient: generateRandomEtheriumAddress(), - startTimestamp: 0, - }); - assert.equal(withdrawals.length, 0); - }); - - it('should return empty array for now startTimestamp', async () => { - const withdrawals = await getPoignArtWithdrawals({ - recipient: unchainWalletAddress, - startTimestamp: convertTimeStampToSeconds(new Date().getTime()), - }); - assert.equal(withdrawals.length, 0); - }); -} +// function getPoignArtWithdrawalsTestCases() { +// const unchainWalletAddress = process.env +// .POIGN_ART_RECIPIENT_ADDRESS as string; +// it('should return result for unchain address', async () => { +// const withdrawals = await getPoignArtWithdrawals({ +// recipient: unchainWalletAddress, +// startTimestamp: 0, +// }); +// assert.isTrue(withdrawals.length > 0); +// assert.equal(withdrawals[0].recipient, unchainWalletAddress.toLowerCase()); +// }); +// it('should return empty array for random walletAddress', async () => { +// const withdrawals = await getPoignArtWithdrawals({ +// recipient: generateRandomEtheriumAddress(), +// startTimestamp: 0, +// }); +// assert.equal(withdrawals.length, 0); +// }); +// +// it('should return empty array for now startTimestamp', async () => { +// const withdrawals = await getPoignArtWithdrawals({ +// recipient: unchainWalletAddress, +// startTimestamp: convertTimeStampToSeconds(new Date().getTime()), +// }); +// assert.equal(withdrawals.length, 0); +// }); +// } diff --git a/src/services/poignArt/syncPoignArtDonationCronJob.ts b/src/services/poignArt/syncPoignArtDonationCronJob.ts index 527367b48..63557813f 100644 --- a/src/services/poignArt/syncPoignArtDonationCronJob.ts +++ b/src/services/poignArt/syncPoignArtDonationCronJob.ts @@ -1,6 +1,6 @@ +import { schedule } from 'node-cron'; import config from '../../config'; import { logger } from '../../utils/logger'; -import { schedule } from 'node-cron'; import { Donation, DONATION_STATUS, diff --git a/src/services/powerBoostingService.test.ts b/src/services/powerBoostingService.test.ts index 358bf89d8..d0f3bac1b 100644 --- a/src/services/powerBoostingService.test.ts +++ b/src/services/powerBoostingService.test.ts @@ -1,3 +1,4 @@ +import { assert } from 'chai'; import { createProjectData, generateRandomEtheriumAddress, @@ -9,7 +10,6 @@ import { findUserPowerBoosting, setMultipleBoosting, } from '../repositories/powerBoostingRepository'; -import { assert } from 'chai'; import { changeUserBoostingsAfterProjectCancelled } from './powerBoostingService'; describe( @@ -22,9 +22,8 @@ function changeUserBoostingsAfterProjectCancelledTestCases() { const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const user2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const firstProject = await saveProjectDirectlyToDb(createProjectData()); - const projectThatWouldGetCancelled = await saveProjectDirectlyToDb( - createProjectData(), - ); + const projectThatWouldGetCancelled = + await saveProjectDirectlyToDb(createProjectData()); await setMultipleBoosting({ userId: user1.id, projectIds: [firstProject.id, projectThatWouldGetCancelled.id], @@ -53,9 +52,8 @@ function changeUserBoostingsAfterProjectCancelledTestCases() { }); it('should change user percentage to zero when project cancelled, even when just has 1 boositng', async () => { const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const projectThatWouldGetCancelled = await saveProjectDirectlyToDb( - createProjectData(), - ); + const projectThatWouldGetCancelled = + await saveProjectDirectlyToDb(createProjectData()); await setMultipleBoosting({ userId: user1.id, projectIds: [projectThatWouldGetCancelled.id], diff --git a/src/services/powerSnapshotServices.ts b/src/services/powerSnapshotServices.ts index fcc19395a..800a9b0ff 100644 --- a/src/services/powerSnapshotServices.ts +++ b/src/services/powerSnapshotServices.ts @@ -2,7 +2,6 @@ import { findInCompletePowerSnapShots, updatePowerSnapShots, } from '../repositories/powerSnapshotRepository'; -import { getTimestampInSeconds } from '../utils/utils'; import { getRoundNumberByDate } from '../utils/powerBoostingUtils'; import { logger } from '../utils/logger'; diff --git a/src/services/priceService.ts b/src/services/priceService.ts new file mode 100644 index 000000000..ef4cd3b94 --- /dev/null +++ b/src/services/priceService.ts @@ -0,0 +1,58 @@ +import { logger } from '../utils/logger'; +import { fetchGivPrice } from './givPriceService'; +import { CryptoComparePriceAdapter } from '../adapters/price/CryptoComparePriceAdapter'; +import { CoingeckoPriceAdapter } from '../adapters/price/CoingeckoPriceAdapter'; +import { MonoswapPriceAdapter } from '../adapters/price/MonoswapPriceAdapter'; + +export interface CustomToken { + symbol: string; + cryptoCompareId?: string; + coingeckoId?: string; + isStableCoin?: boolean; +} + +export const getTokenPrice = async ( + chainId: number, + token: CustomToken, +): Promise => { + if (!token) { + return 0; + } + try { + const { symbol, cryptoCompareId, isStableCoin, coingeckoId } = token; + let priceUsd: number; + if (isStableCoin) { + priceUsd = 1; + // } else if (currency === 'mpETH') { + // const mpEthPriceInUsd = await fetchMpEthPrice(); + // donation.priceUsd = toFixNumber(mpEthPriceInUsd, 4); + // donation.valueUsd = toFixNumber(donation.amount * mpEthPriceInUsd, 4); + } else if (symbol === 'GIV') { + const { givPriceInUsd } = await fetchGivPrice(); + priceUsd = givPriceInUsd; + } else if (cryptoCompareId) { + priceUsd = await new CryptoComparePriceAdapter().getTokenPrice({ + symbol: cryptoCompareId, + networkId: chainId, + }); + } else if (coingeckoId) { + priceUsd = await new CoingeckoPriceAdapter().getTokenPrice({ + symbol: coingeckoId, + networkId: chainId, + }); + } else { + priceUsd = await new MonoswapPriceAdapter().getTokenPrice({ + symbol, + networkId: chainId, + }); + } + return Number(priceUsd || 0); + } catch (error) { + logger.debug('getTokenPrice() error', { + token, + chainId, + error, + }); + throw new Error(error); + } +}; diff --git a/src/services/projectService.ts b/src/services/projectService.ts index 7c51f363e..e360c5235 100644 --- a/src/services/projectService.ts +++ b/src/services/projectService.ts @@ -4,6 +4,7 @@ export const getAppropriateSlug = async ( slugBase: string, projectId?: number, ): Promise => { + if (!slugBase) throw new Error('slugBase is required'); let slug = slugBase.toLowerCase(); const query = Project.createQueryBuilder('project') // check current slug and previous slugs diff --git a/src/services/projectUpdatesService.test.ts b/src/services/projectUpdatesService.test.ts index 68b09ab06..4a1ff8ab4 100644 --- a/src/services/projectUpdatesService.test.ts +++ b/src/services/projectUpdatesService.test.ts @@ -2,7 +2,6 @@ import { assert } from 'chai'; import { createProjectData, saveProjectDirectlyToDb, - SEED_DATA, } from '../../test/testUtils'; import { Project, ProjectUpdate } from '../entities/project'; import { updateTotalProjectUpdatesOfAProject } from './projectUpdatesService'; @@ -33,8 +32,8 @@ function updateTotalProjectUpdatesOfAProjectTestCases() { "userId","projectId",content,title,"createdAt","isMain" ) VALUES ( ${Number(project.admin)}, ${project.id}, '', '', '${ - new Date().toISOString().split('T')[0] - }', false + new Date().toISOString().split('T')[0] + }', false )`); await updateTotalProjectUpdatesOfAProject(project.id); const updatedProject = (await findProjectById(project.id)) as Project; diff --git a/src/services/projectVerificationFormService.ts b/src/services/projectVerificationFormService.ts index ab8e39472..40ff7ae6b 100644 --- a/src/services/projectVerificationFormService.ts +++ b/src/services/projectVerificationFormService.ts @@ -28,11 +28,7 @@ import { updateProjectVerificationStatus, updateTermsAndConditionsOfProjectVerification, } from '../repositories/projectVerificationRepository'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { ProjectVerificationUpdateInput } from '../resolvers/types/ProjectVerificationUpdateInput'; import { removeUndefinedFieldsFromObject } from '../utils/utils'; @@ -65,7 +61,7 @@ export const updateProjectVerificationFormByUser = async (params: { projectVerificationForm: ProjectVerificationForm; projectVerificationUpdateInput: ProjectVerificationUpdateInput; }): Promise => { - const { projectVerificationUpdateInput, projectVerificationForm } = params; + const { projectVerificationUpdateInput } = params; const { projectVerificationId, step } = projectVerificationUpdateInput; const personalInfo = projectVerificationUpdateInput.personalInfo as PersonalInfo; @@ -145,7 +141,7 @@ export const updateProjectVerificationFormByUser = async (params: { milestones, }); break; - case PROJECT_VERIFICATION_STEPS.TERM_AND_CONDITION: + case PROJECT_VERIFICATION_STEPS.TERM_AND_CONDITION: { validateWithJoiSchema( { isTermAndConditionsAccepted, @@ -173,6 +169,7 @@ export const updateProjectVerificationFormByUser = async (params: { projectVerificationId, }); break; + } default: throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_STEP)); } diff --git a/src/services/projectViewService.test.ts b/src/services/projectViewService.test.ts index 6d30bc3ed..675e10668 100644 --- a/src/services/projectViewService.test.ts +++ b/src/services/projectViewService.test.ts @@ -1,3 +1,5 @@ +import moment from 'moment'; +import { assert } from 'chai'; import { getQfRoundActualDonationDetails, refreshProjectActualMatchingView, @@ -5,7 +7,6 @@ import { import { QfRound } from '../entities/qfRound'; import { Project } from '../entities/project'; import { NETWORK_IDS } from '../provider'; -import moment from 'moment'; import { createDonationData, createProjectData, @@ -14,7 +15,6 @@ import { saveProjectDirectlyToDb, saveUserDirectlyToDb, } from '../../test/testUtils'; -import { assert } from 'chai'; describe( 'getQfRoundActualDonationDetails test cases', @@ -105,7 +105,7 @@ function getQfRoundActualDonationDetailsTestCases() { user.passportScore = qfr.minimumPassportScore; await user.save(); - const donation = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( { ...createDonationData(), status: 'verified', diff --git a/src/services/projectViewsService.ts b/src/services/projectViewsService.ts index 901f6d3b1..d8b0405d4 100644 --- a/src/services/projectViewsService.ts +++ b/src/services/projectViewsService.ts @@ -32,7 +32,7 @@ export const refreshProjectDonationSummaryView = async (): Promise => { }; export const getQfRoundActualDonationDetails = async ( - qfRoundId: Number, + qfRoundId: number, ): Promise => { try { const qfRound = await QfRound.createQueryBuilder('qfRound') @@ -97,10 +97,11 @@ export const getQfRoundActualDonationDetails = async ( totalValuesOfUserDonationsAfterAnalysis: row?.totalValuesOfUserDonationsAfterAnalysis?.join('-'), uniqueUserIdsAfterAnalysis: row?.uniqueUserIdsAfterAnalysis?.join('-'), + projectOwnerEmail: row?.email, // can be empty for new users }; }); - logger.info( - 'Data that we should upload to googlesheet', + logger.debug( + 'Data that we should upload to Google sheet', qfRoundDonationsRows, ); diff --git a/src/services/qfRoundService.test.ts b/src/services/qfRoundService.test.ts index afb80d9cb..470fd536f 100644 --- a/src/services/qfRoundService.test.ts +++ b/src/services/qfRoundService.test.ts @@ -1,11 +1,11 @@ +import { assert } from 'chai'; +import moment from 'moment'; import { createProjectData, saveProjectDirectlyToDb, } from '../../test/testUtils'; import { QfRound } from '../entities/qfRound'; import { relatedActiveQfRoundForProject } from './qfRoundService'; -import { assert } from 'chai'; -import moment from 'moment'; describe( 'relatedActiveQfRoundForProject', diff --git a/src/services/qfRoundService.ts b/src/services/qfRoundService.ts index a2de12dfe..65e495b69 100644 --- a/src/services/qfRoundService.ts +++ b/src/services/qfRoundService.ts @@ -19,6 +19,6 @@ export const relatedActiveQfRoundForProject = async ( return qfRound; }; -export const isQfRoundHasEnded = (params: { endDate: Date }): Boolean => { +export const isQfRoundHasEnded = (params: { endDate: Date }): boolean => { return new Date() >= params.endDate; }; diff --git a/src/services/reactionsService.test.ts b/src/services/reactionsService.test.ts index ec8be934f..bc271e825 100644 --- a/src/services/reactionsService.test.ts +++ b/src/services/reactionsService.test.ts @@ -1,18 +1,10 @@ import { assert } from 'chai'; -import { - isTokenAcceptableForProject, - updateTotalDonationsOfProject, -} from './donationService'; -import { NETWORK_IDS } from '../provider'; + import { createProjectData, - DONATION_SEED_DATA, - saveDonationDirectlyToDb, saveProjectDirectlyToDb, SEED_DATA, } from '../../test/testUtils'; -import { Token } from '../entities/token'; -import { ORGANIZATION_LABELS } from '../entities/organization'; import { Project } from '../entities/project'; import { updateTotalReactionsOfAProject } from './reactionsService'; import { Reaction } from '../entities/reaction'; diff --git a/src/services/recurringDonationService.test.ts b/src/services/recurringDonationService.test.ts new file mode 100644 index 000000000..d5c80e45d --- /dev/null +++ b/src/services/recurringDonationService.test.ts @@ -0,0 +1,326 @@ +import { assert } from 'chai'; +import { + createProjectData, + generateRandomEtheriumAddress, + generateRandomEvmTxHash, + saveProjectDirectlyToDb, + saveRecurringDonationDirectlyToDb, + saveUserDirectlyToDb, +} from '../../test/testUtils'; +import { + createRelatedDonationsToStream, + updateRecurringDonationStatusWithNetwork, +} from './recurringDonationService'; +import { Donation } from '../entities/donation'; +import { addNewAnchorAddress } from '../repositories/anchorContractAddressRepository'; +import { NETWORK_IDS } from '../provider'; +import { findRecurringDonationById } from '../repositories/recurringDonationRepository'; +import { + RECURRING_DONATION_STATUS, + RecurringDonation, +} from '../entities/recurringDonation'; +import { AnchorContractAddress } from '../entities/anchorContractAddress'; + +describe( + 'createRelatedDonationsToStream test cases', + createRelatedDonationsToStreamTestCases, +); + +describe( + 'updateRecurringDonationStatusWithNetwork test cases', + updateRecurringDonationStatusWithNetworkTestCases, +); + +function updateRecurringDonationStatusWithNetworkTestCases() { + it('should verify transaction from OP Sepolia #1 createFlow', async () => { + // https://sepolia-optimism.etherscan.io/tx/0x516567c51c3506afe1291f7055fa0e858cc2ca9ed4079625c747fe92bd125a10 + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const contractCreator = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + + const donor = await saveUserDirectlyToDb( + '0x871cd6353b803ceceb090bb827ecb2f361db81ab', + ); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator: contractCreator, + address: '0x1190f5ac0f509d8f3f4b662bf17437d37d64527c', + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + txHash: generateRandomEvmTxHash(), + }); + + const recurringDonation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + anchorContractAddressId: anchorContractAddress.id, + currency: 'ETH', + status: RECURRING_DONATION_STATUS.PENDING, + txHash: + '0x516567c51c3506afe1291f7055fa0e858cc2ca9ed4079625c747fe92bd125a10', + donorId: donor.id, + flowRate: '285225986', + }, + }); + const updatedDonation = await updateRecurringDonationStatusWithNetwork({ + donationId: recurringDonation.id, + }); + assert.equal(updatedDonation.status, RECURRING_DONATION_STATUS.ACTIVE); + await RecurringDonation.delete({ id: recurringDonation.id }); + await AnchorContractAddress.delete({ id: anchorContractAddress.id }); + }); + + it('should verify transaction from OP Sepolia #2 batchCall', async () => { + // https://sepolia-optimism.etherscan.io/tx/0x1833603bc894448b54cf9c03483fa361508fa101abcfa6c3b6ef51425cab533f + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const contractCreator = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + + const donor = await saveUserDirectlyToDb( + '0xa1179f64638adb613ddaac32d918eb6beb824104', + ); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator: contractCreator, + address: '0xe6375bc298aEB29D173B2AB359D492439A43b268', + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + txHash: generateRandomEvmTxHash(), + }); + + const recurringDonation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + anchorContractAddressId: anchorContractAddress.id, + currency: 'ETH', + status: RECURRING_DONATION_STATUS.PENDING, + txHash: + '0x1833603bc894448b54cf9c03483fa361508fa101abcfa6c3b6ef51425cab533f', + donorId: donor.id, + flowRate: '152207001', + isBatch: true, + }, + }); + const updatedDonation = await updateRecurringDonationStatusWithNetwork({ + donationId: recurringDonation.id, + }); + assert.equal(updatedDonation.status, RECURRING_DONATION_STATUS.ACTIVE); + await RecurringDonation.delete({ id: recurringDonation.id }); + await AnchorContractAddress.delete({ id: anchorContractAddress.id }); + }); + + it('should verify transaction from OP Sepolia when updateFlow function of smart contract has been called', async () => { + // https://sepolia-optimism.etherscan.io/tx/0x74d98ba95c7969746afc38e46748aa64f239e816785be74b03372397cf844986 + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const contractCreator = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + + const donor = await saveUserDirectlyToDb( + '0xf577ae8b97d839b9c0522a620299dc08792c738c', + ); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator: contractCreator, + address: '0x0015cE4FeA643B64000400B0e61F4C03E020b75f', + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + txHash: generateRandomEvmTxHash(), + }); + + const recurringDonation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + anchorContractAddressId: anchorContractAddress.id, + currency: 'ETH', + status: RECURRING_DONATION_STATUS.PENDING, + txHash: + '0x74d98ba95c7969746afc38e46748aa64f239e816785be74b03372397cf844986', + donorId: donor.id, + flowRate: '23194526400669', + }, + }); + const updatedDonation = await updateRecurringDonationStatusWithNetwork({ + donationId: recurringDonation.id, + }); + assert.equal(updatedDonation.status, RECURRING_DONATION_STATUS.ACTIVE); + await RecurringDonation.delete({ id: recurringDonation.id }); + await AnchorContractAddress.delete({ id: anchorContractAddress.id }); + }); + it('should remain pending, different toAddress from OP Sepolia', async () => { + // https://sepolia-optimism.etherscan.io/tx/0x516567c51c3506afe1291f7055fa0e858cc2ca9ed4079625c747fe92bd125a10 + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const contractCreator = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + + const donor = await saveUserDirectlyToDb( + '0x871cd6353b803ceceb090bb827ecb2f361db81ab', + ); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator: contractCreator, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + txHash: generateRandomEvmTxHash(), + }); + + const recurringDonation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + anchorContractAddressId: anchorContractAddress.id, + currency: 'ETH', + status: RECURRING_DONATION_STATUS.PENDING, + txHash: + '0x516567c51c3506afe1291f7055fa0e858cc2ca9ed4079625c747fe92bd125a10', + donorId: donor.id, + flowRate: '285225986', + }, + }); + const updatedDonation = await updateRecurringDonationStatusWithNetwork({ + donationId: recurringDonation.id, + }); + assert.equal(updatedDonation.status, RECURRING_DONATION_STATUS.PENDING); + + await RecurringDonation.delete({ id: recurringDonation.id }); + await AnchorContractAddress.delete({ id: anchorContractAddress.id }); + }); + it('should donation remain pending, different amount from OP Sepolia', async () => { + // https://sepolia-optimism.etherscan.io/tx/0x516567c51c3506afe1291f7055fa0e858cc2ca9ed4079625c747fe92bd125a10 + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const contractCreator = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + + const donor = await saveUserDirectlyToDb( + '0x871cd6353b803ceceb090bb827ecb2f361db81ab', + ); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator: contractCreator, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, + txHash: generateRandomEvmTxHash(), + }); + + const recurringDonation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + anchorContractAddressId: anchorContractAddress.id, + currency: 'ETH', + status: RECURRING_DONATION_STATUS.PENDING, + txHash: + '0x516567c51c3506afe1291f7055fa0e858cc2ca9ed4079625c747fe92bd125a10', + donorId: donor.id, + flowRate: '10000000', + }, + }); + const updatedDonation = await updateRecurringDonationStatusWithNetwork({ + donationId: recurringDonation.id, + }); + assert.equal(updatedDonation.status, RECURRING_DONATION_STATUS.PENDING); + + await RecurringDonation.delete({ id: recurringDonation.id }); + await AnchorContractAddress.delete({ id: anchorContractAddress.id }); + }); +} + +function createRelatedDonationsToStreamTestCases() { + // TODO As I changed superFluid adapter to user staging address + // And return not mockAdapter in test more this test is not valid anymore + // I will skip it for now but we will keep it here for future reference + it.skip('should search by the currency', async () => { + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + createProjectData(), + projectOwner, + ); + const contractCreator = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: projectOwner, + creator: contractCreator, + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + + const recurringDonation = await saveRecurringDonationDirectlyToDb({ + donationData: { + projectId: project.id, + anchorContractAddressId: anchorContractAddress.id, + currency: 'Daix', + status: 'pending', + }, + }); + + const recurringDonationWithAnchorContract = await findRecurringDonationById( + recurringDonation.id, + ); + + await createRelatedDonationsToStream(recurringDonationWithAnchorContract!); + + const recurringDonationUpdated = await findRecurringDonationById( + recurringDonationWithAnchorContract!.id, + ); + + const donations = await Donation.createQueryBuilder('donation') + .where(`donation."recurringDonationId" = :recurringDonationId`, { + recurringDonationId: recurringDonationWithAnchorContract!.id, + }) + .getMany(); + + // STREAM TEST DATA HAS ENDED STATUS + assert.equal( + recurringDonationUpdated?.status, + RECURRING_DONATION_STATUS.ENDED, + ); + assert.equal(donations.length, 4); + assert.equal(true, true); // its not saving the recurring donation Id, saving as null + // add more tests, define criteria for verified + }); +} diff --git a/src/services/recurringDonationService.ts b/src/services/recurringDonationService.ts index 139ba23d9..5069e7f76 100644 --- a/src/services/recurringDonationService.ts +++ b/src/services/recurringDonationService.ts @@ -1,11 +1,403 @@ -import { RecurringDonation } from '../entities/recurringDonation'; -import { findRecurringDonationById } from '../repositories/recurringDonationRepository'; +import path from 'path'; +import { promises as fs } from 'fs'; +import { ethers } from 'ethers'; +import { + getNotificationAdapter, + getSuperFluidAdapter, +} from '../adapters/adaptersFactory'; +import { DONATION_STATUS, Donation } from '../entities/donation'; +import { + RECURRING_DONATION_STATUS, + RecurringDonation, +} from '../entities/recurringDonation'; +import { Token } from '../entities/token'; +import { getProvider, NETWORK_IDS, superTokensToToken } from '../provider'; +import { findProjectRecipientAddressByNetworkId } from '../repositories/projectAddressRepository'; +import { findProjectById } from '../repositories/projectRepository'; +import { + findRecurringDonationById, + updateRecurringDonationFromTheStreamDonations, +} from '../repositories/recurringDonationRepository'; +import { findUserById } from '../repositories/userRepository'; +import { ChainType } from '../types/network'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; +import { logger } from '../utils/logger'; +import { + isTokenAcceptableForProject, + updateDonationPricesAndValues, + updateTotalDonationsOfProject, +} from './donationService'; +import { calculateGivbackFactor } from './givbackService'; +import { relatedActiveQfRoundForProject } from './qfRoundService'; +import { updateUserTotalDonated, updateUserTotalReceived } from './userService'; +import config from '../config'; +import { User } from '../entities/user'; + +// Initially it will only be monthly data +export const priceDisplay = 'month'; + +export const fetchStreamTableStartDate = ( + recurringDonation: RecurringDonation, +): number => { + if (recurringDonation.donations && recurringDonation.donations.length > 0) { + const latestDonation = recurringDonation?.donations?.reduce( + (prev, current) => { + return prev.createdAt > current.createdAt ? prev : current; + }, + ); + + return Math.floor(latestDonation?.createdAt?.getTime() / 1000); + } + + return Math.floor(recurringDonation.createdAt.getTime() / 1000); +}; + +export const createRelatedDonationsToStream = async ( + recurringDonation: RecurringDonation, +) => { + const superFluidAdapter = getSuperFluidAdapter(); + const streamData = await superFluidAdapter.streamPeriods({ + address: recurringDonation.anchorContractAddress.address, + chain: recurringDonation.networkId, + start: fetchStreamTableStartDate(recurringDonation), + end: Math.floor(new Date().getTime() / 1000), + priceGranularity: priceDisplay, + virtualization: priceDisplay, + currency: 'USD', + recurringDonationTxHash: recurringDonation.txHash, + }); + + if ( + streamData && + recurringDonation.status === RECURRING_DONATION_STATUS.PENDING + ) { + recurringDonation.status = RECURRING_DONATION_STATUS.ACTIVE; + await recurringDonation.save(); + } + + if (streamData.stoppedAtTimestamp) { + recurringDonation.finished = true; + recurringDonation.status = RECURRING_DONATION_STATUS.ENDED; + await recurringDonation.save(); + } + + const project = await findProjectById(recurringDonation.projectId); + const donorUser = await findUserById(recurringDonation.donorId); + + if (!project) return; + if (!donorUser) return; + + const uniquePeriods: any[] = []; + + for (const period of streamData.virtualPeriods) { + const existingPeriod = await Donation.findOne({ + where: { + recurringDonationId: recurringDonation.id, + virtualPeriodStart: period.startTime, + virtualPeriodEnd: period.endTime, + }, + }); + + if (!existingPeriod) { + uniquePeriods.push({ + startTime: period.startTime, + endTime: period.endTime, + amount: period.amount, + amountFiat: period.amountFiat, + }); + } + } + // create donation if any virtual period is missing + if (uniquePeriods.length === 0) return; + + for (const streamPeriod of uniquePeriods) { + try { + const environment = config.get('ENVIRONMENT') as string; + + const networkId: number = + environment !== 'production' + ? NETWORK_IDS.OPTIMISM_SEPOLIA + : NETWORK_IDS.OPTIMISTIC; + + const symbolCurrency = recurringDonation.currency.includes('x') + ? superTokensToToken[recurringDonation.currency] + : recurringDonation.currency; + const tokenInDb = await Token.findOne({ + where: { + networkId, + symbol: symbolCurrency, + }, + }); + const isCustomToken = !tokenInDb; + let isTokenEligibleForGivback = false; + if (isCustomToken && !project!.organization.supportCustomTokens) { + throw new Error(i18n.__(translationErrorMessagesKeys.TOKEN_NOT_FOUND)); + } else if (tokenInDb) { + const acceptsToken = await isTokenAcceptableForProject({ + projectId: project!.id, + tokenId: tokenInDb.id, + }); + if (!acceptsToken && !project!.organization.supportCustomTokens) { + throw new Error( + i18n.__( + translationErrorMessagesKeys.PROJECT_DOES_NOT_SUPPORT_THIS_TOKEN, + ), + ); + } + isTokenEligibleForGivback = tokenInDb.isGivbackEligible; + } + const projectRelatedAddress = + await findProjectRecipientAddressByNetworkId({ + projectId: project.id, + networkId, + }); + if (!projectRelatedAddress) { + throw new Error( + i18n.__( + translationErrorMessagesKeys.THERE_IS_NO_RECIPIENT_ADDRESS_FOR_THIS_NETWORK_ID_AND_PROJECT, + ), + ); + } + + const toAddress = projectRelatedAddress?.address?.toLowerCase(); + const fromAddress = donorUser.walletAddress?.toLowerCase(); + const transactionTx = `${streamData.id?.toLowerCase()}-${streamPeriod.endTime}`; + const donation = Donation.create({ + amount: normalizeNegativeAmount( + streamPeriod.amount, + tokenInDb!.decimals, + ), + + // prevent setting NaN value for valueUsd + valueUsd: Math.abs(Number(streamPeriod.amountFiat)) || 0, + transactionId: transactionTx, + isFiat: false, + transactionNetworkId: networkId, + currency: tokenInDb?.symbol, + user: donorUser, + tokenAddress: tokenInDb?.address, + project, + status: DONATION_STATUS.VERIFIED, + isTokenEligibleForGivback, + isCustomToken, + isProjectVerified: project.verified, + createdAt: new Date(), + segmentNotified: false, + toWalletAddress: toAddress, + fromWalletAddress: fromAddress, + recurringDonation, + anonymous: Boolean(recurringDonation.anonymous), + chainType: ChainType.EVM, + virtualPeriodStart: streamPeriod.startTime, + virtualPeriodEnd: streamPeriod.endTime, + }); + + await donation.save(); + logger.debug(`Streamed donation has been created successfully`, { + donationId: donation.id, + recurringDonationId: recurringDonation.id, + amount: donation.amount, + }); + + const activeQfRoundForProject = await relatedActiveQfRoundForProject( + project.id, + ); + + if ( + activeQfRoundForProject && + activeQfRoundForProject.isEligibleNetwork(networkId) + ) { + donation.qfRound = activeQfRoundForProject; + } + + const { givbackFactor, projectRank, bottomRankInRound, powerRound } = + await calculateGivbackFactor(project.id); + donation.givbackFactor = givbackFactor; + donation.projectRank = projectRank; + donation.bottomRankInRound = bottomRankInRound; + donation.powerRound = powerRound; + + await donation.save(); + + if (!donation.valueUsd || donation.valueUsd === 0) { + await updateDonationPricesAndValues( + donation, + project, + tokenInDb!, + donation.transactionNetworkId, + ); + } + + logger.debug(`Streamed donation After filling valueUsd`, { + donationId: donation.id, + recurringDonationId: recurringDonation.id, + amount: donation.amount, + valueUsd: donation.valueUsd, + }); + await updateRecurringDonationFromTheStreamDonations(recurringDonation.id); + + await updateUserTotalDonated(donation.userId); + + // After updating price we update totalDonations + await updateTotalDonationsOfProject(donation.projectId); + await updateUserTotalReceived(project!.adminUser.id); + } catch (e) { + logger.error( + 'createRelatedDonationsToStream() error', + { + recurringDonationId: recurringDonation.id, + }, + e, + ); + } + } +}; + +export function normalizeNegativeAmount( + amount: string, + decimals: number, +): number { + return Math.abs(Number(amount)) / 10 ** decimals; +} + +export const getRecurringDonationTxInfo = async (params: { + txHash: string; + networkId: number; + isBatch: boolean; +}): Promise< + { + receiver: string; + flowRate: string; + tokenAddress: string; + }[] +> => { + const { txHash, networkId, isBatch } = params; + const output: { + receiver: string; + flowRate: string; + tokenAddress: string; + }[] = []; + + logger.debug('getRecurringDonationTxInfo() has been called', params); + + try { + const web3Provider = getProvider(networkId); + const networkData = await web3Provider.getTransaction(txHash); + if (!networkData) { + logger.error( + 'Transaction not found in the network. maybe its not mined yet', + { + networkId, + txHash, + }, + ); + throw new Error( + i18n.__(translationErrorMessagesKeys.TRANSACTION_NOT_FOUND), + ); + } + + let receiverLowercase = ''; + let flowRateBigNumber = ''; + let tokenAddress = ''; + + if (!isBatch) { + const abiPath = path.join(__dirname, '../abi/superFluidAbi.json'); + const abi = JSON.parse(await fs.readFile(abiPath, 'utf-8')); + const iface = new ethers.utils.Interface(abi); + const decodedData = iface.parseTransaction({ data: networkData.data }); + tokenAddress = decodedData.args[0].toLowerCase(); + receiverLowercase = decodedData.args[2].toLowerCase(); + flowRateBigNumber = decodedData.args[3]; + output.push({ + tokenAddress, + receiver: receiverLowercase, + flowRate: ethers.BigNumber.from(flowRateBigNumber).toString(), + }); + } else { + // ABI comes from https://sepolia-optimism.etherscan.io/address/0x78743a68d52c9d6ccf3ff4558f3af510592e3c2d#code + const abiPath = path.join(__dirname, '../abi/superFluidAbiBatch.json'); + const abi = JSON.parse(await fs.readFile(abiPath, 'utf-8')); + const iface = new ethers.utils.Interface(abi); + const decodedData = iface.parseTransaction({ data: networkData.data }); + + for (const bachItem of decodedData.args[0]) { + // console.log('opData', decodedData.args) + const operationData = bachItem[2]; + const decodedOperationData = ethers.utils.defaultAbiCoder.decode( + ['bytes', 'bytes'], + operationData, + ); + const abiPath2 = path.join( + __dirname, + '../abi/superFluidAbi_batch_decoded.json', + ); + const decodedDataAbi = JSON.parse(await fs.readFile(abiPath2, 'utf-8')); + const decodedDataIface = new ethers.utils.Interface(decodedDataAbi); + const finalDecodedData = decodedDataIface.parseTransaction({ + data: decodedOperationData[0], + }); + receiverLowercase = finalDecodedData.args[1].toLowerCase(); + flowRateBigNumber = finalDecodedData.args[2]; + tokenAddress = decodedData.args[0].toLowerCase(); + output.push({ + tokenAddress, + receiver: receiverLowercase, + flowRate: ethers.BigNumber.from(flowRateBigNumber).toString(), + }); + } + } + + return output; + } catch (e) { + logger.error('getRecurringDonationTxInfo() error', { + error: e, + params, + }); + throw e; + } +}; export const updateRecurringDonationStatusWithNetwork = async (params: { donationId: number; }): Promise => { - // TODO Should implement it - return (await findRecurringDonationById( - params.donationId, - )) as RecurringDonation; + logger.debug( + 'updateRecurringDonationStatusWithNetwork() has been called', + params, + ); + const recurringDonation = await findRecurringDonationById(params.donationId); + if (!recurringDonation) { + throw new Error('Recurring donation not found'); + } + + try { + const superFluidAdapter = getSuperFluidAdapter(); + const txData = await superFluidAdapter.getFlowByTxHash({ + receiver: + recurringDonation?.anchorContractAddress?.address?.toLowerCase() as string, + flowRate: recurringDonation.flowRate, + sender: recurringDonation?.donor?.walletAddress?.toLowerCase() as string, + transactionHash: recurringDonation.txHash, + }); + if (!txData) { + throw new Error( + `SuperFluid tx not found in the subgraph txHash:${recurringDonation.txHash}`, + ); + } + recurringDonation.status = RECURRING_DONATION_STATUS.ACTIVE; + await recurringDonation.save(); + const project = recurringDonation.project; + const projectOwner = await User.findOneBy({ id: project.adminUserId }); + await getNotificationAdapter().donationReceived({ + project, + user: projectOwner, + donation: recurringDonation, + }); + return recurringDonation; + } catch (e) { + logger.error('updateRecurringDonationStatusWithNetwork() error', { + error: e, + params, + }); + return recurringDonation; + } }; diff --git a/src/services/recurringDonationStreamQueue.ts b/src/services/recurringDonationStreamQueue.ts index 30108d0d4..647aa915c 100644 --- a/src/services/recurringDonationStreamQueue.ts +++ b/src/services/recurringDonationStreamQueue.ts @@ -1,9 +1,13 @@ -import { logger } from '../utils/logger'; -import { findActiveRecurringDonations } from '../repositories/recurringDonationRepository'; import Bull from 'bull'; +import { logger } from '../utils/logger'; +import { + findActiveRecurringDonations, + findRecurringDonationById, +} from '../repositories/recurringDonationRepository'; import { redisConfig } from '../redis'; import config from '../config'; import { getCurrentDateFormatted } from '../utils/utils'; +import { createRelatedDonationsToStream } from './recurringDonationService'; const updateRecurringDonationsStreamQueue = new Bull( 'update-recurring-donations-stream-queue', @@ -72,6 +76,14 @@ const numberOfUpdateRecurringDonationsStreamConcurrentJob = export const updateRecurringDonationStream = async (params: { recurringDonationId: number; }) => { - // TODO Implement this (Get stream from blockchain and update the recurring donations) - logger.debug('updateRecurringDonationStream() has been called'); + logger.debug( + 'updateRecurringDonationStream() has been called for id', + params.recurringDonationId, + ); + const recurringDonation = await findRecurringDonationById( + params.recurringDonationId, + ); + + if (!recurringDonation) return; + await createRelatedDonationsToStream(recurringDonation); }; diff --git a/src/services/socialProfileService.ts b/src/services/socialProfileService.ts index 34b0e0403..3c781b71a 100644 --- a/src/services/socialProfileService.ts +++ b/src/services/socialProfileService.ts @@ -1,15 +1,9 @@ import { getSocialNetworkAdapter } from '../adapters/adaptersFactory'; import { createSocialProfile, - findSocialProfileById, isSocialNetworkAddedToVerificationForm, - verifySocialProfileById, } from '../repositories/socialProfileRepository'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { SocialProfile } from '../entities/socialProfile'; import { generateRandomString } from '../utils/utils'; import { getRedisObject, setObjectInRedis } from '../redis'; diff --git a/src/services/the-giving-blocks/api.ts b/src/services/the-giving-blocks/api.ts index a110914bd..b9e5bffaf 100644 --- a/src/services/the-giving-blocks/api.ts +++ b/src/services/the-giving-blocks/api.ts @@ -1,4 +1,3 @@ -import { extractTraceparentData } from '@sentry/tracing'; import Axios, { AxiosResponse } from 'axios'; import axiosRetry from 'axios-retry'; import config from '../../config'; diff --git a/src/services/the-giving-blocks/syncProjectsCronJob.ts b/src/services/the-giving-blocks/syncProjectsCronJob.ts index c912f4bb3..e591ba81f 100644 --- a/src/services/the-giving-blocks/syncProjectsCronJob.ts +++ b/src/services/the-giving-blocks/syncProjectsCronJob.ts @@ -1,4 +1,5 @@ import { schedule } from 'node-cron'; +import slugify from 'slugify'; import { Project, ProjStatus, @@ -14,19 +15,13 @@ import { fetchOrganizationById, GivingBlockProject, } from './api'; - import config from '../../config'; -import slugify from 'slugify'; import { ProjectStatus } from '../../entities/projectStatus'; import { logger } from '../../utils/logger'; import { getAppropriateSlug, getQualityScore } from '../projectService'; import { Organization, ORGANIZATION_LABELS } from '../../entities/organization'; import { findUserById } from '../../repositories/userRepository'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; // Every week once on sunday at 0 hours const cronJobTime = diff --git a/src/services/transak/order.ts b/src/services/transak/order.ts index bdae87500..f10b04c16 100644 --- a/src/services/transak/order.ts +++ b/src/services/transak/order.ts @@ -4,9 +4,7 @@ "eventID": "ORDER_CREATED", "createdAt": "2020-02-17T01:55:05.100Z", "webhookData": { - // tslint:disable-next-line:jsdoc-format "id": "9151faa1-e69b-4a36-b959-3c4f894afb68", - // tslint:disable-next-line:jsdoc-format "walletAddress": "0x86349020e9394b2BE1b1262531B0C3335fc32F20", "createdAt": "2020-02-17T01:55:05.095Z", "status": "AWAITING_PAYMENT_FROM_USER", diff --git a/src/services/userService.test.ts b/src/services/userService.test.ts index 3bceb1937..63ee3d723 100644 --- a/src/services/userService.test.ts +++ b/src/services/userService.test.ts @@ -1,8 +1,7 @@ -import { assert, use } from 'chai'; +import { assert } from 'chai'; import 'mocha'; import { User, UserRole } from '../entities/user'; -import { Project } from '../entities/project'; -import { Donation, DONATION_STATUS } from '../entities/donation'; +import { DONATION_STATUS } from '../entities/donation'; import { createDonationData, createProjectData, @@ -10,18 +9,17 @@ import { saveDonationDirectlyToDb, saveProjectDirectlyToDb, saveUserDirectlyToDb, - SEED_DATA, } from '../../test/testUtils'; import { fetchAdminAndValidatePassword, updateUserTotalDonated, updateUserTotalReceived, -} from '../services/userService'; +} from './userService'; import { ORGANIZATION_LABELS } from '../entities/organization'; import { generateRandomString } from '../utils/utils'; -// tslint:disable-next-line:no-var-requires -const bcrypt = require('bcrypt'); import { findUserById } from '../repositories/userRepository'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const bcrypt = require('bcrypt'); describe( 'updateUserTotalDonated() test cases', @@ -42,7 +40,7 @@ function updateUserTotalDonatedTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb(createProjectData()); const valueUsd = 100; - const donation = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( { ...createDonationData(), status: DONATION_STATUS.VERIFIED, @@ -54,7 +52,7 @@ function updateUserTotalDonatedTestCases() { const user2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const donation2 = await saveDonationDirectlyToDb( + await saveDonationDirectlyToDb( { ...createDonationData(), status: DONATION_STATUS.FAILED, @@ -82,7 +80,7 @@ function updateUserTotalReceivedTestCases() { loginType: 'wallet', firstName: 'test name', }).save(); - const project = await saveProjectDirectlyToDb({ + await saveProjectDirectlyToDb({ ...createProjectData(), admin: String(user.id), organizationLabel: ORGANIZATION_LABELS.GIVING_BLOCK, diff --git a/src/services/userService.ts b/src/services/userService.ts index 91def23f3..426183534 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -2,7 +2,7 @@ import { User } from '../entities/user'; import { Donation } from '../entities/donation'; import { logger } from '../utils/logger'; import { findAdminUserByEmail } from '../repositories/userRepository'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const bcrypt = require('bcrypt'); export const updateUserTotalDonated = async (userId: number) => { @@ -13,10 +13,14 @@ export const updateUserTotalDonated = async (userId: number) => { SET "totalDonated" = ( SELECT COALESCE(SUM(d."valueUsd"),0) FROM donation as d - WHERE d."userId" = $1 AND d."status" = 'verified' + WHERE d."userId" = $1 AND d."status" = 'verified' AND d."recurringDonationId" IS NULL + ) + ( + SELECT COALESCE(SUM(rd."totalUsdStreamed"), 0) + FROM recurring_donation as rd + WHERE rd."donorId" = $1 ) WHERE "id" = $1 - `, + `, [userId], ); } catch (e) { diff --git a/src/types/GivPowerBalanceAggregator.ts b/src/types/GivPowerBalanceAggregator.ts index d54c5fe2e..b70d55553 100644 --- a/src/types/GivPowerBalanceAggregator.ts +++ b/src/types/GivPowerBalanceAggregator.ts @@ -1,5 +1,5 @@ export interface NetworksInputParams { - networks?: string; // comma separated sample: 100,420 + networks?: string; // comma separated sample: 100,11155420 network?: string | number; // comma separated sample: 100 } diff --git a/src/types/OkMixin.ts b/src/types/OkMixin.ts deleted file mode 100644 index 64b6f2a1a..000000000 --- a/src/types/OkMixin.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ClassType, Field, InputType } from 'type-graphql'; - -export const OkMixin = (BaseClass: T) => { - @InputType() - class OkInput extends BaseClass { - @Field() - ok: boolean; - } - return OkInput; -}; diff --git a/src/types/PasswordInput.ts b/src/types/PasswordInput.ts deleted file mode 100644 index 6e2bb6a9d..000000000 --- a/src/types/PasswordInput.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { MinLength } from 'class-validator'; -import { Field, InputType, ClassType } from 'type-graphql'; - -export const PasswordMixin = (BaseClass: T) => { - @InputType() - class PasswordInput extends BaseClass { - @Field() - @MinLength(5) - password: string; - } - return PasswordInput; -}; diff --git a/src/types/etherscan.ts b/src/types/etherscan.ts index c2f8ebffa..c8e804a34 100644 --- a/src/types/etherscan.ts +++ b/src/types/etherscan.ts @@ -20,3 +20,38 @@ export interface ITxInfo { methodId: string; functionName: string; } + +/** + * sample + * { + "blockNumber": "10099051", + "timeStamp": "1712000642", + "hash": "0x1833603bc894448b54cf9c03483fa361508fa101abcfa6c3b6ef51425cab533f", + "nonce": "2", + "blockHash": "0xadaebf96e0e469e00379eb014f4defc7265fd5ecb2c7df606dbd138974c9b540", + "from": "0x0000000000000000000000000000000000000000", + "contractAddress": "0xda6db863cb2ee39b196edb8159c38a1ed5c55344", + "to": "0xa1179f64638adb613ddaac32d918eb6beb824104", + "tokenID": "60697138672961111746906773636285553871149145650772398189596589717428568084291", + "tokenName": "Constant Outflow NFT", + "tokenSymbol": "COF", + "tokenDecimal": "0", + "transactionIndex": "1", + "gas": "908400", + "gasPrice": "1500000252", + "gasUsed": "691628", + "cumulativeGasUsed": "735467", + "input": "deprecated", + "confirmations": "582530" + } + */ +export interface IContractCallTxInfo { + timeStamp: string; + hash: string; + to: string; + contractAddress: string; + tokenID: string; + tokenName: string; + tokenSymbol: string; + tokenDecimal: string; +} diff --git a/src/types/projectSocialMediaType.ts b/src/types/projectSocialMediaType.ts new file mode 100644 index 000000000..38ede741c --- /dev/null +++ b/src/types/projectSocialMediaType.ts @@ -0,0 +1,19 @@ +import { registerEnumType } from 'type-graphql'; + +export enum ProjectSocialMediaType { + FACEBOOK = 'FACEBOOK', + X = 'X', + INSTAGRAM = 'INSTAGRAM', + YOUTUBE = 'YOUTUBE', + LINKEDIN = 'LINKEDIN', + REDDIT = 'REDDIT', + DISCORD = 'DISCORD', + FARCASTER = 'FARCASTER', + LENS = 'LENS', + WEBSITE = 'WEBSITE', +} + +registerEnumType(ProjectSocialMediaType, { + name: 'ProjectSocialMediaType', + description: 'The social media platform types', +}); diff --git a/src/types/qfTypes.ts b/src/types/qfTypes.ts index cede70c62..64e5f5448 100644 --- a/src/types/qfTypes.ts +++ b/src/types/qfTypes.ts @@ -2,12 +2,12 @@ import { Field, Float, ObjectType } from 'type-graphql'; @ObjectType() export class EstimatedMatching { - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) projectDonationsSqrtRootSum?: number; - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) allProjectsSum?: number; - @Field(type => Float, { nullable: true }) + @Field(_type => Float, { nullable: true }) matchingPool?: number; } diff --git a/src/user/ConfirmUserResolver.ts b/src/user/ConfirmUserResolver.ts deleted file mode 100644 index 9f0d6d660..000000000 --- a/src/user/ConfirmUserResolver.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Resolver, Mutation, Arg } from 'type-graphql'; - -import { redis } from '../redis'; -import { User } from '../entities/user'; -import { confirmUserPrefix } from '../constants/redisPrefixes'; - -@Resolver() -export class ConfirmUserResolver { - @Mutation(() => Boolean) - async confirmUser(@Arg('token') token: string): Promise { - const userId = await redis.get(confirmUserPrefix + token); - - if (!userId) { - return false; - } - - await User.update({ id: parseInt(userId, 10) }, { confirmed: true }); - await redis.del(token); - - return true; - } -} diff --git a/src/user/LoginResolver.test.ts b/src/user/LoginResolver.test.ts deleted file mode 100644 index 350bf9c0f..000000000 --- a/src/user/LoginResolver.test.ts +++ /dev/null @@ -1,2 +0,0 @@ -// TODO Write test cases -// describe('loginWallet() test cases', loginWalletTestCases); diff --git a/src/user/LoginResolver.ts b/src/user/LoginResolver.ts deleted file mode 100644 index c7ff55a03..000000000 --- a/src/user/LoginResolver.ts +++ /dev/null @@ -1,281 +0,0 @@ -// tslint:disable-next-line:no-var-requires -import { logger } from '../utils/logger'; - -import * as bcrypt from 'bcryptjs'; -import { Arg, Ctx, Mutation, Resolver } from 'type-graphql'; -import { keccak256 } from 'ethers/lib/utils'; -import { User } from '../entities/user'; -import { ApolloContext } from '../types/ApolloContext'; -import * as jwt from 'jsonwebtoken'; -import { registerEnumType, Field, ID, ObjectType } from 'type-graphql'; -import config from '../config'; -import SentryLogger from '../sentryLogger'; -import { findUserByWalletAddress } from '../repositories/userRepository'; -import { getNotificationAdapter } from '../adapters/adaptersFactory'; -// tslint:disable-next-line:no-var-requires -const sigUtil = require('eth-sig-util'); - -@ObjectType() -class LoginResponse { - @Field({ nullable: false }) - user: User; - - @Field({ nullable: false }) - token: string; -} - -enum LoginType { - Password = 'password', - SignedMessage = 'message', -} - -registerEnumType(LoginType, { - name: 'Direction', // this one is mandatory - description: 'Is the login request with a password or a signed message', // this one is optional -}); - -@Resolver() -export class LoginResolver { - hostnameWhitelist = new Set( - (config.get('HOSTNAME_WHITELIST') as string).split(','), - ); - - hostnameSignedMessageHashCache: { [id: string]: string } = {}; - // Return hash of message which should be signed by user - // Null return means no hash message is available for hostname - // Sign message differs based on application hostname (domain) in order to prevent sign-message popup in UI - getHostnameSignMessageHash(hostname: string): string | null { - const cache = this.hostnameSignedMessageHashCache; - if (cache[hostname]) return cache[hostname]; - - if ( - !this.hostnameWhitelist.has(hostname) && - !this.allowHostnameForDevelopment(hostname) - ) - return null; - - const message = config.get('OUR_SECRET') as string; - const customPrefix = `\u0019${hostname} Signed Message:\n`; - const prefixWithLength = Buffer.from( - `${customPrefix}${message.length.toString()}`, - 'utf-8', - ); - const hashedMsg = keccak256( - Buffer.concat([prefixWithLength, Buffer.from(message)]), - ); - cache[hostname] = hashedMsg; - return hashedMsg; - } - - allowHostnameForDevelopment(hostname): boolean { - if ((config.get('ENVIRONMENT') as string) === 'production') return false; - - const regex = config.get('DEVELOPMENT_HOSTNAME_REGEX') as string; - if (!regex) return false; - - if (hostname.match(regex)) return true; - - return false; - } - - // James: We don't need this right now, maybe in the future - @Mutation(() => Boolean, { nullable: true }) - async validateToken( - @Arg('token') token: string, - @Ctx() ctx: ApolloContext, - ): Promise { - const secret = config.get('JWT_SECRET') as string; - - try { - const decodedJwt: any = jwt.verify(token, secret); - return true; - } catch (error) { - SentryLogger.captureMessage(error); - - logger.error(`Apollo Server error : ${JSON.stringify(error, null, 2)}`); - logger.error(`Error for token ${token}`); - return false; - } - } - - @Mutation(() => LoginResponse, { nullable: true }) - async login( - @Arg('email') email: string, - @Arg('password') password: string, - @Arg('loginType', { nullable: true }) loginType: LoginType, - @Ctx() ctx: ApolloContext, - ): Promise { - if (typeof loginType === 'undefined') { - loginType = LoginType.Password; - } - switch (loginType) { - case LoginType.SignedMessage: - logger.debug('MESSAGE'); - loginType = LoginType.SignedMessage; - break; - case LoginType.Password: - loginType = LoginType.Password; - break; - default: - throw Error('Invalid login type'); - } - - const user: any = await User.createQueryBuilder('user') - .where('user.email = :email', { email }) - .andWhere('user.loginType = :loginType', { loginType: 'password' }) - .addSelect('user.password') - .getOne(); - - if (!user) { - logger.debug(`No user with email address ${email}`); - return null; - } - - const valid = await bcrypt.compare(password, user.password); - - if (!valid) { - // logger.debug('Invalid password') - - return null; - } - - // if (!user.confirmed) { - // logger.debug('not confirmed') - - // return null - // } - - // Not using sessions anymore - ctx.req.session!.userId = user.id - const accessToken = jwt.sign( - { userId: user.id, firstName: user.firstName }, - config.get('JWT_SECRET') as string, - { expiresIn: '30d' }, - ); - - const response = new LoginResponse(); - - delete user.password; - response.user = user; - response.token = accessToken; - return response; - } - - createToken(user: any) { - return jwt.sign(user, config.get('JWT_SECRET') as string, { - expiresIn: '30d', - }); - } - - @Mutation(() => LoginResponse, { nullable: true }) - async loginWallet( - @Arg('walletAddress') walletAddress: string, - @Arg('signature') signature: string, - @Arg('hostname') hostname: string, - @Arg('email', { nullable: true }) email: string, - @Arg('name', { nullable: true }) name: string, - @Arg('avatar', { nullable: true }) avatar: string, - @Arg('networkId') networkId: number, - @Ctx() ctx: ApolloContext, - ): Promise { - const hashedMsg = this.getHostnameSignMessageHash(hostname); - - const msgParams = JSON.stringify({ - primaryType: 'Login', - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'version', type: 'string' }, - // { name: 'verifyingContract', type: 'address' } - ], - Login: [{ name: 'user', type: 'User' }], - User: [{ name: 'wallets', type: 'address[]' }], - }, - domain: { - name: 'Giveth Login', - chainId: networkId, - version: '1', - }, - message: { - contents: hashedMsg, - user: { - wallets: [walletAddress], - }, - }, - }); - - if (hashedMsg === null) return null; - - const publicAddress = sigUtil.recoverTypedSignature_v4({ - data: JSON.parse(msgParams), - sig: signature, - }); - - if (!publicAddress) return null; - - const publicAddressLowerCase = publicAddress.toLocaleLowerCase(); - - if (walletAddress.toLocaleLowerCase() !== publicAddressLowerCase) - return null; - - let user = await findUserByWalletAddress(publicAddressLowerCase); - - try { - if (!user) { - user = await User.create({ - email, - name, - walletAddress: publicAddressLowerCase, - loginType: 'wallet', - avatar, - segmentIdentified: true, - }).save(); - logger.debug(`analytics.identifyUser -> New user`); - } else { - let modified = false; - const updateUserIfNeeded = (field, value) => { - // @ts-ignore - if (user[field] !== value) { - // @ts-ignore - user[field] = value; - modified = true; - } - }; - - if (name) updateUserIfNeeded('name', name); - - updateUserIfNeeded('avatar', avatar); - updateUserIfNeeded('walletAddress', publicAddressLowerCase); - if (user.segmentIdentified === false) { - logger.debug(`analytics.identifyUser -> User was already logged in`); - user.segmentIdentified = true; - modified = true; - } - if (modified) await user.save(); - } - - await getNotificationAdapter().updateOrttoUser({ - firstName: user.firstName, - lastName: user.lastName, - email: user.email, - userId: user.id.toString(), - }); - - const response = new LoginResponse(); - - response.token = this.createToken({ - userId: user.id, - firstName: user.name, - email: user.email, - walletAddress: publicAddressLowerCase, - }); - - response.user = user; - - return response; - } catch (e) { - logger.error(e); - return null; - } - } -} diff --git a/src/user/MeResolver.ts b/src/user/MeResolver.ts index b8129bdab..ab214002a 100644 --- a/src/user/MeResolver.ts +++ b/src/user/MeResolver.ts @@ -1,9 +1,9 @@ import { Resolver, Query, Ctx, Authorized } from 'type-graphql'; +import { Repository } from 'typeorm'; import { User } from '../entities/user'; import { Project } from '../entities/project'; import { ApolloContext } from '../types/ApolloContext'; -import { Repository, In } from 'typeorm'; import { getLoggedInUser } from '../services/authorizationServices'; import { AppDataSource } from '../orm'; diff --git a/src/user/register/RegisterInput.ts b/src/user/register/RegisterInput.ts deleted file mode 100644 index bd21f38e0..000000000 --- a/src/user/register/RegisterInput.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Length, IsEmail } from 'class-validator'; -import { Field, InputType } from 'type-graphql'; -import { IsEmailAlreadyExist } from './isEmailAlreadyExist'; -import { PasswordMixin } from '../../types/PasswordInput'; - -@InputType() -export class RegisterInput extends PasswordMixin(class {}) { - @Field() - @Length(1, 255) - firstName: string; - - @Field() - @Length(1, 255) - lastName: string; - - @Field() - @IsEmail() - @IsEmailAlreadyExist({ message: 'email already in use' }) - email: string; -} diff --git a/src/user/register/RegisterResolver.ts b/src/user/register/RegisterResolver.ts deleted file mode 100644 index 8de991c92..000000000 --- a/src/user/register/RegisterResolver.ts +++ /dev/null @@ -1,42 +0,0 @@ -// tslint:disable-next-line:no-var-requires -import { logger } from '../../utils/logger'; - -// tslint:disable-next-line:no-var-requires -const bcrypt = require('bcryptjs'); -import { Resolver, Query, Mutation, Arg, UseMiddleware } from 'type-graphql'; - -import { User } from '../../entities/user'; -import { RegisterWalletInput } from './RegisterWalletInput'; -import { RegisterInput } from './RegisterInput'; -// import { isAuth } from '../../middleware/isAuth' -// import { logger } from '../../middleware/logger' -import { sendEmail } from '../../utils/sendEmail'; -import { createConfirmationUrl } from '../../utils/createConfirmationUrl'; -import { Repository, getRepository } from 'typeorm'; - -@Resolver() -export class RegisterResolver { - @Mutation(() => User) - async register( - @Arg('data') - { email, firstName, lastName, password }: RegisterInput, - ): Promise { - logger.debug(`In Register Resolver : ${JSON.stringify(bcrypt, null, 2)}`); - - // const hashedPassword = await bcrypt.hash(password, 12) - const hashedPassword = bcrypt.hashSync(password, 12); - logger.debug(`hashedPassword ---> : ${hashedPassword}`); - const user = await User.create({ - firstName, - lastName, - email, - password: hashedPassword, - loginType: 'password', - }).save(); - - await sendEmail(email, await createConfirmationUrl(user.id)); - - delete user.password; - return user; - } -} diff --git a/src/user/register/RegisterWalletInput.ts b/src/user/register/RegisterWalletInput.ts deleted file mode 100644 index 7e19e8ee2..000000000 --- a/src/user/register/RegisterWalletInput.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Length, IsEmail } from 'class-validator'; -import { Field, InputType } from 'type-graphql'; -import { IsEmailAlreadyExist } from './isEmailAlreadyExist'; -import { PasswordMixin } from '../../types/PasswordInput'; - -@InputType() -export class RegisterWalletInput { - @Field() - @Length(1, 255) - firstName?: string; - - @Field() - @Length(1, 255) - lastName?: string; - - @Field() - @Length(1, 255) - name?: string; - - @Field() - @Length(1, 255) - walletAddress?: string; - - @Field() - @IsEmail() - @IsEmailAlreadyExist({ message: 'email already in use' }) - email: string; - - @Field() - organisationId?: number; -} diff --git a/src/user/register/isEmailAlreadyExist.ts b/src/user/register/isEmailAlreadyExist.ts deleted file mode 100644 index d962d2fec..000000000 --- a/src/user/register/isEmailAlreadyExist.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - registerDecorator, - ValidationOptions, - ValidatorConstraint, - ValidatorConstraintInterface, -} from 'class-validator'; - -import { User } from '../../entities/user'; -import { findAdminUserByEmail } from '../../repositories/userRepository'; - -@ValidatorConstraint({ async: true }) -export class IsEmailAlreadyExistConstraint - implements ValidatorConstraintInterface -{ - validate(email: string) { - return findAdminUserByEmail(email).then(user => { - if (user) return false; - return true; - }); - } -} - -export function IsEmailAlreadyExist(validationOptions?: ValidationOptions) { - return (object: Object, propertyName: string) => { - registerDecorator({ - target: object.constructor, - propertyName, - options: validationOptions, - constraints: [], - validator: IsEmailAlreadyExistConstraint, - }); - }; -} diff --git a/src/utils/createConfirmationUrl.ts b/src/utils/createConfirmationUrl.ts deleted file mode 100644 index a437106ea..000000000 --- a/src/utils/createConfirmationUrl.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { v4 } from 'uuid'; -import { redis } from '../redis'; -import { confirmUserPrefix } from '../constants/redisPrefixes'; -import config from '../config'; - -export const createConfirmationUrl = async (userId: number) => { - const token = v4(); - await redis.set(confirmUserPrefix + token, userId, 'EX', 60 * 60 * 24); // 1 day expiration - - return `http://${config.get('WEBSITE_URL')}/user/confirm/${token}`; -}; diff --git a/src/utils/documents.ts b/src/utils/documents.ts index 387c32399..9e3db531d 100644 --- a/src/utils/documents.ts +++ b/src/utils/documents.ts @@ -1,14 +1,14 @@ import HTMLToPDF from 'html-pdf-node'; import { pinFileDataBase64 } from '../middleware/pinataUtils'; -// tslint:disable-next-line:no-var-requires -const Handlebars = require('handlebars'); -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const path = require('path'); -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const fs = require('fs'); -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const util = require('util'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const Handlebars = require('handlebars'); const readFile = util.promisify(fs.readFile); @@ -24,7 +24,7 @@ export async function generateHTMLDocument(name: string, data: any) { export async function generatePDFDocument( name: string, data: any, -): Promise { +): Promise { const html = await generateHTMLDocument(name, data); const buf = await HTMLToPDF.generatePdf({ content: html }, { format: 'A4' }); @@ -36,6 +36,7 @@ export async function generatePDFDocument( export async function changeBase64ToIpfsImageInHTML( html: string, ): Promise { + // eslint-disable-next-line no-constant-condition while (true) { // Find image with base64 const regex = /', // sender address - to: email, // list of receivers - subject: 'Hello ✔', // Subject line - text: 'Hello world?', // plain text body - html: `${url}`, // html body - }; - - const info = await transporter.sendMail(mailOptions); - - logger.debug('Message sent: %s', info.messageId); - // Preview only available when sending through an Ethereal account - logger.debug('Preview URL: %s', nodemailer.getTestMessageUrl(info)); -} diff --git a/src/utils/stripe.ts b/src/utils/stripe.ts index 6d88141f6..8a6ed9e4c 100644 --- a/src/utils/stripe.ts +++ b/src/utils/stripe.ts @@ -34,11 +34,7 @@ export async function createStripeAccount(project: Project) { return account; } -export function createStripeAccountLink( - accountId: string, - refreshUrl: string, - returnUrl: string, -) { +export function createStripeAccountLink(accountId: string, refreshUrl: string) { return stripe.accountLinks.create({ account: accountId, type: 'account_onboarding', diff --git a/src/utils/tokenUtils.ts b/src/utils/tokenUtils.ts index 0aae18b0b..35e603290 100644 --- a/src/utils/tokenUtils.ts +++ b/src/utils/tokenUtils.ts @@ -1,10 +1,5 @@ -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from './errorMessages'; +import { i18n, translationErrorMessagesKeys } from './errorMessages'; import { Token } from '../entities/token'; -import { logger } from './logger'; export const findTokenByNetworkAndSymbol = async ( networkId: number, diff --git a/src/utils/utils.test.ts b/src/utils/utils.test.ts index 2ea812ea3..180e767a0 100644 --- a/src/utils/utils.test.ts +++ b/src/utils/utils.test.ts @@ -1,5 +1,6 @@ -import { getCreatedAtFromMongoObjectId, getHtmlTextSummary } from './utils'; +/* eslint-disable no-irregular-whitespace */ import { assert } from 'chai'; +import { getCreatedAtFromMongoObjectId, getHtmlTextSummary } from './utils'; import { SUMMARY_LENGTH } from '../constants/summary'; describe('getHtmlTextSummary test cases', getHtmlTextSummaryTestCases); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 3cc7aefc7..8cdc4a78e 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,12 +1,15 @@ -import { Country } from '../entities/Country'; -import { FilterField, SortingField } from '../entities/project'; import { convert } from 'html-to-text'; import slugify from 'slugify'; - import stringify from 'json-stable-stringify'; +import { isEqual } from 'lodash'; +import { Country } from '../entities/Country'; +import { FilterField, SortingField } from '../entities/project'; + import { SUMMARY_LENGTH } from '../constants/summary'; import config from '../config'; -// tslint:disable-next-line:no-var-requires +import { ProjectSocialMedia } from '../entities/projectSocialMedia'; +import { ProjectSocialMediaInput } from '../resolvers/types/ProjectVerificationUpdateInput'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const { createHash } = require('node:crypto'); export const sleep = ms => { @@ -28,7 +31,7 @@ export const generateProjectFiltersCacheKey = async (args: { }; export const titleWithoutSpecialCharacters = (title: string): string => { - const ALLOWED_SPECIAL_CHARACTERS_FOR_PROJECT_TITLE = [ + const UNALLOWED_SPECIAL_CHARACTERS_FOR_PROJECT_TITLE = [ '`', `'`, '<', @@ -51,9 +54,9 @@ export const titleWithoutSpecialCharacters = (title: string): string => { '`', ]; let cleanTitle = title; - ALLOWED_SPECIAL_CHARACTERS_FOR_PROJECT_TITLE.forEach( + UNALLOWED_SPECIAL_CHARACTERS_FOR_PROJECT_TITLE.forEach( character => - // this do like replaceAll + // this does like replaceAll (cleanTitle = cleanTitle.split(character).join('')), ); return cleanTitle; @@ -413,6 +416,10 @@ export const getHtmlTextSummary = ( export const isTestEnv = (config.get('ENVIRONMENT') as string) === 'test'; +export const isStaging = (config.get('ENVIRONMENT') as string) === 'staging'; +export const isProduction = + (config.get('ENVIRONMENT') as string) === 'production'; + export const dateToTimestampMs = (date: Date | string | number): number => { return new Date(date).valueOf(); }; @@ -438,3 +445,21 @@ export function getCurrentDateFormatted(): string { return `${year}${month}${day}`; } + +export const isSocialMediaEqual = ( + newSocialMedia?: ProjectSocialMedia[] | ProjectSocialMediaInput[], + oldSocialMedia?: ProjectSocialMedia[] | ProjectSocialMediaInput[], +) => { + return isEqual( + newSocialMedia + ?.map(s => { + return { type: s.type, link: s.link }; + }) + .sort(), + oldSocialMedia + ?.map(s => { + return { type: s.type, link: s.link }; + }) + .sort(), + ); +}; diff --git a/src/utils/validators/graphqlQueryValidators.ts b/src/utils/validators/graphqlQueryValidators.ts index 767e9ab9c..95da5af03 100644 --- a/src/utils/validators/graphqlQueryValidators.ts +++ b/src/utils/validators/graphqlQueryValidators.ts @@ -1,8 +1,4 @@ -import { CustomHelpers, number, ObjectSchema, ValidationResult } from 'joi'; -import { Connection, clusterApiUrl } from '@solana/web3.js'; - -// tslint:disable-next-line:no-var-requires -const Joi = require('joi'); +import { ObjectSchema, ValidationResult } from 'joi'; import { errorMessages, i18n, @@ -13,6 +9,8 @@ import { DONATION_STATUS } from '../../entities/donation'; import { PROJECT_VERIFICATION_STATUSES } from '../../entities/projectVerificationForm'; import { countriesList } from '../utils'; import { ChainType } from '../../types/network'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const Joi = require('joi'); const filterDateRegex = new RegExp('^[0-9]{8} [0-9]{2}:[0-9]{2}:[0-9]{2}$'); const resourcePerDateRegex = new RegExp( @@ -25,7 +23,7 @@ const solanaProgramIdRegex = /^(11111111111111111111111111111111|[1-9A-HJ-NP-Za-km-z]{43,44})$/; const txHashRegex = /^0x[a-fA-F0-9]{64}$/; const solanaTxRegex = /^[A-Za-z0-9]{86,88}$/; // TODO: Is this enough? We are using the signature to fetch transactions -const tokenSymbolRegex = /^[a-zA-Z0-9]{2,10}$/; // OPTIMISTIC OP token is 2 chars long +// const tokenSymbolRegex = /^[a-zA-Z0-9]{2,10}$/; // OPTIMISTIC OP token is 2 chars long // const tokenSymbolRegex = /^[a-zA-Z0-9]{2,10}$/; export const validateWithJoiSchema = (data: any, schema: ObjectSchema) => { @@ -41,8 +39,6 @@ const throwHttpErrorIfJoiValidatorFails = ( } }; -const projectIdValidator = Joi.number().integer().min(0).required(); - export const getDonationsQueryValidator = Joi.object({ fromDate: Joi.string() .pattern(filterDateRegex) @@ -151,6 +147,20 @@ export const createDraftDonationQueryValidator = Joi.object({ chainType: Joi.string().required(), }); +export const createDraftRecurringDonationQueryValidator = Joi.object({ + networkId: Joi.number() + .required() + .valid(...Object.values(NETWORK_IDS)), + currency: Joi.string().required(), + flowRate: Joi.string().required(), + projectId: Joi.number().integer().min(0).required(), + recurringDonationId: Joi.number().integer(), + anonymous: Joi.boolean(), + isBatch: Joi.boolean(), + isForUpdate: Joi.boolean(), + chainType: Joi.string().required(), +}); + export const updateDonationQueryValidator = Joi.object({ donationId: Joi.number().integer().min(0).required(), status: Joi.string().valid(DONATION_STATUS.VERIFIED, DONATION_STATUS.FAILED), @@ -175,7 +185,7 @@ const projectRegistryValidator = Joi.object({ organizationCountry: Joi.string().valid( // We allow country to be empty string '', - ...countriesList.map(({ name, code }) => name), + ...countriesList.map(({ name }) => name), ), organizationWebsite: Joi.string().allow(''), organizationDescription: Joi.string().allow(''), @@ -223,7 +233,7 @@ const managingFundsValidator = Joi.object({ NETWORK_IDS.ARBITRUM_MAINNET, NETWORK_IDS.ARBITRUM_SEPOLIA, NETWORK_IDS.OPTIMISTIC, - NETWORK_IDS.OPTIMISM_GOERLI, + NETWORK_IDS.OPTIMISM_SEPOLIA, NETWORK_IDS.XDAI, NETWORK_IDS.ETC, NETWORK_IDS.MORDOR_ETC_TESTNET, diff --git a/src/utils/validators/projectValidator.test.ts b/src/utils/validators/projectValidator.test.ts index af859414f..1b541ad27 100644 --- a/src/utils/validators/projectValidator.test.ts +++ b/src/utils/validators/projectValidator.test.ts @@ -1,10 +1,10 @@ +import { assert } from 'chai'; import { isWalletAddressSmartContract, isWalletAddressValid, validateProjectTitle, validateProjectWalletAddress, } from './projectValidator'; -import { assert } from 'chai'; import { assertThrowsAsync, createProjectData, @@ -37,7 +37,7 @@ describe( function validateProjectTitleTestCases() { it('should return an english message if title is invalid with including ()', async () => { try { - const valid = await validateProjectTitle('fdf()'); + await validateProjectTitle('fdf()'); } catch (e) { assert.equal( e.message, @@ -129,7 +129,7 @@ function validateProjectWalletAddressTestCases() { await assertThrowsAsync(async () => { await validateProjectWalletAddress(SEED_DATA.MALFORMED_ETHEREUM_ADDRESS); }, errorMessages.INVALID_WALLET_ADDRESS); - const project = await saveProjectDirectlyToDb(createProjectData()); + await saveProjectDirectlyToDb(createProjectData()); }); it('should throw exception when address is not valid - Solana', async () => { await assertThrowsAsync(async () => { diff --git a/src/utils/validators/projectValidator.ts b/src/utils/validators/projectValidator.ts index a9f57b185..9352b3027 100644 --- a/src/utils/validators/projectValidator.ts +++ b/src/utils/validators/projectValidator.ts @@ -6,7 +6,6 @@ import { findRelatedAddressByWalletAddress } from '../../repositories/projectAdd import { RelatedAddressInputType } from '../../resolvers/types/ProjectVerificationUpdateInput'; import { findProjectById } from '../../repositories/projectRepository'; import { titleWithoutSpecialCharacters } from '../utils'; -import { ethers } from 'ethers'; import { ChainType } from '../../types/network'; import { detectAddressChainType } from '../networks'; diff --git a/src/views/lastSnapshotProjectPowerView.ts b/src/views/lastSnapshotProjectPowerView.ts index f1f10c1d3..426bf2797 100644 --- a/src/views/lastSnapshotProjectPowerView.ts +++ b/src/views/lastSnapshotProjectPowerView.ts @@ -18,7 +18,7 @@ export class LastSnapshotProjectPowerView extends BaseEntity { projectId: number; @ViewColumn() - @Field(type => Float) + @Field(_type => Float) @Column('numeric', { scale: 2, transformer: new ColumnNumericTransformer(), @@ -27,11 +27,11 @@ export class LastSnapshotProjectPowerView extends BaseEntity { @ViewColumn() // Not sure why queries return type was string! - @Field(type => String) + @Field(_type => String) @Index() powerRank: string; @ViewColumn() - @Field(type => Int) + @Field(_type => Int) round: number; } diff --git a/src/views/projectFuturePowerView.ts b/src/views/projectFuturePowerView.ts index e88b6c382..ca613506e 100644 --- a/src/views/projectFuturePowerView.ts +++ b/src/views/projectFuturePowerView.ts @@ -7,8 +7,8 @@ import { BaseEntity, PrimaryColumn, } from 'typeorm'; -import { Project } from '../entities/project'; import { Field, Int, ObjectType } from 'type-graphql'; +import { Project } from '../entities/project'; @ViewEntity('project_future_power_view', { synchronize: false }) @ObjectType() @@ -26,16 +26,16 @@ export class ProjectFuturePowerView extends BaseEntity { @Field() totalPower: number; - @Field(type => Project) - @OneToOne(type => Project, project => project.projectFuturePower) + @Field(_type => Project) + @OneToOne(_type => Project, project => project.projectFuturePower) @JoinColumn({ referencedColumnName: 'id' }) project: Project; @ViewColumn() - @Field(type => Int) + @Field(_type => Int) powerRank: number; @ViewColumn() - @Field(type => Int) + @Field(_type => Int) round: number; } diff --git a/src/views/projectInstantPowerView.ts b/src/views/projectInstantPowerView.ts index 0150cc03a..7787ceff0 100644 --- a/src/views/projectInstantPowerView.ts +++ b/src/views/projectInstantPowerView.ts @@ -8,8 +8,8 @@ import { PrimaryColumn, Column, } from 'typeorm'; +import { Field, Float, ObjectType } from 'type-graphql'; import { Project } from '../entities/project'; -import { Field, Float, Int, ObjectType } from 'type-graphql'; import { ColumnNumericTransformer } from '../utils/entities'; @ViewEntity('project_instant_power_view', { synchronize: false }) @@ -24,15 +24,15 @@ export class ProjectInstantPowerView extends BaseEntity { projectId: number; @ViewColumn() - @Field(type => Float) + @Field(_type => Float) @Column('numeric', { scale: 2, transformer: new ColumnNumericTransformer(), }) totalPower: number; - @Field(type => Project) - @OneToOne(type => Project, project => project.projectPower) + @Field(_type => Project) + @OneToOne(_type => Project, project => project.projectPower) @JoinColumn({ referencedColumnName: 'id' }) project: Project; diff --git a/src/views/projectPowerView.ts b/src/views/projectPowerView.ts index bb6c5defd..6ab02b1f0 100644 --- a/src/views/projectPowerView.ts +++ b/src/views/projectPowerView.ts @@ -8,8 +8,8 @@ import { PrimaryColumn, Column, } from 'typeorm'; -import { Project } from '../entities/project'; import { Field, Float, Int, ObjectType } from 'type-graphql'; +import { Project } from '../entities/project'; import { ColumnNumericTransformer } from '../utils/entities'; @ViewEntity('project_power_view', { synchronize: false }) @@ -22,23 +22,23 @@ export class ProjectPowerView extends BaseEntity { projectId: number; @ViewColumn() - @Field(type => Float) + @Field(_type => Float) @Column('numeric', { scale: 2, transformer: new ColumnNumericTransformer(), }) totalPower: number; - @Field(type => Project) - @OneToOne(type => Project, project => project.projectPower) + @Field(_type => Project) + @OneToOne(_type => Project, project => project.projectPower) @JoinColumn({ referencedColumnName: 'id' }) project: Project; @ViewColumn() - @Field(type => Int) + @Field(_type => Int) powerRank: number; @ViewColumn() - @Field(type => Int) + @Field(_type => Int) round: number; } diff --git a/src/views/projectUserInstantPowerView.ts b/src/views/projectUserInstantPowerView.ts index c8f686cb6..0e1150fc8 100644 --- a/src/views/projectUserInstantPowerView.ts +++ b/src/views/projectUserInstantPowerView.ts @@ -6,7 +6,7 @@ import { Column, ManyToOne, } from 'typeorm'; -import { Field, Float, Int, ObjectType } from 'type-graphql'; +import { Field, Float, ObjectType } from 'type-graphql'; import { ColumnNumericTransformer } from '../utils/entities'; import { User } from '../entities/user'; @@ -22,8 +22,8 @@ export class ProjectUserInstantPowerView extends BaseEntity { @Field() projectId: number; - @Field(type => User) - @ManyToOne(type => User, { eager: true }) + @Field(_type => User) + @ManyToOne(_type => User, { eager: true }) user?: User; @ViewColumn() @@ -31,7 +31,7 @@ export class ProjectUserInstantPowerView extends BaseEntity { userId: number; @ViewColumn() - @Field(type => Float) + @Field(_type => Float) @Column('numeric', { scale: 2, transformer: new ColumnNumericTransformer(), diff --git a/src/views/userProjectPowerView.ts b/src/views/userProjectPowerView.ts index 1e262b9b5..c74348c30 100644 --- a/src/views/userProjectPowerView.ts +++ b/src/views/userProjectPowerView.ts @@ -21,7 +21,7 @@ export class UserProjectPowerView extends BaseEntity { // it's the powerBoostingId see the migration creation file to understand better id: number; - @Field(type => User, { nullable: true }) + @Field(_type => User, { nullable: true }) @JoinColumn({ referencedColumnName: 'id' }) @ManyToOne(() => User, { eager: true }) user?: User; diff --git a/src/workers/draftDonationMatchWorker.test.ts b/src/workers/draftDonationMatchWorker.test.ts index a4dbb9616..98911bd1a 100644 --- a/src/workers/draftDonationMatchWorker.test.ts +++ b/src/workers/draftDonationMatchWorker.test.ts @@ -2,7 +2,7 @@ import { DraftDonation, DRAFT_DONATION_STATUS, } from '../entities/draftDonation'; -import { Project } from '../entities/project'; +import { Project, ProjectUpdate } from '../entities/project'; import { NETWORK_IDS } from '../provider'; import { runDraftDonationMatchWorker } from '../services/chains/evm/draftDonationService'; import { @@ -48,6 +48,7 @@ describe('draftDonationMatchWorker', () => { }); if (projectAddress) { await ProjectAddress.delete({ address: RandomAddress2 }); + await ProjectUpdate.delete({ projectId: projectAddress.projectId }); await Project.delete(projectAddress.projectId); } diff --git a/src/workers/draftDonationMatchWorker.ts b/src/workers/draftDonationMatchWorker.ts index 974a95f52..6743f31f0 100644 --- a/src/workers/draftDonationMatchWorker.ts +++ b/src/workers/draftDonationMatchWorker.ts @@ -21,6 +21,7 @@ const worker: DraftDonationWorker = { // const dataSource = await AppDataSource.getDataSource(); try { let userIdSkip = 0; + // eslint-disable-next-line no-constant-condition while (true) { const userIds = await DraftDonation.createQueryBuilder('draftDonation') .select('DISTINCT(draftDonation.userId)', 'userId') @@ -39,6 +40,7 @@ const worker: DraftDonationWorker = { let draftDonationSkip = 0; logger.debug('match draft donation of user: ', userId); + // eslint-disable-next-line no-constant-condition while (true) { const draftDonations = await DraftDonation.find({ where: { diff --git a/src/workers/draftRecurringDonationMatchWorker.ts b/src/workers/draftRecurringDonationMatchWorker.ts new file mode 100644 index 000000000..1fe617137 --- /dev/null +++ b/src/workers/draftRecurringDonationMatchWorker.ts @@ -0,0 +1,48 @@ +import { expose } from 'threads/worker'; +import { WorkerModule } from 'threads/dist/types/worker'; +import { DRAFT_DONATION_STATUS } from '../entities/draftDonation'; +import { matchDraftRecurringDonations } from '../services/chains/evm/draftRecurringDonationService'; +import { logger } from '../utils/logger'; +import { AppDataSource } from '../orm'; +import { DraftRecurringDonation } from '../entities/draftRecurringDonation'; + +type DraftRecurringDonationWorkerFunctions = 'matchDraftRecurringDonations'; + +export type DrafRecurringtDonationWorker = + WorkerModule; + +const TAKE_DRAFT_RECURRING_DONATION = 1000; + +const worker: DrafRecurringtDonationWorker = { + async matchDraftRecurringDonations() { + await AppDataSource.initialize(false); + // const dataSource = await AppDataSource.getDataSource(); + try { + let draftDonationSkip = 0; + // eslint-disable-next-line no-constant-condition + while (true) { + const draftRecurringDonations = await DraftRecurringDonation.find({ + where: { + status: DRAFT_DONATION_STATUS.PENDING, + }, + order: { networkId: 'ASC' }, + take: TAKE_DRAFT_RECURRING_DONATION, + skip: draftDonationSkip, + }); + + if (draftRecurringDonations.length === 0) break; + + await matchDraftRecurringDonations(draftRecurringDonations); + if (draftRecurringDonations.length < TAKE_DRAFT_RECURRING_DONATION) { + break; + } else { + draftDonationSkip += draftRecurringDonations.length; + } + } + } catch (e) { + logger.error('Error in matchDraftRecurringDonations worker', e); + } + }, +}; + +expose(worker); diff --git a/src/workers/projectsResolverWorker.ts b/src/workers/projectsResolverWorker.ts index 23da983c5..632738c14 100644 --- a/src/workers/projectsResolverWorker.ts +++ b/src/workers/projectsResolverWorker.ts @@ -1,9 +1,9 @@ // workers/auth.js import { expose } from 'threads/worker'; +import { WorkerModule } from 'threads/dist/types/worker'; import { FilterField, Project, SortingField } from '../entities/project'; import { generateProjectFiltersCacheKey } from '../utils/utils'; import { Reaction } from '../entities/reaction'; -import { WorkerModule } from 'threads/dist/types/worker'; type ProjectsResolverWorkerFunctions = | 'hashProjectFilters' diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index db739af79..fe3dac5c0 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -54,6 +54,30 @@ export const createDraftDonationMutation = ` } `; +export const createDraftRecurringDonationMutation = ` + mutation ( + $networkId: Float! + $currency: String! + $projectId: Float! + $recurringDonationId: Float + $anonymous: Boolean + $isBatch: Boolean + $isForUpdate: Boolean + $flowRate: String! + ) { + createDraftRecurringDonation( + networkId: $networkId + currency: $currency + recurringDonationId: $recurringDonationId + projectId: $projectId + anonymous: $anonymous + isBatch: $isBatch + isForUpdate: $isForUpdate + flowRate: $flowRate + ) + } +`; + export const updateDonationStatusMutation = ` mutation ( $status: String @@ -321,6 +345,7 @@ export const fetchDonationsByProjectIdQuery = ` } totalCount totalUsdBalance + recurringDonationsCount } } `; @@ -331,10 +356,9 @@ export const fetchRecurringDonationsByProjectIdQuery = ` $projectId: Int! $searchTerm: String $status: String - $finished: Boolean + $includeArchived: Boolean + $finishStatus: FinishStatus $orderBy: RecurringDonationSortBy - - ) { recurringDonationsByProjectId( take: $take @@ -342,7 +366,8 @@ export const fetchRecurringDonationsByProjectIdQuery = ` projectId: $projectId searchTerm: $searchTerm status: $status - finished: $finished + includeArchived: $includeArchived + finishStatus: $finishStatus orderBy: $orderBy ) { @@ -350,9 +375,10 @@ export const fetchRecurringDonationsByProjectIdQuery = ` id txHash networkId - amount + flowRate currency anonymous + isArchived status donor { id @@ -372,9 +398,11 @@ export const fetchRecurringDonationsByUserIdQuery = ` $take: Int $skip: Int $status: String + $includeArchived: Boolean $orderBy: RecurringDonationSortBy - $finished: Boolean + $finishStatus: FinishStatus $userId: Int! + $filteredTokens: [String!] ) { recurringDonationsByUserId( take: $take @@ -382,22 +410,35 @@ export const fetchRecurringDonationsByUserIdQuery = ` orderBy: $orderBy userId: $userId status: $status - finished: $finished + includeArchived: $includeArchived + finishStatus: $finishStatus + filteredTokens: $filteredTokens ) { recurringDonations { id txHash networkId - amount + flowRate currency anonymous status + isArchived donor { id walletAddress firstName email } + project { + id + title + slug + anchorContracts { + id + address + isActive + } + } createdAt } totalCount @@ -484,11 +525,13 @@ export const fetchTotalDonationsPerCategoryPerDate = ` $fromDate: String $toDate: String $fromOptimismOnly: Boolean + $onlyVerified: Boolean ) { totalDonationsPerCategory( fromDate: $fromDate toDate: $toDate fromOptimismOnly: $fromOptimismOnly + onlyVerified: $onlyVerified ) { id title @@ -544,11 +587,13 @@ export const fetchTotalDonationsUsdAmount = ` $fromDate: String $toDate: String $fromOptimismOnly: Boolean + $onlyVerified: Boolean ) { donationsTotalUsdPerDate ( fromDate: $fromDate toDate: $toDate fromOptimismOnly: $fromOptimismOnly + onlyVerified: $onlyVerified ) { total totalPerMonthAndYear { @@ -564,11 +609,13 @@ export const fetchTotalDonationsNumberPerDateRange = ` $fromDate: String $toDate: String $fromOptimismOnly: Boolean + $onlyVerified: Boolean ) { totalDonationsNumberPerDate ( fromDate: $fromDate toDate: $toDate fromOptimismOnly: $fromOptimismOnly + onlyVerified: $onlyVerified ) { total totalPerMonthAndYear { @@ -579,6 +626,34 @@ export const fetchTotalDonationsNumberPerDateRange = ` } `; +export const fetchNewDonorsCount = ` + query ( + $fromDate: String! + $toDate: String! + ) { + newDonorsCountPerDate( + fromDate: $fromDate + toDate: $toDate + ) { + total + } + } +`; + +export const fetchNewDonorsDonationTotalUsd = ` + query ( + $fromDate: String! + $toDate: String! + ) { + newDonorsDonationTotalUsdPerDate( + fromDate: $fromDate + toDate: $toDate + ) { + total + } + } +`; + export const fetchAllDonationsQuery = ` query ( $fromDate: String @@ -1271,6 +1346,7 @@ export const userByAddress = ` boostedProjectsCount likedProjectsCount donationsCount + totalDonated projectsCount passportScore passportStamps @@ -2229,20 +2305,93 @@ export const createRecurringDonationQuery = ` mutation ($projectId: Int!, $networkId: Int!, $txHash: String! - $interval: String! - $amount: Int! + $flowRate: String! $currency: String! + $anonymous: Boolean + $isBatch: Boolean ) { createRecurringDonation( projectId: $projectId networkId: $networkId txHash:$txHash - amount:$amount + flowRate: $flowRate currency:$currency - interval:$interval + anonymous:$anonymous + isBatch:$isBatch ) { txHash networkId + anonymous + isArchived + isBatch } } `; + +export const updateRecurringDonationQueryById = ` + mutation ( + $recurringDonationId: Int!, + $projectId: Int!, + $networkId: Int!, + $currency: String!, + $txHash: String + $flowRate: String + $anonymous: Boolean + $isArchived: Boolean + $status: String + ) { + updateRecurringDonationParamsById( + recurringDonationId: $recurringDonationId + projectId: $projectId + networkId: $networkId + currency:$currency + txHash:$txHash + anonymous:$anonymous + flowRate:$flowRate + status:$status + isArchived:$isArchived + ) { + txHash + networkId + currency + flowRate + anonymous + status + isArchived + finished + } + } +`; + +export const updateRecurringDonationQuery = ` + mutation ( + $projectId: Int!, + $networkId: Int!, + $currency: String!, + $txHash: String + $flowRate: String + $anonymous: Boolean + $isArchived: Boolean + $status: String + ) { + updateRecurringDonationParams( + projectId: $projectId + networkId: $networkId + currency:$currency + txHash:$txHash + anonymous:$anonymous + flowRate:$flowRate + status:$status + isArchived:$isArchived + ) { + txHash + networkId + currency + flowRate + anonymous + status + isArchived + finished + } + } +`; diff --git a/test/pre-test-scripts.ts b/test/pre-test-scripts.ts index 601e26a2c..2c3196381 100644 --- a/test/pre-test-scripts.ts +++ b/test/pre-test-scripts.ts @@ -198,10 +198,10 @@ async function seedTokens() { } await Token.create(tokenData as Token).save(); } - for (const token of SEED_DATA.TOKENS.optimism_goerli) { + for (const token of SEED_DATA.TOKENS.optimism_sepolia) { const tokenData = { ...token, - networkId: NETWORK_IDS.OPTIMISM_GOERLI, + networkId: NETWORK_IDS.OPTIMISM_SEPOLIA, isGivbackEligible: true, }; if (token.symbol === 'OP') { @@ -335,6 +335,7 @@ async function seedProjects() { await saveProjectDirectlyToDb(SEED_DATA.FOURTH_PROJECT); await saveProjectDirectlyToDb(SEED_DATA.FIFTH_PROJECT); await saveProjectDirectlyToDb(SEED_DATA.SIXTH_PROJECT); + await saveProjectDirectlyToDb(SEED_DATA.NON_VERIFIED_PROJECT); } async function seedProjectUpdates() { @@ -478,8 +479,6 @@ async function runMigrations() { ); await new createDonationethUser1701756190381().up(queryRunner); await new projectActualMatchingV14_1713545913826().up(queryRunner); - } catch (e) { - throw e; } finally { await queryRunner.release(); } diff --git a/test/testUtils.ts b/test/testUtils.ts index cd1fdd9a4..58ba6b75e 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,5 +1,6 @@ import { assert } from 'chai'; import * as jwt from 'jsonwebtoken'; +import { Keypair } from '@solana/web3.js'; import config from '../src/config'; import { NETWORK_IDS } from '../src/provider'; import { User } from '../src/entities/user'; @@ -32,12 +33,11 @@ import { MainCategory } from '../src/entities/mainCategory'; import { Category, CATEGORY_NAMES } from '../src/entities/category'; import { FeaturedUpdate } from '../src/entities/featuredUpdate'; import { ChainType } from '../src/types/network'; -import { Keypair } from '@solana/web3.js'; import { RecurringDonation } from '../src/entities/recurringDonation'; import { AnchorContractAddress } from '../src/entities/anchorContractAddress'; import { findProjectById } from '../src/repositories/projectRepository'; -// tslint:disable-next-line:no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires const moment = require('moment'); export const graphqlUrl = 'http://localhost:4000/graphql'; @@ -178,7 +178,7 @@ export const saveAnchorContractDirectlyToDb = async (params: { projectId: params.projectId, creatorId: params.creatorId, address: params.contractAddress || generateRandomEtheriumAddress(), - networkId: params.networkId || NETWORK_IDS.OPTIMISM_GOERLI, + networkId: params.networkId || NETWORK_IDS.OPTIMISM_SEPOLIA, txHash: params.txHash || generateRandomEtheriumAddress(), ownerId: projectOwnerId, }).save(); @@ -293,8 +293,8 @@ export const saveProjectDirectlyToDb = async ( "userId","projectId",content,title,"createdAt","isMain" ) VALUES ( ${user.id}, ${project.id}, '', '', '${ - projectUpdateCreatedAt.toISOString().split('T')[0] - }', true + projectUpdateCreatedAt.toISOString().split('T')[0] + }', true )`); return project; }; @@ -435,11 +435,20 @@ export const SEED_DATA = { SIXTH_PROJECT: { ...createProjectData(), title: 'fifth project', - slug: 'fifth-project', + slug: 'sixth-project', description: 'forth description', id: 6, admin: '1', }, + NON_VERIFIED_PROJECT: { + ...createProjectData(), + title: 'non verified project', + slug: 'non-verified-project', + description: 'non verified description', + id: 7, + admin: '1', + verified: false, + }, MAIN_CATEGORIES: ['drink', 'food', 'nonProfit'], NON_PROFIT_SUB_CATEGORIES: [CATEGORY_NAMES.registeredNonProfits], FOOD_SUB_CATEGORIES: [ @@ -1397,19 +1406,13 @@ export const SEED_DATA = { decimals: 18, }, ], - optimism_goerli: [ + optimism_sepolia: [ { name: 'OPTIMISM native token', symbol: 'ETH', address: '0x0000000000000000000000000000000000000000', decimals: 18, }, - { - name: 'OPTIMISM OP token', - symbol: 'OP', - address: '0x4200000000000000000000000000000000000042', - decimals: 18, - }, ], solana_mainnet: [ { @@ -1923,6 +1926,7 @@ export const saveRecurringDonationDirectlyToDb = async (params?: { const donorId = params?.donationData?.donorId || (await saveUserDirectlyToDb(generateRandomEtheriumAddress())).id; + const anonymous = params?.donationData?.anonymous || false; const anchorContractAddressId = params?.donationData?.anchorContractAddressId || ( @@ -1932,12 +1936,25 @@ export const saveRecurringDonationDirectlyToDb = async (params?: { }) ).id; return RecurringDonation.create({ - amount: params?.donationData?.amount || 10, + flowRate: params?.donationData?.flowRate || '10', + totalUsdStreamed: params?.donationData?.totalUsdStreamed || 0, status: params?.donationData?.status || 'pending', - networkId: params?.donationData?.networkId || NETWORK_IDS.OPTIMISM_GOERLI, + networkId: params?.donationData?.networkId || NETWORK_IDS.OPTIMISM_SEPOLIA, currency: params?.donationData?.currency || 'USDT', - interval: params?.donationData?.interval || 'monthly', + finished: + params?.donationData?.finished !== undefined + ? params?.donationData?.finished + : false, + isArchived: + params?.donationData?.isArchived !== undefined + ? params?.donationData?.isArchived + : false, + isBatch: + params?.donationData?.isBatch !== undefined + ? params?.donationData?.isBatch + : false, txHash: params?.donationData?.txHash || generateRandomEtheriumAddress(), + anonymous, donorId, projectId, anchorContractAddressId, diff --git a/tslint.json b/tslint.json deleted file mode 100644 index c751c2a37..000000000 --- a/tslint.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "defaultSeverity": "error", - "extends": [ - "tslint:recommended", - "tslint-plugin-prettier", - "tslint-config-prettier" - ], - "rules": { - "prettier": [ - true, - { - "arrowParens": "avoid", - "singleQuote": true, - "semi": true, - "tabWidth": 2, - "useTabs": false, - "trailingComma": "all", - "jsdoc-format": false, - "endOfLine": "auto" - } - ], - "jsdoc-format": false, - "adjacent-overload-signatures": true, - "quotemark": { - "options": "single" - }, - "ban-types": false, - "no-console": true, - "no-namespace": false, - "object-literal-sort-keys": false, - "max-classes-per-file": false, - "member-access": [ - true, - "no-public" - ], - "member-ordering": [ - true, - { - "order": [ - "public-static-field", - "protected-static-field", - "private-static-field", - "public-instance-field", - "protected-instance-field", - "private-instance-field", - "public-static-method", - "protected-static-method", - "private-static-method", - "public-constructor", - "protected-constructor", - "private-constructor", - "public-instance-method", - "protected-instance-method", - "private-instance-method" - ] - } - ], - "unified-signatures": false, - "ordered-imports": false, - "no-unused-expression": false, - "callable-types": false, - "variable-name": [ - true, - "ban-keywords", - "check-format", - "allow-leading-underscore", - "allow-pascal-case" - ], - "whitespace": [ - true, - "check-branch", - "check-module" - ], - "indent": [ - true, - "spaces", - 2 - ] - } -}