From b9e93096da13cfd593aff67f3eef0467faf8fd3a Mon Sep 17 00:00:00 2001 From: danielailie Date: Tue, 27 Jun 2023 11:45:12 +0300 Subject: [PATCH] Development to main (#984) * SERVICES1086 rebuild marketplace data/state endpoint, service and functions (part 1) * SERVICES-1085 extract confusing logic in a separete function; renaming; remove comment * SERVICES-1085 code splitting, renamings & improvements * SERVICES-1085 rename param * SERVICES-1085 add marketplace reindex process event logic * SERVICES-1085 fix batch slice function; code improvements for get event category function * SERVICES-1085 remove console log * SERVICES-1085 extract total bought tokens in a separate function * SERVICES-1085 split reindex marketplace data code * SERVICES-1085 use handlers and summary classes for rebuilding marketplace state * SERVICES-1085 fix marketplace elastic query time range filter * SERVICES-1085 create MarketplaceReindexState class with common methods and remove common handlers * SERVICES-1085 add handlers for XOXNO specfic events * set default paymentNonce if not already set * SERVICES-1085 ignore not used marketplace events for better performance\ * undo default paymentNonce * SERVICES-1085 marketplace events summaries * SERVICES-1085 update marketplace event summaries and add DeadRare updatePrice topics * SERVICES-1085 marketplace events summary service * SERVICES-1085 fix marketplace reindex summaries and handle krogan acceptOffer * SERVICES-1085 marketplace reindex state handlers * SERVICES-1085 remove confusing method * SERVICES-1085 remove confusing method * SERVICES-1085 extract create new order from event summary logic into a separate method * SERVICES-1085 improve createOrder from summary function * SERVICES-1085 renamings * SERVICES-1085 renamings * SERVICES-1085 update marketplace reindex state service * SERVICES-1085 process event even if no payment token found * SERVICES-1085 xoxno user deposit * SERVICES-1085 fix elrondnftswap offerId type * SERVICES-1085 reindex marketplace state before & after time range * SERVICES-1085 reindex marketplace state logs & fixes * SERVICES-1085 renaming * SERVICES-1085 fix duplicate bid for internal marketplace * SERVICES-1085 try-catch and better logging * SERVICES-1085 fix xoxno no startTime * SERVICES-1085 fix xoxno no startTime * SERVICES-1085 fix endAuction with no winning order * SERVICES-1085 fix case when undefined beforeTimestamp * SERVICES-1085 save auction tags * SERVICES-1085 admin endpoints fire and forget * SERVICES-1085 fix internal auction timestamp topic * SERVICES-1085 marketplace reindex events locker & logs * SERVICES-1085 handle multiple internal marketplaces with same address case * fix admin resolver catch * SERVICES-1085 exclude collection filtering when unique marketplace address * SERVICES-1085 fix case when timestamp not spported by DB * Add plugins module * Update scam cronjob * Update scam service * Add none scam info * Update workflow * Update cronjob time * Remode delete by pattern where not needed * Fix scam report * Remove unused code * trigger cache invalidation * Change delete to update * Remove delete by pattern for asset history * Remove delete by pattern from cache invalidation * Clear to none nft scam info * Update scam update cache invalidation * Add null check * Add collections with no volum * Fix trending order by * Fix trending return * Fix order trending * Move blacklist filter before order * Update filter collection * Remove clear by pattern in auctions * Add Scam Collection setting * Add cache invalidation * Remove scam info from api * clean up code * Upgrade testing packages * Remove data api calls * Update mapping * Refactor Token model * Add historical price retrieve * Add elrond tools back * Add create nft with multiple files * Extract duplicated code * Fix typo * Decrease caching time * Decrease caching time * Add ici handling * Update key * Update key * Fix Scam Mapping * Fix bug for event processing * Add tickets type on collections * Add page consts and remove extra pagination * Remove extra line * Add isTicket field on asset * Update redis handler for is ticket * Add debuging logging * Fix tickets assets return * Remove logging * Update length check * Update collection length checking * Fix undefined error * Allow nullable for role address --------- Co-authored-by: johnykes Co-authored-by: johnykes <48965850+johnykes@users.noreply.github.com> --- .github/workflows/node.js.yml | 3 +- .gitignore | 1 + package-lock.json | 998 +++++++----------- package.json | 10 +- schema.gql | 21 +- src/common/persistence/persistence.service.ts | 68 +- src/common/pluggins/plugin.service.ts | 7 + .../services/caching/entities/cache.info.ts | 12 +- src/common/services/mx-communication/index.ts | 1 - .../mx-communication/models/account.info.ts | 19 + .../models/api-token.model.ts | 23 + .../mx-communication/models/nft.dto.ts | 1 + .../mx-communication/models/scam-info.dto.ts | 1 + .../mx-communication/mx-api.service.ts | 50 +- .../mx-communication.module.ts | 9 +- .../mx-communication/mx-data.service.ts | 89 ++ .../mx-communication/mx-extras-api.service.ts | 90 -- .../mx-communication/mx-tools.service.ts | 106 -- src/config/default.json | 15 +- src/db/auctions/auctions.repository.ts | 41 +- src/db/auctions/tags.repository.ts | 3 +- src/db/featuredNfts/featured.repository.ts | 15 +- .../marketplaces/marketplace-events.entity.ts | 24 + src/db/offers/offers.repository.ts | 22 + src/db/orders/orders.repository.ts | 7 + src/document-db/document-db.module.ts | 12 + src/document-db/document-db.service.ts | 42 +- .../collection-scam.repository.ts | 56 + .../repositories/nft-scam.repository.ts | 6 +- .../admins/admin-operations.resolver.ts | 34 +- .../assets-history-caching.service.ts | 12 - src/modules/assets/assets-getter.service.ts | 25 +- .../assets/assets-mutations.resolver.ts | 15 + src/modules/assets/assets-queries.resolver.ts | 13 +- src/modules/assets/assets-query.ts | 7 - .../assets/assets-transaction.service.ts | 115 +- src/modules/assets/assets.module.ts | 10 +- .../assets/loaders/asset-is-ticket.loader.ts | 27 + .../loaders/asset-is-ticket.redis-handler.ts | 33 + src/modules/assets/models/Asset.dto.ts | 50 +- src/modules/assets/models/AssetAction.enum.ts | 3 + src/modules/assets/models/AssetOfferEnum.ts | 12 + src/modules/assets/models/NftTypes.enum.ts | 10 + src/modules/assets/models/Price.dto.ts | 2 +- src/modules/assets/models/ScamInfo.dto.ts | 25 +- .../models/requests/CreateNftRequest.ts | 24 + .../auctions/auctions-getter.service.ts | 2 +- .../auctions/auctions-queries.resolver.ts | 2 +- .../auctions/auctions-setter.service.ts | 4 + .../caching/auctions-caching.service.ts | 10 +- .../common/api-config/api.config.service.ts | 22 + src/modules/common/filters/filtersTypes.ts | 13 +- .../featured/FeatureCollectionType.enum.ts | 1 + .../featured/featured-caching.service.ts | 10 +- .../featured/featured-collections.resolver.ts | 2 +- src/modules/featured/featured.module.ts | 3 +- src/modules/featured/featured.service.ts | 7 +- .../marketplaces-events-indexing.service.ts | 95 +- ...etplaces-reindex-events-summary.service.ts | 182 ++++ .../reindex-auction-bid.handler.ts | 55 + .../reindex-auction-bought.handler.ts | 81 ++ .../reindex-auction-closed.handler.ts | 38 + .../reindex-auction-ended.handler.ts | 57 + .../reindex-auction-price-updated.handler.ts | 53 + .../reindex-auction-started.handler.ts | 64 ++ .../reindex-auction-updated.handler.ts | 49 + .../reindex-global-offer-accepted.handler.ts | 28 + .../reindex-offer-accepted.handler.ts | 40 + .../reindex-offer-closed.handler.ts | 25 + .../reindex-offer-created.hander.ts | 42 + .../marketplaces-reindex.service.ts | 657 ++++++++++++ .../marketplaces.elastic.queries.ts | 1 + .../marketplaces/marketplaces.module.ts | 34 +- .../marketplaces/marketplaces.resolver.ts | 2 +- .../marketplaces/marketplaces.service.ts | 22 +- .../marketplaces/models/Marketplace.dto.ts | 5 +- .../models/MarketplaceEventLogInput.ts | 63 ++ .../models/MarketplaceReindexDataArgs.ts | 13 + .../models/MarketplaceReindexState.ts | 168 +++ .../AuctionBidSummary.ts | 69 ++ .../AuctionBuySummary.ts | 94 ++ .../AuctionClosedSummary.ts | 72 ++ .../AuctionEndedSummary.ts | 53 + .../AuctionPriceUpdated.ts | 70 ++ .../AuctionStartedSummary.ts | 115 ++ .../AuctionUpdatedSummary.ts | 71 ++ .../GloballyOfferAcceptedSummary.ts | 35 + .../OfferAcceptedSummary.ts | 102 ++ .../OfferClosedSummary.ts | 42 + .../OfferCreatedSummary.ts | 65 ++ .../ReindexGenericSummary.ts | 16 + .../base-collection-assets.redis-handler.ts | 4 - .../collection-assets-model.resolver.ts | 5 +- .../collections-getter.service.ts | 36 +- .../nftCollections/models/Collection.dto.ts | 2 +- .../blockchain-events/feed-events.service.ts | 30 +- .../handlers/updateListing-event.handler.ts | 2 +- .../marketplace-events.service.ts | 8 +- .../blockchain-events/nft-events.module.ts | 3 +- .../cache-events.consumer.ts | 9 +- .../cache-invalidation-events.service.ts | 11 - .../events/changed.event.ts | 1 + .../elastic-updates-events.service.ts | 9 +- .../acceptOfferFrameit.event.topics.ts | 6 +- .../auction/auctionToken.event.topics.ts | 44 +- .../elrondswap-acceptOffer.event.topics.ts | 63 ++ .../elrondswap-acceptOffer.event.ts | 15 + .../elrondswap-updateAuction.event.topics.ts | 34 +- .../updatePriceDeadrare.event.topics.ts | 38 + .../auction/updatePriceDeadrare.event.ts | 15 + .../rabbitmq/entities/generic.event.ts | 11 + src/modules/scam/collection-scam.service.ts | 46 +- .../scam/models/collection-scam-info.model.ts | 29 + .../scam/models/nft-scam-data.model.ts | 8 +- src/modules/scam/nft-scam.elastic.service.ts | 47 +- src/modules/scam/nft-scam.queries.ts | 4 +- src/modules/scam/nft-scam.resolver.ts | 4 +- src/modules/scam/nft-scam.service.ts | 433 ++++---- src/modules/scam/scam.module.ts | 2 + .../usdPrice}/Token.model.ts | 9 +- src/modules/usdPrice/usd-price.resolver.ts | 2 +- src/modules/usdPrice/usd-price.service.ts | 155 ++- .../usdPrice/usd-token-price.resolver.ts | 2 +- .../plugins.module.ts.template | 8 + src/utils/constants/index.ts | 1 + src/utils/date-utils.ts | 4 + src/utils/dynamic.module.utils.ts | 31 + 127 files changed, 4506 insertions(+), 1448 deletions(-) create mode 100644 src/common/pluggins/plugin.service.ts create mode 100644 src/common/services/mx-communication/models/account.info.ts create mode 100644 src/common/services/mx-communication/models/api-token.model.ts create mode 100644 src/common/services/mx-communication/mx-data.service.ts delete mode 100644 src/common/services/mx-communication/mx-extras-api.service.ts delete mode 100644 src/common/services/mx-communication/mx-tools.service.ts create mode 100644 src/document-db/repositories/collection-scam.repository.ts create mode 100644 src/modules/assets/loaders/asset-is-ticket.loader.ts create mode 100644 src/modules/assets/loaders/asset-is-ticket.redis-handler.ts create mode 100644 src/modules/assets/models/AssetOfferEnum.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex-events-summary.service.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-bid.handler.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-bought.handler.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-closed.handler.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-ended.handler.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-price-updated.handler.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-started.handler.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-updated.handler.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex-handlers/reindex-global-offer-accepted.handler.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex-handlers/reindex-offer-accepted.handler.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex-handlers/reindex-offer-closed.handler.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex-handlers/reindex-offer-created.hander.ts create mode 100644 src/modules/marketplaces/marketplaces-reindex.service.ts create mode 100644 src/modules/marketplaces/models/MarketplaceEventLogInput.ts create mode 100644 src/modules/marketplaces/models/MarketplaceReindexDataArgs.ts create mode 100644 src/modules/marketplaces/models/MarketplaceReindexState.ts create mode 100644 src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionBidSummary.ts create mode 100644 src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionBuySummary.ts create mode 100644 src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionClosedSummary.ts create mode 100644 src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionEndedSummary.ts create mode 100644 src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionPriceUpdated.ts create mode 100644 src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionStartedSummary.ts create mode 100644 src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionUpdatedSummary.ts create mode 100644 src/modules/marketplaces/models/marketplaces-reindex-events-summaries/GloballyOfferAcceptedSummary.ts create mode 100644 src/modules/marketplaces/models/marketplaces-reindex-events-summaries/OfferAcceptedSummary.ts create mode 100644 src/modules/marketplaces/models/marketplaces-reindex-events-summaries/OfferClosedSummary.ts create mode 100644 src/modules/marketplaces/models/marketplaces-reindex-events-summaries/OfferCreatedSummary.ts create mode 100644 src/modules/marketplaces/models/marketplaces-reindex-events-summaries/ReindexGenericSummary.ts create mode 100644 src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-acceptOffer.event.topics.ts create mode 100644 src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-acceptOffer.event.ts create mode 100644 src/modules/rabbitmq/entities/auction/updatePriceDeadrare.event.topics.ts create mode 100644 src/modules/rabbitmq/entities/auction/updatePriceDeadrare.event.ts create mode 100644 src/modules/scam/models/collection-scam-info.model.ts rename src/{common/services/mx-communication/models => modules/usdPrice}/Token.model.ts (76%) create mode 100644 src/plugins.template/plugins.module.ts.template diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index ab2856e9d..c74ee5a20 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -26,6 +26,5 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'npm' - - run: npm install - - run: npm run build --if-present + - run: npm ci - run: npm test diff --git a/.gitignore b/.gitignore index 54036a75b..c91f73360 100644 --- a/.gitignore +++ b/.gitignore @@ -400,3 +400,4 @@ build.yaml # PEM file(s) *.pem +/src/plugins diff --git a/package-lock.json b/package-lock.json index 1f4eb34ad..152043dce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,11 +72,11 @@ "devDependencies": { "@nestjs/cli": "9.2.0", "@nestjs/schematics": "9.0.4", - "@nestjs/testing": "9.1.4", + "@nestjs/testing": "9.3.12", "@types/express": "^4.17.11", "@types/graphql-relay": "^0.6.0", - "@types/jest": "^27.4.1", - "@types/node": "^14.14.36", + "@types/jest": "29.5.0", + "@types/node": "^14.14.42", "@types/passport-local": "^1.0.33", "@types/supertest": "^2.0.12", "@types/tiny-async-pool": "^1.0.0", @@ -85,7 +85,7 @@ "eslint": "8.28.0", "eslint-config-prettier": "8.5.0", "eslint-plugin-prettier": "3.4.1", - "jest": "29.4.2", + "jest": "29.5.0", "prettier": "^2.2.1", "supertest": "6.3.3", "ts-jest": "29.0.5", @@ -2796,16 +2796,16 @@ } }, "node_modules/@jest/console": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.4.2.tgz", - "integrity": "sha512-0I/rEJwMpV9iwi9cDEnT71a5nNGK9lj8Z4+1pRAU2x/thVXCDnaTGrvxyK+cAqZTFVFCiR+hfVrP4l2m+dCmQg==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", + "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", "dev": true, "dependencies": { - "@jest/types": "^29.4.2", + "@jest/types": "^29.5.0", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.4.2", - "jest-util": "^29.4.2", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", "slash": "^3.0.0" }, "engines": { @@ -2813,37 +2813,37 @@ } }, "node_modules/@jest/core": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.4.2.tgz", - "integrity": "sha512-KGuoQah0P3vGNlaS/l9/wQENZGNKGoWb+OPxh3gz+YzG7/XExvYu34MzikRndQCdM2S0tzExN4+FL37i6gZmCQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", + "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", "dev": true, "dependencies": { - "@jest/console": "^29.4.2", - "@jest/reporters": "^29.4.2", - "@jest/test-result": "^29.4.2", - "@jest/transform": "^29.4.2", - "@jest/types": "^29.4.2", + "@jest/console": "^29.5.0", + "@jest/reporters": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.4.2", - "jest-config": "^29.4.2", - "jest-haste-map": "^29.4.2", - "jest-message-util": "^29.4.2", - "jest-regex-util": "^29.4.2", - "jest-resolve": "^29.4.2", - "jest-resolve-dependencies": "^29.4.2", - "jest-runner": "^29.4.2", - "jest-runtime": "^29.4.2", - "jest-snapshot": "^29.4.2", - "jest-util": "^29.4.2", - "jest-validate": "^29.4.2", - "jest-watcher": "^29.4.2", + "jest-changed-files": "^29.5.0", + "jest-config": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-resolve-dependencies": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "jest-watcher": "^29.5.0", "micromatch": "^4.0.4", - "pretty-format": "^29.4.2", + "pretty-format": "^29.5.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, @@ -2860,97 +2860,88 @@ } }, "node_modules/@jest/environment": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.4.2.tgz", - "integrity": "sha512-JKs3VUtse0vQfCaFGJRX1bir9yBdtasxziSyu+pIiEllAQOe4oQhdCYIf3+Lx+nGglFktSKToBnRJfD5QKp+NQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", + "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", "dev": true, "dependencies": { - "@jest/fake-timers": "^29.4.2", - "@jest/types": "^29.4.2", + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", "@types/node": "*", - "jest-mock": "^29.4.2" + "jest-mock": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.4.2.tgz", - "integrity": "sha512-NUAeZVApzyaeLjfWIV/64zXjA2SS+NuUPHpAlO7IwVMGd5Vf9szTl9KEDlxY3B4liwLO31os88tYNHl6cpjtKQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", "dev": true, "dependencies": { - "expect": "^29.4.2", - "jest-snapshot": "^29.4.2" + "expect": "^29.5.0", + "jest-snapshot": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.4.2.tgz", - "integrity": "sha512-Dd3ilDJpBnqa0GiPN7QrudVs0cczMMHtehSo2CSTjm3zdHx0RcpmhFNVEltuEFeqfLIyWKFI224FsMSQ/nsJQA==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", "dev": true, "dependencies": { - "jest-get-type": "^29.4.2" + "jest-get-type": "^29.4.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/expect-utils/node_modules/jest-get-type": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.2.tgz", - "integrity": "sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jest/fake-timers": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.4.2.tgz", - "integrity": "sha512-Ny1u0Wg6kCsHFWq7A/rW/tMhIedq2siiyHyLpHCmIhP7WmcAmd2cx95P+0xtTZlj5ZbJxIRQi4OPydZZUoiSQQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", + "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", "dev": true, "dependencies": { - "@jest/types": "^29.4.2", + "@jest/types": "^29.5.0", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^29.4.2", - "jest-mock": "^29.4.2", - "jest-util": "^29.4.2" + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.4.2.tgz", - "integrity": "sha512-zCk70YGPzKnz/I9BNFDPlK+EuJLk21ur/NozVh6JVM86/YYZtZHqxFFQ62O9MWq7uf3vIZnvNA0BzzrtxD9iyg==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", + "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", "dev": true, "dependencies": { - "@jest/environment": "^29.4.2", - "@jest/expect": "^29.4.2", - "@jest/types": "^29.4.2", - "jest-mock": "^29.4.2" + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/types": "^29.5.0", + "jest-mock": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.4.2.tgz", - "integrity": "sha512-10yw6YQe75zCgYcXgEND9kw3UZZH5tJeLzWv4vTk/2mrS1aY50A37F+XT2hPO5OqQFFnUWizXD8k1BMiATNfUw==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", + "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.4.2", - "@jest/test-result": "^29.4.2", - "@jest/transform": "^29.4.2", - "@jest/types": "^29.4.2", + "@jest/console": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", "@jridgewell/trace-mapping": "^0.3.15", "@types/node": "*", "chalk": "^4.0.0", @@ -2963,9 +2954,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.4.2", - "jest-util": "^29.4.2", - "jest-worker": "^29.4.2", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -2984,13 +2975,13 @@ } }, "node_modules/@jest/reporters/node_modules/jest-worker": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.4.2.tgz", - "integrity": "sha512-VIuZA2hZmFyRbchsUCHEehoSf2HEl0YVF8SDJqtPnKorAaBuh42V8QsLnde0XP5F6TyCynGPEGgBOn3Fc+wZGw==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.4.2", + "jest-util": "^29.5.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -3014,9 +3005,9 @@ } }, "node_modules/@jest/schemas": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.2.tgz", - "integrity": "sha512-ZrGzGfh31NtdVH8tn0mgJw4khQuNHiKqdzJAFbCaERbyCP9tHlxWuL/mnMu8P7e/+k4puWjI1NOzi/sFsjce/g==", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", "devOptional": true, "dependencies": { "@sinclair/typebox": "^0.25.16" @@ -3026,9 +3017,9 @@ } }, "node_modules/@jest/source-map": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.2.tgz", - "integrity": "sha512-tIoqV5ZNgYI9XCKXMqbYe5JbumcvyTgNN+V5QW4My033lanijvCD0D4PI9tBw4pRTqWOc00/7X3KVvUh+qnF4Q==", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", + "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.15", @@ -3040,13 +3031,13 @@ } }, "node_modules/@jest/test-result": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.4.2.tgz", - "integrity": "sha512-HZsC3shhiHVvMtP+i55MGR5bPcc3obCFbA5bzIOb8pCjwBZf11cZliJncCgaVUbC5yoQNuGqCkC0Q3t6EItxZA==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", + "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", "dev": true, "dependencies": { - "@jest/console": "^29.4.2", - "@jest/types": "^29.4.2", + "@jest/console": "^29.5.0", + "@jest/types": "^29.5.0", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, @@ -3055,14 +3046,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.4.2.tgz", - "integrity": "sha512-9Z2cVsD6CcObIVrWigHp2McRJhvCxL27xHtrZFgNC1RwnoSpDx6fZo8QYjJmziFlW9/hr78/3sxF54S8B6v8rg==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", + "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", "dev": true, "dependencies": { - "@jest/test-result": "^29.4.2", + "@jest/test-result": "^29.5.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.4.2", + "jest-haste-map": "^29.5.0", "slash": "^3.0.0" }, "engines": { @@ -3070,22 +3061,22 @@ } }, "node_modules/@jest/transform": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.4.2.tgz", - "integrity": "sha512-kf1v5iTJHn7p9RbOsBuc/lcwyPtJaZJt5885C98omWz79NIeD3PfoiiaPSu7JyCyFzNOIzKhmMhQLUhlTL9BvQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", + "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^29.4.2", + "@jest/types": "^29.5.0", "@jridgewell/trace-mapping": "^0.3.15", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.4.2", - "jest-regex-util": "^29.4.2", - "jest-util": "^29.4.2", + "jest-haste-map": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -3096,12 +3087,12 @@ } }, "node_modules/@jest/types": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.4.2.tgz", - "integrity": "sha512-CKlngyGP0fwlgC1BRUtPZSiWLBhyS9dKwKmyGxk8Z6M82LBEGB2aLQSg+U1MyLsU+M7UjnlLllBM2BLWKVm/Uw==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.2", + "@jest/schemas": "^29.4.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -4092,12 +4083,12 @@ } }, "node_modules/@nestjs/testing": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.1.4.tgz", - "integrity": "sha512-gO6b9QJyUajh38DNdss9gSE0UO7x60Jh10W4SwHEjQT1W+yxaEWr3aLyuQItTvUVY6C28XKFLTykMpr8GO28Ug==", + "version": "9.3.12", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.3.12.tgz", + "integrity": "sha512-nH274IXEqU4hr4bcb71POe58hYLONt9RcfKKM5ZvOS7wYMnybMpKKR8DkC1WcfE1P2k2GQmQoHeSH5emPtYrBA==", "dev": true, "dependencies": { - "tslib": "2.4.0" + "tslib": "2.5.0" }, "funding": { "type": "opencollective", @@ -4118,12 +4109,6 @@ } } }, - "node_modules/@nestjs/testing/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - }, "node_modules/@nestjs/typeorm": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-9.0.1.tgz", @@ -4274,9 +4259,9 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "node_modules/@sinclair/typebox": { - "version": "0.25.21", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.21.tgz", - "integrity": "sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==", + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", "devOptional": true }, "node_modules/@sinonjs/commons": { @@ -4561,47 +4546,15 @@ } }, "node_modules/@types/jest": { - "version": "27.5.2", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", - "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", - "dev": true, - "dependencies": { - "jest-matcher-utils": "^27.0.0", - "pretty-format": "^27.0.0" - } - }, - "node_modules/@types/jest/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@types/jest/node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==", "dev": true, "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "expect": "^29.0.0", + "pretty-format": "^29.0.0" } }, - "node_modules/@types/jest/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -5755,15 +5708,15 @@ } }, "node_modules/babel-jest": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.4.2.tgz", - "integrity": "sha512-vcghSqhtowXPG84posYkkkzcZsdayFkubUgbE3/1tuGbX7AQtwCkkNA/wIbB0BMjuCPoqTkiDyKN7Ty7d3uwNQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", + "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", "dev": true, "dependencies": { - "@jest/transform": "^29.4.2", + "@jest/transform": "^29.5.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.4.2", + "babel-preset-jest": "^29.5.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -5792,9 +5745,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.4.2.tgz", - "integrity": "sha512-5HZRCfMeWypFEonRbEkwWXtNS1sQK159LhRVyRuLzyfVBxDy/34Tr/rg4YVi0SScSJ4fqeaR/OIeceJ/LaQ0pQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", + "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", "dev": true, "dependencies": { "@babel/template": "^7.3.3", @@ -5830,12 +5783,12 @@ } }, "node_modules/babel-preset-jest": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.4.2.tgz", - "integrity": "sha512-ecWdaLY/8JyfUDr0oELBMpj3R5I1L6ZqG+kRJmwqfHtLWuPrJStR0LUkvUhfykJWTsXXMnohsayN/twltBbDrQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", + "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", "dev": true, "dependencies": { - "babel-plugin-jest-hoist": "^29.4.2", + "babel-plugin-jest-hoist": "^29.5.0", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { @@ -7141,12 +7094,12 @@ } }, "node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", "dev": true, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/dir-glob": { @@ -7822,64 +7775,16 @@ } }, "node_modules/expect": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.4.2.tgz", - "integrity": "sha512-+JHYg9O3hd3RlICG90OPVjRkPBoiUH7PxvDVMnRiaq1g6JUgZStX514erMl0v2Dc5SkfVbm7ztqbd6qHHPn+mQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.4.2", - "jest-get-type": "^29.4.2", - "jest-matcher-utils": "^29.4.2", - "jest-message-util": "^29.4.2", - "jest-util": "^29.4.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/expect/node_modules/diff-sequences": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.2.tgz", - "integrity": "sha512-R6P0Y6PrsH3n4hUXxL3nns0rbRk6Q33js3ygJBeEpbzLzgcNuJ61+u0RXasFpTKISw99TxUzFnumSnRLsjhLaw==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/expect/node_modules/jest-diff": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.4.2.tgz", - "integrity": "sha512-EK8DSajVtnjx9sa1BkjZq3mqChm2Cd8rIzdXkQMA8e0wuXq53ypz6s5o5V8HRZkoEt2ywJ3eeNWFKWeYr8HK4g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.4.2", - "jest-get-type": "^29.4.2", - "pretty-format": "^29.4.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/expect/node_modules/jest-get-type": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.2.tgz", - "integrity": "sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/expect/node_modules/jest-matcher-utils": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.4.2.tgz", - "integrity": "sha512-EZaAQy2je6Uqkrm6frnxBIdaWtSYFoR8SVb2sNLAtldswlR/29JAgx+hy67llT3+hXBaLB0zAm5UfeqerioZyg==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.4.2", - "jest-get-type": "^29.4.2", - "pretty-format": "^29.4.2" + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -9640,15 +9545,15 @@ } }, "node_modules/jest": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.4.2.tgz", - "integrity": "sha512-+5hLd260vNIHu+7ZgMIooSpKl7Jp5pHKb51e73AJU3owd5dEo/RfVwHbA/na3C/eozrt3hJOLGf96c7EWwIAzg==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", "dev": true, "dependencies": { - "@jest/core": "^29.4.2", - "@jest/types": "^29.4.2", + "@jest/core": "^29.5.0", + "@jest/types": "^29.5.0", "import-local": "^3.0.2", - "jest-cli": "^29.4.2" + "jest-cli": "^29.5.0" }, "bin": { "jest": "bin/jest.js" @@ -9666,9 +9571,9 @@ } }, "node_modules/jest-changed-files": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.4.2.tgz", - "integrity": "sha512-Qdd+AXdqD16PQa+VsWJpxR3kN0JyOCX1iugQfx5nUgAsI4gwsKviXkpclxOK9ZnwaY2IQVHz+771eAvqeOlfuw==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", + "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", "dev": true, "dependencies": { "execa": "^5.0.0", @@ -9679,28 +9584,29 @@ } }, "node_modules/jest-circus": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.4.2.tgz", - "integrity": "sha512-wW3ztp6a2P5c1yOc1Cfrt5ozJ7neWmqeXm/4SYiqcSriyisgq63bwFj1NuRdSR5iqS0CMEYwSZd89ZA47W9zUg==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", + "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", "dev": true, "dependencies": { - "@jest/environment": "^29.4.2", - "@jest/expect": "^29.4.2", - "@jest/test-result": "^29.4.2", - "@jest/types": "^29.4.2", + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^0.7.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.4.2", - "jest-matcher-utils": "^29.4.2", - "jest-message-util": "^29.4.2", - "jest-runtime": "^29.4.2", - "jest-snapshot": "^29.4.2", - "jest-util": "^29.4.2", + "jest-each": "^29.5.0", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", "p-limit": "^3.1.0", - "pretty-format": "^29.4.2", + "pretty-format": "^29.5.0", + "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -9708,70 +9614,22 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-circus/node_modules/diff-sequences": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.2.tgz", - "integrity": "sha512-R6P0Y6PrsH3n4hUXxL3nns0rbRk6Q33js3ygJBeEpbzLzgcNuJ61+u0RXasFpTKISw99TxUzFnumSnRLsjhLaw==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-diff": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.4.2.tgz", - "integrity": "sha512-EK8DSajVtnjx9sa1BkjZq3mqChm2Cd8rIzdXkQMA8e0wuXq53ypz6s5o5V8HRZkoEt2ywJ3eeNWFKWeYr8HK4g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.4.2", - "jest-get-type": "^29.4.2", - "pretty-format": "^29.4.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-get-type": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.2.tgz", - "integrity": "sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-matcher-utils": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.4.2.tgz", - "integrity": "sha512-EZaAQy2je6Uqkrm6frnxBIdaWtSYFoR8SVb2sNLAtldswlR/29JAgx+hy67llT3+hXBaLB0zAm5UfeqerioZyg==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.4.2", - "jest-get-type": "^29.4.2", - "pretty-format": "^29.4.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-cli": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.4.2.tgz", - "integrity": "sha512-b+eGUtXq/K2v7SH3QcJvFvaUaCDS1/YAZBYz0m28Q/Ppyr+1qNaHmVYikOrbHVbZqYQs2IeI3p76uy6BWbXq8Q==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", + "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", "dev": true, "dependencies": { - "@jest/core": "^29.4.2", - "@jest/test-result": "^29.4.2", - "@jest/types": "^29.4.2", + "@jest/core": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.4.2", - "jest-util": "^29.4.2", - "jest-validate": "^29.4.2", + "jest-config": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", "prompts": "^2.0.1", "yargs": "^17.3.1" }, @@ -9805,9 +9663,9 @@ } }, "node_modules/jest-cli/node_modules/yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", "dev": true, "dependencies": { "cliui": "^8.0.1", @@ -9832,31 +9690,31 @@ } }, "node_modules/jest-config": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.4.2.tgz", - "integrity": "sha512-919CtnXic52YM0zW4C1QxjG6aNueX1kBGthuMtvFtRTAxhKfJmiXC9qwHmi6o2josjbDz8QlWyY55F1SIVmCWA==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", + "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.4.2", - "@jest/types": "^29.4.2", - "babel-jest": "^29.4.2", + "@jest/test-sequencer": "^29.5.0", + "@jest/types": "^29.5.0", + "babel-jest": "^29.5.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.4.2", - "jest-environment-node": "^29.4.2", - "jest-get-type": "^29.4.2", - "jest-regex-util": "^29.4.2", - "jest-resolve": "^29.4.2", - "jest-runner": "^29.4.2", - "jest-util": "^29.4.2", - "jest-validate": "^29.4.2", + "jest-circus": "^29.5.0", + "jest-environment-node": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.4.2", + "pretty-format": "^29.5.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -9876,66 +9734,25 @@ } } }, - "node_modules/jest-config/node_modules/jest-get-type": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.2.tgz", - "integrity": "sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-diff/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, "node_modules/jest-docblock": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.2.tgz", - "integrity": "sha512-dV2JdahgClL34Y5vLrAHde3nF3yo2jKRH+GIYJuCpfqwEJZcikzeafVTGAjbOfKPG17ez9iWXwUYp7yefeCRag==", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", + "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", "dev": true, "dependencies": { "detect-newline": "^3.0.0" @@ -9945,71 +9762,62 @@ } }, "node_modules/jest-each": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.4.2.tgz", - "integrity": "sha512-trvKZb0JYiCndc55V1Yh0Luqi7AsAdDWpV+mKT/5vkpnnFQfuQACV72IoRV161aAr6kAVIBpmYzwhBzm34vQkA==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", + "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", "dev": true, "dependencies": { - "@jest/types": "^29.4.2", + "@jest/types": "^29.5.0", "chalk": "^4.0.0", - "jest-get-type": "^29.4.2", - "jest-util": "^29.4.2", - "pretty-format": "^29.4.2" + "jest-get-type": "^29.4.3", + "jest-util": "^29.5.0", + "pretty-format": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-each/node_modules/jest-get-type": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.2.tgz", - "integrity": "sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-environment-node": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.4.2.tgz", - "integrity": "sha512-MLPrqUcOnNBc8zTOfqBbxtoa8/Ee8tZ7UFW7hRDQSUT+NGsvS96wlbHGTf+EFAT9KC3VNb7fWEM6oyvmxtE/9w==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", + "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", "dev": true, "dependencies": { - "@jest/environment": "^29.4.2", - "@jest/fake-timers": "^29.4.2", - "@jest/types": "^29.4.2", + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", "@types/node": "*", - "jest-mock": "^29.4.2", - "jest-util": "^29.4.2" + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", "dev": true, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.4.2.tgz", - "integrity": "sha512-WkUgo26LN5UHPknkezrBzr7lUtV1OpGsp+NfXbBwHztsFruS3gz+AMTTBcEklvi8uPzpISzYjdKXYZQJXBnfvw==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", + "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", "dev": true, "dependencies": { - "@jest/types": "^29.4.2", + "@jest/types": "^29.5.0", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.2", - "jest-util": "^29.4.2", - "jest-worker": "^29.4.2", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -10021,13 +9829,13 @@ } }, "node_modules/jest-haste-map/node_modules/jest-worker": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.4.2.tgz", - "integrity": "sha512-VIuZA2hZmFyRbchsUCHEehoSf2HEl0YVF8SDJqtPnKorAaBuh42V8QsLnde0XP5F6TyCynGPEGgBOn3Fc+wZGw==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.4.2", + "jest-util": "^29.5.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -10051,87 +9859,46 @@ } }, "node_modules/jest-leak-detector": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.4.2.tgz", - "integrity": "sha512-Wa62HuRJmWXtX9F00nUpWlrbaH5axeYCdyRsOs/+Rb1Vb6+qWTlB5rKwCCRKtorM7owNwKsyJ8NRDUcZ8ghYUA==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", + "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", "dev": true, "dependencies": { - "jest-get-type": "^29.4.2", - "pretty-format": "^29.4.2" + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-leak-detector/node_modules/jest-get-type": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.2.tgz", - "integrity": "sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-matcher-utils": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", - "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-matcher-utils/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, "node_modules/jest-message-util": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.4.2.tgz", - "integrity": "sha512-SElcuN4s6PNKpOEtTInjOAA8QvItu0iugkXqhYyguRvQoXapg5gN+9RQxLAkakChZA7Y26j6yUCsFWN+hlKD6g==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.4.2", + "@jest/types": "^29.5.0", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.4.2", + "pretty-format": "^29.5.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -10140,14 +9907,14 @@ } }, "node_modules/jest-mock": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.4.2.tgz", - "integrity": "sha512-x1FSd4Gvx2yIahdaIKoBjwji6XpboDunSJ95RpntGrYulI1ByuYQCKN/P7hvk09JB74IonU3IPLdkutEWYt++g==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", + "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", "dev": true, "dependencies": { - "@jest/types": "^29.4.2", + "@jest/types": "^29.5.0", "@types/node": "*", - "jest-util": "^29.4.2" + "jest-util": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -10171,26 +9938,26 @@ } }, "node_modules/jest-regex-util": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.2.tgz", - "integrity": "sha512-XYZXOqUl1y31H6VLMrrUL1ZhXuiymLKPz0BO1kEeR5xER9Tv86RZrjTm74g5l9bPJQXA/hyLdaVPN/sdqfteig==", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", + "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.4.2.tgz", - "integrity": "sha512-RtKWW0mbR3I4UdkOrW7552IFGLYQ5AF9YrzD0FnIOkDu0rAMlA5/Y1+r7lhCAP4nXSBTaE7ueeqj6IOwZpgoqw==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", + "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", "dev": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.4.2", + "jest-haste-map": "^29.5.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.4.2", - "jest-validate": "^29.4.2", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" @@ -10200,43 +9967,43 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.4.2.tgz", - "integrity": "sha512-6pL4ptFw62rjdrPk7rRpzJYgcRqRZNsZTF1VxVTZMishbO6ObyWvX57yHOaNGgKoADtAHRFYdHQUEvYMJATbDg==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", + "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", "dev": true, "dependencies": { - "jest-regex-util": "^29.4.2", - "jest-snapshot": "^29.4.2" + "jest-regex-util": "^29.4.3", + "jest-snapshot": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.4.2.tgz", - "integrity": "sha512-wqwt0drm7JGjwdH+x1XgAl+TFPH7poowMguPQINYxaukCqlczAcNLJiK+OLxUxQAEWMdy+e6nHZlFHO5s7EuRg==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", + "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", "dev": true, "dependencies": { - "@jest/console": "^29.4.2", - "@jest/environment": "^29.4.2", - "@jest/test-result": "^29.4.2", - "@jest/transform": "^29.4.2", - "@jest/types": "^29.4.2", + "@jest/console": "^29.5.0", + "@jest/environment": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^29.4.2", - "jest-environment-node": "^29.4.2", - "jest-haste-map": "^29.4.2", - "jest-leak-detector": "^29.4.2", - "jest-message-util": "^29.4.2", - "jest-resolve": "^29.4.2", - "jest-runtime": "^29.4.2", - "jest-util": "^29.4.2", - "jest-watcher": "^29.4.2", - "jest-worker": "^29.4.2", + "jest-docblock": "^29.4.3", + "jest-environment-node": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-leak-detector": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-resolve": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-util": "^29.5.0", + "jest-watcher": "^29.5.0", + "jest-worker": "^29.5.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -10245,13 +10012,13 @@ } }, "node_modules/jest-runner/node_modules/jest-worker": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.4.2.tgz", - "integrity": "sha512-VIuZA2hZmFyRbchsUCHEehoSf2HEl0YVF8SDJqtPnKorAaBuh42V8QsLnde0XP5F6TyCynGPEGgBOn3Fc+wZGw==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.4.2", + "jest-util": "^29.5.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -10294,32 +10061,31 @@ } }, "node_modules/jest-runtime": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.4.2.tgz", - "integrity": "sha512-3fque9vtpLzGuxT9eZqhxi+9EylKK/ESfhClv4P7Y9sqJPs58LjVhTt8jaMp/pRO38agll1CkSu9z9ieTQeRrw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.4.2", - "@jest/fake-timers": "^29.4.2", - "@jest/globals": "^29.4.2", - "@jest/source-map": "^29.4.2", - "@jest/test-result": "^29.4.2", - "@jest/transform": "^29.4.2", - "@jest/types": "^29.4.2", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", + "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/globals": "^29.5.0", + "@jest/source-map": "^29.4.3", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.4.2", - "jest-message-util": "^29.4.2", - "jest-mock": "^29.4.2", - "jest-regex-util": "^29.4.2", - "jest-resolve": "^29.4.2", - "jest-snapshot": "^29.4.2", - "jest-util": "^29.4.2", - "semver": "^7.3.5", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -10328,9 +10094,9 @@ } }, "node_modules/jest-snapshot": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.4.2.tgz", - "integrity": "sha512-PdfubrSNN5KwroyMH158R23tWcAXJyx4pvSvWls1dHoLCaUhGul9rsL3uVjtqzRpkxlkMavQjGuWG1newPgmkw==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", + "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", @@ -10339,84 +10105,35 @@ "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.4.2", - "@jest/transform": "^29.4.2", - "@jest/types": "^29.4.2", + "@jest/expect-utils": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", "@types/babel__traverse": "^7.0.6", "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.4.2", + "expect": "^29.5.0", "graceful-fs": "^4.2.9", - "jest-diff": "^29.4.2", - "jest-get-type": "^29.4.2", - "jest-haste-map": "^29.4.2", - "jest-matcher-utils": "^29.4.2", - "jest-message-util": "^29.4.2", - "jest-util": "^29.4.2", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", "natural-compare": "^1.4.0", - "pretty-format": "^29.4.2", + "pretty-format": "^29.5.0", "semver": "^7.3.5" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/diff-sequences": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.2.tgz", - "integrity": "sha512-R6P0Y6PrsH3n4hUXxL3nns0rbRk6Q33js3ygJBeEpbzLzgcNuJ61+u0RXasFpTKISw99TxUzFnumSnRLsjhLaw==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-diff": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.4.2.tgz", - "integrity": "sha512-EK8DSajVtnjx9sa1BkjZq3mqChm2Cd8rIzdXkQMA8e0wuXq53ypz6s5o5V8HRZkoEt2ywJ3eeNWFKWeYr8HK4g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.4.2", - "jest-get-type": "^29.4.2", - "pretty-format": "^29.4.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-get-type": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.2.tgz", - "integrity": "sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.4.2.tgz", - "integrity": "sha512-EZaAQy2je6Uqkrm6frnxBIdaWtSYFoR8SVb2sNLAtldswlR/29JAgx+hy67llT3+hXBaLB0zAm5UfeqerioZyg==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.4.2", - "jest-get-type": "^29.4.2", - "pretty-format": "^29.4.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-util": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.4.2.tgz", - "integrity": "sha512-wKnm6XpJgzMUSRFB7YF48CuwdzuDIHenVuoIb1PLuJ6F+uErZsuDkU+EiExkChf6473XcawBrSfDSnXl+/YG4g==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", "dev": true, "dependencies": { - "@jest/types": "^29.4.2", + "@jest/types": "^29.5.0", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -10428,17 +10145,17 @@ } }, "node_modules/jest-validate": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.4.2.tgz", - "integrity": "sha512-tto7YKGPJyFbhcKhIDFq8B5od+eVWD/ySZ9Tvcp/NGCvYA4RQbuzhbwYWtIjMT5W5zA2W0eBJwu4HVw34d5G6Q==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", + "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", "dev": true, "dependencies": { - "@jest/types": "^29.4.2", + "@jest/types": "^29.5.0", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^29.4.2", + "jest-get-type": "^29.4.3", "leven": "^3.1.0", - "pretty-format": "^29.4.2" + "pretty-format": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -10456,28 +10173,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-validate/node_modules/jest-get-type": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.2.tgz", - "integrity": "sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-watcher": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.4.2.tgz", - "integrity": "sha512-onddLujSoGiMJt+tKutehIidABa175i/Ays+QvKxCqBwp7fvxP3ZhKsrIdOodt71dKxqk4sc0LN41mWLGIK44w==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", + "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", "dev": true, "dependencies": { - "@jest/test-result": "^29.4.2", - "@jest/types": "^29.4.2", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.4.2", + "jest-util": "^29.5.0", "string-length": "^4.0.1" }, "engines": { @@ -12494,12 +12202,12 @@ } }, "node_modules/pretty-format": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.4.2.tgz", - "integrity": "sha512-qKlHR8yFVCbcEWba0H0TOC8dnLlO4vPlyEjRPw31FZ2Rupy9nLa8ZLbYny8gWEl8CkEhJqAE6IzdNELTBVcBEg==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", "devOptional": true, "dependencies": { - "@jest/schemas": "^29.4.2", + "@jest/schemas": "^29.4.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -12660,6 +12368,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz", + "integrity": "sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, "node_modules/qs": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", @@ -12939,9 +12663,9 @@ } }, "node_modules/resolve.exports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.0.tgz", - "integrity": "sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", "dev": true, "engines": { "node": ">=10" @@ -14688,9 +14412,9 @@ "devOptional": true }, "node_modules/v8-to-istanbul": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", - "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", diff --git a/package.json b/package.json index b8c35a502..0bf0a0150 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "description": "Nest TypeScript starter repository", "license": "MIT", "scripts": { + "init": "run-script-os", + "init:nix": "rimraf src/plugins && mkdir src/plugins && cp src/plugins.template/plugin.module.ts.template src/plugins/plugin.module.ts", "prebuild": "rimraf dist", "build": "nest build && npm run copy-pem", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", @@ -90,11 +92,11 @@ "devDependencies": { "@nestjs/cli": "9.2.0", "@nestjs/schematics": "9.0.4", - "@nestjs/testing": "9.1.4", + "@nestjs/testing": "9.3.12", "@types/express": "^4.17.11", "@types/graphql-relay": "^0.6.0", - "@types/jest": "^27.4.1", - "@types/node": "^14.14.36", + "@types/jest": "29.5.0", + "@types/node": "^14.14.42", "@types/passport-local": "^1.0.33", "@types/supertest": "^2.0.12", "@types/tiny-async-pool": "^1.0.0", @@ -103,7 +105,7 @@ "eslint": "8.28.0", "eslint-config-prettier": "8.5.0", "eslint-plugin-prettier": "3.4.1", - "jest": "29.4.2", + "jest": "29.5.0", "prettier": "^2.2.1", "supertest": "6.3.3", "ts-jest": "29.0.5", diff --git a/schema.gql b/schema.gql index 34f2aa008..7e07b9510 100644 --- a/schema.gql +++ b/schema.gql @@ -88,6 +88,7 @@ type Asset { identifier: String! isLiked(byAddress: String!): Boolean isNsfw: Boolean + isTicket: Boolean isWhitelistedStorage: Boolean! likesCount: Int lowestAuction(filters: MarketplaceFilters): Auction @@ -115,12 +116,15 @@ type Asset { enum AssetActionEnum { Added + Bid Bought ClosedAuction Created EndedAuction + PriceUpdated Received StartedAuction + Updated } type AssetEdge { @@ -181,6 +185,7 @@ input AssetsFilter { """This will work only with an owner address""" collections: [String!] creatorAddress: String + customFilters: CustomFiltersEnum identifier: String identifiers: [String!] likedByAddress: String @@ -508,7 +513,7 @@ type CollectionResponse { } type CollectionRole { - address: String! + address: String canAddQuantity: Boolean canAddUri: Boolean canBurn: Boolean @@ -623,6 +628,10 @@ input CurrentPaymentTokensFilters { marketplaceKey: String } +enum CustomFiltersEnum { + Tickets +} + type ExploreCollectionsStats { activeLast30DaysCount: Int! allCollectionsCount: Int! @@ -645,6 +654,7 @@ type ExploreStats { enum FeaturedCollectionTypeEnum { Featured Hero + Tickets } input FeaturedCollectionsArgs { @@ -768,6 +778,12 @@ type MarketplacePageInfo { startCursor: String } +input MarketplaceReindexDataArgs { + afterTimestamp: Float + beforeTimestamp: Float + marketplaceAddress: String! +} + enum MarketplaceTypeEnum { External Internal @@ -838,6 +854,7 @@ type Mutation { clearReportNft(input: ClearReportInput!): Boolean! createAuction(input: CreateAuctionArgs!): TransactionNode! createNft(file: Upload!, input: CreateNftArgs!): TransactionNode! + createNftWithMultipleFiles(files: [Upload!]!, input: CreateNftArgs!): TransactionNode! endAuction(auctionId: Int!): TransactionNode! flagCollection(input: FlagCollectionInput!): Boolean! flagNft(input: FlagNftInput!): Boolean! @@ -845,6 +862,7 @@ type Mutation { issueCampaign(input: IssueCampaignArgs!): TransactionNode! issueNftCollection(input: IssueCollectionArgs!): TransactionNode! issueSftCollection(input: IssueCollectionArgs!): TransactionNode! + reindexMarketplaceData(input: MarketplaceReindexDataArgs!): Boolean! reindexMarketplaceEvents(input: MarketplaceEventsIndexingArgs!): Boolean! removeBlacklistedCollection(collection: String!): Boolean! removeFeaturedCollection(input: FeaturedCollectionsArgs!): Boolean! @@ -1201,6 +1219,7 @@ type ScamInfo { } enum ScamInfoTypeEnum { + none potentialScam scam } diff --git a/src/common/persistence/persistence.service.ts b/src/common/persistence/persistence.service.ts index 8ee3e2722..3a7275a15 100644 --- a/src/common/persistence/persistence.service.ts +++ b/src/common/persistence/persistence.service.ts @@ -84,7 +84,7 @@ export class PersistenceService { private readonly auctionsRepository: AuctionsRepository, private readonly marketplaceEventsRepository: MarketplaceEventsRepository, private readonly offersRepository: OffersRepository, - ) {} + ) { } private async execute(key: string, action: Promise): Promise { const profiler = new PerformanceProfiler(); @@ -338,6 +338,13 @@ export class PersistenceService { ); } + async getFeaturedCollectionsByIdentifiers(collections: string[]): Promise { + return await this.execute( + this.getFeaturedCollections.name, + this.featuredCollectionsRepository.getFeaturedCollectionsByIdentifiers(collections), + ); + } + async addFeaturedCollection( collection: string, type: FeaturedCollectionTypeEnum, @@ -729,6 +736,13 @@ export class PersistenceService { ); } + async saveBulkOrders(orders: OrderEntity[]) { + return await this.execute( + this.saveBulkOrders.name, + this.ordersRepository.saveBulkOrders(orders), + ); + } + async updateOrderWithStatus(order: OrderEntity, status: OrderStatusEnum) { return await this.execute( this.updateOrderWithStatus.name, @@ -878,6 +892,18 @@ export class PersistenceService { this.auctionsRepository.getBulkAuctions(auctionsIds), ); } + async getBulkAuctionsByAuctionIdsAndMarketplace( + auctionsIds: number[], + marketplaceKey: string, + ): Promise { + return await this.execute( + this.getBulkAuctionsByAuctionIdsAndMarketplace.name, + this.auctionsRepository.getBulkAuctionsByAuctionIdsAndMarketplace( + auctionsIds, + marketplaceKey, + ), + ); + } async getAuctionByMarketplace( id: number, @@ -918,6 +944,19 @@ export class PersistenceService { ); } + async getBulkAuctionsByIdentifierAndMarketplace( + identifiers: string[], + marletplaceKey: string, + ): Promise { + return await this.execute( + this.getBulkAuctionsByIdentifierAndMarketplace.name, + this.auctionsRepository.getBulkAuctionsByIdentifiersAndMarketplace( + identifiers, + marletplaceKey, + ), + ); + } + async getAvailableTokensForIdentifiers(identifiers: string[]): Promise { return await this.execute( this.getAvailableTokensForIdentifiers.name, @@ -978,6 +1017,13 @@ export class PersistenceService { ); } + async saveBulkAuctions(auctions: AuctionEntity[]): Promise { + return await this.execute( + this.saveBulkAuctions.name, + this.auctionsRepository.saveBulkAuctions(auctions), + ); + } + async rollbackAuctionAndOrdersByHash(blockHash: string): Promise { return await this.execute( this.rollbackAuctionAndOrdersByHash.name, @@ -1078,6 +1124,13 @@ export class PersistenceService { ); } + async saveBulkOffers(offers: OfferEntity[]): Promise { + return await this.execute( + this.saveBulkOffers.name, + this.offersRepository.saveBulkOffers(offers), + ); + } + async getOffersThatReachedDeadline(): Promise { return await this.execute( this.getOffersThatReachedDeadline.name, @@ -1092,6 +1145,19 @@ export class PersistenceService { ); } + async getBulkOffersByOfferIdsAndMarketplace( + offerIds: number[], + marketplaceKey: string, + ): Promise { + return await this.execute( + this.getBulkOffersByOfferIdsAndMarketplace.name, + this.offersRepository.getBulkOffersByOfferIdsAndMarketplace( + offerIds, + marketplaceKey, + ), + ); + } + async getMostLikedAssetsIdentifiers( offset?: number, limit?: number, diff --git a/src/common/pluggins/plugin.service.ts b/src/common/pluggins/plugin.service.ts new file mode 100644 index 000000000..12b7126c6 --- /dev/null +++ b/src/common/pluggins/plugin.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { Asset } from 'src/modules/assets/models/Asset.dto'; + +@Injectable() +export class PluginService { + async computeScamInfo(_nft: Asset[]): Promise {} +} diff --git a/src/common/services/caching/entities/cache.info.ts b/src/common/services/caching/entities/cache.info.ts index 849350237..b1394a26e 100644 --- a/src/common/services/caching/entities/cache.info.ts +++ b/src/common/services/caching/entities/cache.info.ts @@ -129,6 +129,16 @@ export class CacheInfo { ttl: 10 * Constants.oneMinute(), }; + static CexTokens: CacheInfo = { + key: 'cexTokens', + ttl: 10 * Constants.oneMinute(), + }; + + static xExchangeTokens: CacheInfo = { + key: 'xExchangeTokens', + ttl: 10 * Constants.oneMinute(), + }; + static AllTokens: CacheInfo = { key: 'allTokens', ttl: Constants.oneMinute(), @@ -156,7 +166,7 @@ export class CacheInfo { static AssetHistory: CacheInfo = { key: 'assetHistory', - ttl: Constants.oneDay(), + ttl: 5 * Constants.oneMinute(), }; static CollectionTypes: CacheInfo = { diff --git a/src/common/services/mx-communication/index.ts b/src/common/services/mx-communication/index.ts index 2e408bd4a..4f65ef978 100644 --- a/src/common/services/mx-communication/index.ts +++ b/src/common/services/mx-communication/index.ts @@ -1,7 +1,6 @@ export * from './mx-api.service'; export * from './mx-stats.service'; export * from './mx-communication.module'; -export * from './mx-tools.service'; export * from './mx-elastic.service'; export * from './mx-identity.service'; export * from './mx-proxy.service'; diff --git a/src/common/services/mx-communication/models/account.info.ts b/src/common/services/mx-communication/models/account.info.ts new file mode 100644 index 000000000..4c471bc8b --- /dev/null +++ b/src/common/services/mx-communication/models/account.info.ts @@ -0,0 +1,19 @@ +import { ScamInfo } from 'src/modules/assets/models/ScamInfo.dto'; + +export class AccountInfo { + address: string = ''; + nonce: number = 0; + balance: string = ''; + username: string = ''; + code: string = ''; + codeHash: string | undefined; + rootHash: string = ''; + codeMetadata: string = ''; + developerReward: string = ''; + ownerAddress: string = ''; + scamInfo: ScamInfo | undefined = undefined; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} diff --git a/src/common/services/mx-communication/models/api-token.model.ts b/src/common/services/mx-communication/models/api-token.model.ts new file mode 100644 index 000000000..11ce41d7f --- /dev/null +++ b/src/common/services/mx-communication/models/api-token.model.ts @@ -0,0 +1,23 @@ +export class ApiToken { + identifier: string; + ticker: string; + name: string; + price: string; + decimals: number; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} + +export class DexToken { + id: string; + symbol: string; + name: string; + price: string; + decimals: number; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} diff --git a/src/common/services/mx-communication/models/nft.dto.ts b/src/common/services/mx-communication/models/nft.dto.ts index 1006918e0..681ed64c5 100644 --- a/src/common/services/mx-communication/models/nft.dto.ts +++ b/src/common/services/mx-communication/models/nft.dto.ts @@ -31,6 +31,7 @@ export interface Nft { nonce: number; royalties: string; timestamp: number; + url: string; uris: string[]; tags: string[]; metadata: NftMetadata; diff --git a/src/common/services/mx-communication/models/scam-info.dto.ts b/src/common/services/mx-communication/models/scam-info.dto.ts index ff5855af4..638684de8 100644 --- a/src/common/services/mx-communication/models/scam-info.dto.ts +++ b/src/common/services/mx-communication/models/scam-info.dto.ts @@ -4,6 +4,7 @@ export interface ScamInfoApi { } export enum ScamInfoTypeApiEnum { + none = 'none', scam = 'scam', potentialScam = 'potentialScam', } diff --git a/src/common/services/mx-communication/mx-api.service.ts b/src/common/services/mx-communication/mx-api.service.ts index 410ed6df7..0cd571527 100644 --- a/src/common/services/mx-communication/mx-api.service.ts +++ b/src/common/services/mx-communication/mx-api.service.ts @@ -11,7 +11,7 @@ import { TransactionOnNetwork, } from '@multiversx/sdk-network-providers'; import { AssetsQuery } from 'src/modules/assets/assets-query'; -import { Token } from './models/Token.model'; +import { Token } from '../../../modules/usdPrice/Token.model'; import { Address } from '@multiversx/sdk-core'; import { SmartContractApi } from './models/smart-contract.api'; import { XOXNO_MINTING_MANAGER } from 'src/utils/constants'; @@ -567,18 +567,6 @@ export class MxApiService { return [nfts, lastTimestamp]; } - async getNftsWithScamInfoBeforeTimestamp( - beforeTimestamp: number, - size: number, - ): Promise<[Nft[], number]> { - let [nfts, lastTimestamp] = await this.getNftsBeforeTimestamp( - beforeTimestamp, - size, - ['identifier', 'scamInfo', 'timestamp'], - ); - return [nfts, lastTimestamp]; - } - async getTagsBySearch(searchTerm: string = ''): Promise { return await this.doGetGeneric( this.getTagsBySearch.name, @@ -626,7 +614,7 @@ export class MxApiService { async getAllTokens(): Promise { const allTokens = await this.doGetGeneric( this.getAllTokens.name, - 'tokens?size=10000&fields=identifier,name,ticker,decimals', + 'tokens?size=10000&fields=identifier,name,ticker,decimals,price', ); return allTokens.map((t) => Token.fromMxApiToken(t)); } @@ -641,13 +629,13 @@ export class MxApiService { async getTokenData(tokenId: string): Promise { const token = await this.doGetGeneric( this.getTokenData.name, - `tokens/${tokenId}?fields=identifier,name,ticker,decimals`, + `tokens/${tokenId}?fields=identifier,name,ticker,decimals,price`, ); return token ? new Token({ - ...token, - symbol: token.ticker, - }) + ...token, + symbol: token.ticker, + }) : undefined; } @@ -719,32 +707,6 @@ export class MxApiService { return new MxApiAbout(about); } - async getBulkNftScamInfo( - identifiers: string[], - computeScamInfo: boolean, - ): Promise { - const query = new AssetsQuery() - .addIdentifiers(identifiers) - .addPageSize(0, identifiers.length) - .addFields(['identifier', 'scamInfo']) - .addComputeScamInfo(computeScamInfo); - const url = `nfts${query.build(false)}`; - let nfts = await this.doGetGeneric(this.getBulkNftScamInfo.name, url); - return nfts; - } - - async getNftScamInfo( - identifier: string, - computeScamInfo: boolean, - ): Promise { - const query = new AssetsQuery() - .addFields(['identifier', 'scamInfo']) - .addComputeScamInfo(computeScamInfo); - const url = `nfts/${identifier}${query.build(false)}`; - let nftScamInfo = await this.doGetGeneric(this.getNftScamInfo.name, url); - return nftScamInfo; - } - private filterUniqueNftsByNonce(nfts: Nft[]): Nft[] { return nfts.distinct((nft) => nft.nonce); } diff --git a/src/common/services/mx-communication/mx-communication.module.ts b/src/common/services/mx-communication/mx-communication.module.ts index 6c899468a..71ed4e4d6 100644 --- a/src/common/services/mx-communication/mx-communication.module.ts +++ b/src/common/services/mx-communication/mx-communication.module.ts @@ -1,7 +1,6 @@ import { Logger, Module } from '@nestjs/common'; import { ApiService } from './api.service'; import { MxApiService } from './mx-api.service'; -import { MxToolsService } from './mx-tools.service'; import { MxElasticService } from './mx-elastic.service'; import { MxFeedService } from './mx-feed.service'; import { MxIdentityService } from './mx-identity.service'; @@ -10,8 +9,8 @@ import { MxProxyService } from './mx-proxy.service'; import { MxStatsService } from './mx-stats.service'; import { SlackReportService } from './slack-report.service'; import { ConfigService } from '@nestjs/config'; -import { MxExtrasApiService } from './mx-extras-api.service'; import { ApiConfigService } from 'src/modules/common/api-config/api.config.service'; +import { MxDataApiService } from './mx-data.service'; @Module({ providers: [ @@ -25,10 +24,9 @@ import { ApiConfigService } from 'src/modules/common/api-config/api.config.servi MxStatsService, MxElasticService, MxIdentityService, - MxToolsService, MxFeedService, - MxExtrasApiService, SlackReportService, + MxDataApiService, ], exports: [ ApiService, @@ -38,10 +36,9 @@ import { ApiConfigService } from 'src/modules/common/api-config/api.config.servi MxApiService, MxPrivateApiService, MxIdentityService, - MxToolsService, MxFeedService, - MxExtrasApiService, SlackReportService, + MxDataApiService, ], }) export class MxCommunicationModule {} diff --git a/src/common/services/mx-communication/mx-data.service.ts b/src/common/services/mx-communication/mx-data.service.ts new file mode 100644 index 000000000..4d1567e0d --- /dev/null +++ b/src/common/services/mx-communication/mx-data.service.ts @@ -0,0 +1,89 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { mxConfig } from 'src/config'; +import { ApiService } from './api.service'; +import { ApiConfigService } from 'src/modules/common/api-config/api.config.service'; + +@Injectable() +export class MxDataApiService { + private url: string; + + constructor( + private readonly logger: Logger, + private readonly apiConfigService: ApiConfigService, + private readonly apiService: ApiService, + ) { + this.url = this.apiConfigService.getToolsUrl(); + } + + async getCexPrice(timestamp: string): Promise { + let requestUrl = `${this.url}/v1/quotes/cex/${mxConfig.egld}?fields=price&date=${timestamp}`; + + try { + let response = await this.apiService.get(requestUrl); + return response.data.price; + } catch (error) { + this.logger.error( + `An error occurred while calling the mx data service on url ${requestUrl}`, + { + path: this.getCexPrice.name, + exception: error, + }, + ); + return; + } + } + + async getCexTokens(): Promise { + const requestUrl = `${this.url}/v1/tokens/cex?fields=identifier`; + try { + let response = await this.apiService.get(requestUrl); + return response?.data?.identifier; + } catch (error) { + this.logger.error( + `An error occurred while calling the mx data service on url ${requestUrl}`, + { + path: this.getCexTokens.name, + exception: error, + }, + ); + return; + } + } + + async getXexchangeTokens(): Promise { + const requestUrl = `${this.url}/v1/tokens/xexchange?fields=identifier`; + try { + let response = await this.apiService.get(requestUrl); + return response?.data?.identifier; + } catch (error) { + this.logger.error( + `An error occurred while calling the mx data service on url ${requestUrl}`, + { + path: this.getXexchangeTokens.name, + exception: error, + }, + ); + return; + } + } + + async getXechangeTokenPrice( + token: string, + isoDateOnly: string, + ): Promise { + const requestUrl = `${this.url}/v1/quotes/xexchange/${token}?date=${isoDateOnly}&fields=price`; + try { + let response = await this.apiService.get(requestUrl); + return response.data.price; + } catch (error) { + this.logger.error( + `An error occurred while calling the mx data service on url ${requestUrl}`, + { + path: this.getCexPrice.name, + exception: error, + }, + ); + return; + } + } +} diff --git a/src/common/services/mx-communication/mx-extras-api.service.ts b/src/common/services/mx-communication/mx-extras-api.service.ts deleted file mode 100644 index 44eff7c83..000000000 --- a/src/common/services/mx-communication/mx-extras-api.service.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { mxConfig } from 'src/config'; -import { NativeAuthSigner } from '@multiversx/sdk-nestjs/lib/src/utils/native.auth.signer'; -import { ApiService } from './api.service'; -import { ApiSettings } from './models/api-settings'; -import { getFilePathFromDist } from 'src/utils/helpers'; -import { ApiConfigService } from 'src/modules/common/api-config/api.config.service'; - -@Injectable() -export class MxExtrasApiService { - private url: string; - private nativeAuthSigner: NativeAuthSigner; - - constructor( - private readonly logger: Logger, - private readonly apiConfigService: ApiConfigService, - private readonly apiService: ApiService, - ) { - this.url = this.apiConfigService.getExtrasApiUrl(); - this.nativeAuthSigner = new NativeAuthSigner({ - origin: 'NftService', - apiUrl: this.apiConfigService.getApiUrl(), - signerPrivateKeyPath: getFilePathFromDist(mxConfig.pemFileName), - }); - } - - async setCollectionScam(collection: string): Promise { - await this.doPost( - this.setCollectionScam.name, - 'permissions/deny/collections', - { - name: collection, - description: 'Scam report', - }, - ); - } - - async clearCollectionScam(collection: string): Promise { - await this.doDelete( - this.clearCollectionScam.name, - `permissions/deny/collections/${collection}`, - ); - } - - private async getConfig(): Promise { - const accessTokenInfo = await this.nativeAuthSigner.getToken(); - return { - authorization: `Bearer ${accessTokenInfo.token}`, - timeout: 500, - }; - } - - private async doPost( - name: string, - resourceUrl: string, - body: any, - ): Promise { - try { - const config = await this.getConfig(); - const response = await this.apiService.post( - `${this.url}/${resourceUrl}`, - body, - config, - ); - return response.data; - } catch (error) { - this.logger.error(`Error when trying to get run ${name}`, { - error: error.message, - path: `${MxExtrasApiService.name}.${this.doPost.name}`, - }); - } - } - - private async doDelete(name: string, resourceUrl: string): Promise { - try { - const config = await this.getConfig(); - const response = await this.apiService.delete( - `${this.url}/${resourceUrl}`, - config, - {}, - ); - return response.data; - } catch (error) { - this.logger.error(`Error when trying to get run ${name}`, { - error: error.message, - path: `${MxExtrasApiService.name}.${this.doDelete.name}`, - }); - } - } -} diff --git a/src/common/services/mx-communication/mx-tools.service.ts b/src/common/services/mx-communication/mx-tools.service.ts deleted file mode 100644 index 10257f2f2..000000000 --- a/src/common/services/mx-communication/mx-tools.service.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { mxConfig } from 'src/config'; -import { NativeAuthSigner } from '@multiversx/sdk-nestjs/lib/src/utils/native.auth.signer'; -import BigNumber from 'bignumber.js'; -import { ApiService } from './api.service'; -import { ApiSettings } from './models/api-settings'; -import { getFilePathFromDist } from 'src/utils/helpers'; -import { ApiConfigService } from 'src/modules/common/api-config/api.config.service'; - -@Injectable() -export class MxToolsService { - private url: string; - private nativeAuthSigner: NativeAuthSigner; - - constructor( - private readonly logger: Logger, - private readonly apiConfigService: ApiConfigService, - private readonly apiService: ApiService, - ) { - this.url = this.apiConfigService.getToolsUrl(); - this.nativeAuthSigner = new NativeAuthSigner({ - origin: 'NftService', - apiUrl: this.apiConfigService.getApiUrl(), - signerPrivateKeyPath: getFilePathFromDist(mxConfig.pemFileName), - }); - } - - async getEgldHistoricalPrice( - isoDateOnly: string, - ): Promise { - return await this.getTokenPriceByTimestamp( - mxConfig.wegld, - mxConfig.usdc, - isoDateOnly, - ); - } - - async getTokenHistoricalPriceByEgld( - token: string, - isoDateOnly: string, - cachedEgldPriceUsd?: string, - ): Promise { - const priceInEgld = await this.getTokenPriceByTimestamp( - token, - mxConfig.wegld, - isoDateOnly, - ); - if (!priceInEgld) { - return; - } - const egldPriceUsd = - cachedEgldPriceUsd ?? (await this.getEgldHistoricalPrice(isoDateOnly)); - return new BigNumber(priceInEgld).multipliedBy(egldPriceUsd).toFixed(); - } - - private getTokenPriceByTimestampQuery( - firstToken: string, - secondToken: string, - isoDateOnly: string, - ): string { - return `query tokenUsdPrice { - trading { - pair(first_token: "${firstToken}", second_token: "${secondToken}") { - price(query: { date: "${isoDateOnly}"}) { - last - } - } - } - }`; - } - - private async getTokenPriceByTimestamp( - firstToken: string, - secondToken: string, - isoDateOnly: string, - ): Promise { - const query = this.getTokenPriceByTimestampQuery( - firstToken, - secondToken, - isoDateOnly, - ); - const res = await this.doPost(this.getTokenPriceByTimestamp.name, query); - return res?.data?.trading?.pair?.price?.[0]?.last?.toFixed(20) ?? undefined; - } - - private async getConfig(): Promise { - const accessTokenInfo = await this.nativeAuthSigner.getToken(); - return { - authorization: `Bearer ${accessTokenInfo.token}`, - timeout: 500, - }; - } - - private async doPost(name: string, query: any): Promise { - try { - const config = await this.getConfig(); - const response = await this.apiService.post(this.url, { query }, config); - return response.data; - } catch (error) { - this.logger.error(`Error when trying to get run ${name}`, { - error: error.message, - path: `${MxToolsService.name}.${this.doPost.name}`, - }); - } - } -} diff --git a/src/config/default.json b/src/config/default.json index d4fa29c7c..488c091f2 100644 --- a/src/config/default.json +++ b/src/config/default.json @@ -28,7 +28,7 @@ "cdnUrl": "https://ipfs.io/ipfs/" }, "gas": { - "nftCreate": 3000000, + "nftCreate": 3500000, "nftTransfer": 1000000, "startAuction": 18000000, "endAuction": 12000000, @@ -66,7 +66,10 @@ "elasticMaxBatch": 10000, "dbBatch": 1000, "complexityLevel": 200, - "getLogsFromElasticBatchSize": 1000 + "getLogsFromElasticBatchSize": 1000, + "dbMaxTimestamp": 2147483647, + "defaultPageOffset": 0, + "defaultPageSize": 10 }, "elasticDictionary": { "scamInfo": { @@ -83,8 +86,12 @@ "{size} magnificently designed and truly inspiring NFTs.", "An alluring collection of {size} wonderfully minted NFTs." ], - "forSmallCollections": [""], - "forAssets": [""] + "forSmallCollections": [ + "" + ], + "forAssets": [ + "" + ] }, "ports": { "rarity": 6013, diff --git a/src/db/auctions/auctions.repository.ts b/src/db/auctions/auctions.repository.ts index 8813003c7..5cdc38ecb 100644 --- a/src/db/auctions/auctions.repository.ts +++ b/src/db/auctions/auctions.repository.ts @@ -1,7 +1,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { PersistenceService } from 'src/common/persistence/persistence.service'; -import { mxConfig } from 'src/config'; +import { constants, mxConfig } from 'src/config'; import { AuctionStatusEnum } from 'src/modules/auctions/models/AuctionStatus.enum'; import { AuctionCustomEnum, @@ -424,6 +424,24 @@ export class AuctionsRepository { .getMany(); } + async getBulkAuctionsByAuctionIdsAndMarketplace( + auctionsIds: number[], + marketplaceKey: string, + ): Promise { + if (!auctionsIds || auctionsIds.length === 0) { + return; + } + return await this.auctionsRepository + .createQueryBuilder('auctions') + .where( + `marketplaceAuctionId IN(:...auctionsIds) and marketplaceKey='${marketplaceKey}'`, + { + auctionsIds: auctionsIds, + }, + ) + .getMany(); + } + async getAuctionByMarketplace( id: number, marketplaceKey: string, @@ -480,6 +498,21 @@ export class AuctionsRepository { ); } + async getBulkAuctionsByIdentifiersAndMarketplace( + identifiers: string[], + marketplaceKey: string, + ): Promise { + return await this.auctionsRepository + .createQueryBuilder('auctions') + .where( + `identifier IN(:...identifiers) and marketplaceKey='${marketplaceKey}'`, + { + identifiers: identifiers, + }, + ) + .getMany(); + } + async getAvailableTokensByAuctionId(id: number): Promise { return await this.auctionsRepository.query( getAvailableTokensbyAuctionId(id), @@ -541,6 +574,12 @@ export class AuctionsRepository { return await this.auctionsRepository.save(auction); } + async saveBulkAuctions(auctions: AuctionEntity[]): Promise { + await this.auctionsRepository.save(auctions, { + chunk: constants.dbBatch, + }); + } + async rollbackAuctionAndOrdersByHash(blockHash: string): Promise { const auctions = await this.getAuctionsForHash(blockHash); if (!auctions || auctions.length === 0) { diff --git a/src/db/auctions/tags.repository.ts b/src/db/auctions/tags.repository.ts index 71e555fe5..517f435e3 100644 --- a/src/db/auctions/tags.repository.ts +++ b/src/db/auctions/tags.repository.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { NftTag } from 'src/common/services/mx-communication/models'; +import { constants } from 'src/config'; import { AuctionStatusEnum } from 'src/modules/auctions/models'; import { MYSQL_ALREADY_EXISTS } from 'src/utils/constants'; import { Repository } from 'typeorm'; @@ -75,7 +76,7 @@ export class TagsRepository { async saveTags(tags: TagEntity[]): Promise { try { - return await this.tagsRepository.save(tags); + return await this.tagsRepository.save(tags, { chunk: constants.dbBatch }); } catch (err) { // If like already exists, we ignore the error. if (err.errno === MYSQL_ALREADY_EXISTS) { diff --git a/src/db/featuredNfts/featured.repository.ts b/src/db/featuredNfts/featured.repository.ts index 99830e02d..c0b629975 100644 --- a/src/db/featuredNfts/featured.repository.ts +++ b/src/db/featuredNfts/featured.repository.ts @@ -10,7 +10,7 @@ export class FeaturedNftsRepository { constructor( @InjectRepository(FeaturedNftEntity) private featuredNftsRepository: Repository, - ) {} + ) { } async getFeaturedNfts( limit: number = 20, offset: number = 0, @@ -33,7 +33,8 @@ export class FeaturedCollectionsRepository { constructor( @InjectRepository(FeaturedCollectionEntity) private featuredCollectionsRepository: Repository, - ) {} + ) { } + async getFeaturedCollections( limit: number = 20, offset: number = 0, @@ -61,9 +62,19 @@ export class FeaturedCollectionsRepository { collection: string, type: FeaturedCollectionTypeEnum, ): Promise { + if (!collection) return false const res = await this.featuredCollectionsRepository.save( new FeaturedCollectionEntity({ identifier: collection, type }), ); return !!res.id; } + + async getFeaturedCollectionsByIdentifiers(identifiers: string[]): Promise { + return await this.featuredCollectionsRepository + .createQueryBuilder('featuredCollections') + .where('identifier IN(:...identifiers)', { + identifiers: identifiers, + }) + .getMany(); + } } diff --git a/src/db/marketplaces/marketplace-events.entity.ts b/src/db/marketplaces/marketplace-events.entity.ts index 6cff1a819..8882c6900 100644 --- a/src/db/marketplaces/marketplace-events.entity.ts +++ b/src/db/marketplaces/marketplace-events.entity.ts @@ -33,4 +33,28 @@ export class MarketplaceEventsEntity extends BaseEntity { super(); Object.assign(this, init); } + + hasEventIdentifier(eventType: string): boolean { + return eventType === this.getEventIdentifier(); + } + + hasOneOfEventIdentifiers(eventType: string[]): boolean { + return eventType.includes(this.getEventIdentifier()); + } + + hasEventTopicIdentifier(eventType: string): boolean { + return eventType === this.getEventTopicIdentifier(); + } + + hasOneOfEventTopicIdentifiers(eventType: string[]): boolean { + return eventType.includes(this.getEventTopicIdentifier()); + } + + getEventIdentifier(): string { + return this.data?.eventData?.identifier; + } + + getEventTopicIdentifier(): string { + return Buffer.from(this.data?.eventData?.topics?.[0], 'base64').toString(); + } } diff --git a/src/db/offers/offers.repository.ts b/src/db/offers/offers.repository.ts index 479eb00d8..52cb89026 100644 --- a/src/db/offers/offers.repository.ts +++ b/src/db/offers/offers.repository.ts @@ -10,6 +10,7 @@ import { import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DateUtils } from 'src/utils/date-utils'; +import { constants } from 'src/config'; @Injectable() export class OffersRepository { @@ -70,6 +71,12 @@ export class OffersRepository { return savedOffer; } + async saveBulkOffers(orders: OfferEntity[]): Promise { + await this.offersRepository.save(orders, { + chunk: constants.dbBatch, + }); + } + async updateOfferWithStatus(offer: OfferEntity, status: OfferStatusEnum) { offer.status = status; offer.modifiedDate = new Date(new Date().toUTCString()); @@ -84,6 +91,21 @@ export class OffersRepository { return updatedOffers; } + async getBulkOffersByOfferIdsAndMarketplace( + offerIds: number[], + marketplaceKey: string, + ): Promise { + return await this.offersRepository + .createQueryBuilder('offers') + .where( + `marketplaceOfferId IN(:...offerIds) and marketplaceKey='${marketplaceKey}'`, + { + offerIds: offerIds, + }, + ) + .getMany(); + } + async rollbackOffersByHash(blockHash: string) { const offersByHash = await this.getOffersByBlockHash(blockHash); if (!offersByHash || offersByHash.length === 0) { diff --git a/src/db/orders/orders.repository.ts b/src/db/orders/orders.repository.ts index c2229256f..bf71f957e 100644 --- a/src/db/orders/orders.repository.ts +++ b/src/db/orders/orders.repository.ts @@ -7,6 +7,7 @@ import { Sorting, Sort } from 'src/modules/common/filters/filtersTypes'; import { getOrdersForAuctions } from '../auctions/sql.queries'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { constants } from 'src/config'; @Injectable() export class OrdersRepository { @@ -98,6 +99,12 @@ export class OrdersRepository { return await this.ordersRepository.save(order); } + async saveBulkOrders(orders: OrderEntity[]): Promise { + await this.ordersRepository.save(orders, { + chunk: constants.dbBatch, + }); + } + async updateOrderWithStatus(order: OrderEntity, status: OrderStatusEnum) { order.status = status; order.modifiedDate = new Date(new Date().toUTCString()); diff --git a/src/document-db/document-db.module.ts b/src/document-db/document-db.module.ts index 1bb6dce16..12f9302ba 100644 --- a/src/document-db/document-db.module.ts +++ b/src/document-db/document-db.module.ts @@ -14,6 +14,11 @@ import { import { NftScamInfoRepositoryService } from 'src/document-db/repositories/nft-scam.repository'; import { DocumentDbService } from './document-db.service'; import { ApiConfigService } from 'src/modules/common/api-config/api.config.service'; +import { CollectionScamInfoRepositoryService } from './repositories/collection-scam.repository'; +import { + CollectionScamInfoModel, + CollectionScamInfoSchema, +} from 'src/modules/scam/models/collection-scam-info.model'; @Global() @Module({ @@ -41,11 +46,18 @@ import { ApiConfigService } from 'src/modules/common/api-config/api.config.servi schema: NftScamInfoSchema, }, ]), + MongooseModule.forFeature([ + { + name: CollectionScamInfoModel.name, + schema: CollectionScamInfoSchema, + }, + ]), ], providers: [ DocumentDbService, TraitRepositoryService, NftScamInfoRepositoryService, + CollectionScamInfoRepositoryService, ], exports: [DocumentDbService], }) diff --git a/src/document-db/document-db.service.ts b/src/document-db/document-db.service.ts index c39394c98..b0d8012b5 100644 --- a/src/document-db/document-db.service.ts +++ b/src/document-db/document-db.service.ts @@ -1,18 +1,21 @@ import { PerformanceProfiler } from '@multiversx/sdk-nestjs'; import { Injectable } from '@nestjs/common'; -import { Nft } from 'src/common'; import { ScamInfo } from 'src/modules/assets/models/ScamInfo.dto'; import { MetricsCollector } from 'src/modules/metrics/metrics.collector'; import { NftScamInfoModel } from 'src/modules/scam/models/nft-scam-info.model'; import { CollectionTraitSummary } from 'src/modules/nft-traits/models/collection-traits.model'; import { NftScamInfoRepositoryService } from './repositories/nft-scam.repository'; import { TraitRepositoryService } from './repositories/traits.repository'; +import { Asset } from 'src/modules/assets/models'; +import { CollectionScamInfoRepositoryService } from './repositories/collection-scam.repository'; +import { CollectionScamInfoModel } from 'src/modules/scam/models/collection-scam-info.model'; @Injectable() export class DocumentDbService { constructor( private readonly traitRepositoryService: TraitRepositoryService, private readonly nftScamInfoRepositoryService: NftScamInfoRepositoryService, + private readonly collectionScamInfoRepositoryService: CollectionScamInfoRepositoryService, ) {} private async execute(key: string, action: Promise): Promise { @@ -73,7 +76,7 @@ export class DocumentDbService { } async saveOrUpdateBulkNftScamInfo( - nfts: Nft[], + nfts: Asset[], version: string, ): Promise { return await this.execute( @@ -107,4 +110,39 @@ export class DocumentDbService { this.nftScamInfoRepositoryService.getNftScamInfo(identifier), ); } + + async saveOrUpdateCollectionScamInfo( + collection: string, + version: string, + scamInfo?: ScamInfo, + ): Promise { + return await this.execute( + this.saveOrUpdateCollectionScamInfo.name, + this.collectionScamInfoRepositoryService.saveOrUpdateCollectionScamInfo( + collection, + version, + scamInfo, + ), + ); + } + + async deleteCollectionScamInfo(collection: string): Promise { + return await this.execute( + this.deleteCollectionScamInfo.name, + this.collectionScamInfoRepositoryService.deleteCollectionScamInfo( + collection, + ), + ); + } + + async getCollectionScamInfo( + collection: string, + ): Promise { + return await this.execute( + this.getCollectionScamInfo.name, + this.collectionScamInfoRepositoryService.getCollectionScamInfo( + collection, + ), + ); + } } diff --git a/src/document-db/repositories/collection-scam.repository.ts b/src/document-db/repositories/collection-scam.repository.ts new file mode 100644 index 000000000..5613d60b0 --- /dev/null +++ b/src/document-db/repositories/collection-scam.repository.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { EntityRepository } from './entity.repository'; +import { ScamInfo } from 'src/modules/assets/models/ScamInfo.dto'; +import { + CollectionScamInfoDocument, + CollectionScamInfoModel, +} from 'src/modules/scam/models/collection-scam-info.model'; + +@Injectable() +export class CollectionScamInfoRepositoryService extends EntityRepository { + constructor( + @InjectModel(CollectionScamInfoModel.name) + private readonly collectionScamInfoModel: Model, + ) { + super(collectionScamInfoModel); + } + + async saveOrUpdateCollectionScamInfo( + collection: string, + version: string, + scamInfo?: ScamInfo, + ): Promise { + let doc: CollectionScamInfoModel = { + collectionIdentifier: collection, + version: version, + }; + if (scamInfo) { + doc = { ...doc, type: scamInfo.type, info: scamInfo.info }; + } + const res = await this.findOneAndReplace( + { + collectionIdentifier: collection, + }, + doc, + ); + if (!res) { + await this.create(doc); + } + } + + async deleteCollectionScamInfo(collection: string): Promise { + await this.findOneAndDelete({ + collectionIdentifier: collection, + }); + } + + async getCollectionScamInfo( + collection: string, + ): Promise { + return await this.findOne({ + collectionIdentifier: collection, + }); + } +} diff --git a/src/document-db/repositories/nft-scam.repository.ts b/src/document-db/repositories/nft-scam.repository.ts index 2483a593f..3b6ae3b24 100644 --- a/src/document-db/repositories/nft-scam.repository.ts +++ b/src/document-db/repositories/nft-scam.repository.ts @@ -6,8 +6,8 @@ import { NftScamInfoModel, NftScamInfoDocument, } from 'src/modules/scam/models/nft-scam-info.model'; -import { Nft } from 'src/common'; import { ScamInfo } from 'src/modules/assets/models/ScamInfo.dto'; +import { Asset } from 'src/modules/assets/models'; @Injectable() export class NftScamInfoRepositoryService extends EntityRepository { @@ -42,7 +42,7 @@ export class NftScamInfoRepositoryService extends EntityRepository { if (!nfts || nfts.length === 0) { @@ -70,7 +70,7 @@ export class NftScamInfoRepositoryService extends EntityRepository { const updates: any = nfts.map((nft) => { diff --git a/src/modules/admins/admin-operations.resolver.ts b/src/modules/admins/admin-operations.resolver.ts index 39216037e..4efab5278 100644 --- a/src/modules/admins/admin-operations.resolver.ts +++ b/src/modules/admins/admin-operations.resolver.ts @@ -1,5 +1,5 @@ import { Resolver, Args, Mutation } from '@nestjs/graphql'; -import { UseGuards } from '@nestjs/common'; +import { Logger, UseGuards } from '@nestjs/common'; import { GqlAdminAuthGuard } from '../auth/gql-admin.auth-guard'; import { FlagNftService } from './flag-nft.service'; import { FlagCollectionInput, FlagNftInput } from './models/flag-nft.input'; @@ -16,21 +16,25 @@ import { CacheEventTypeEnum, ChangedEvent, } from '../rabbitmq/cache-invalidation/events/changed.event'; +import { MarketplacesReindexService } from '../marketplaces/marketplaces-reindex.service'; import { ReportsService } from '../reports/reports.service'; import { ClearReportCollectionInput, ClearReportInput, } from './models/clear-report.input'; +import { MarketplaceReindexDataArgs } from '../marketplaces/models/MarketplaceReindexDataArgs'; @Resolver(() => Boolean) export class AdminOperationsResolver { constructor( + private readonly logger: Logger, private readonly flagService: FlagNftService, private reportNfts: ReportsService, private readonly nftRarityService: NftRarityService, private readonly nftTraitService: NftTraitsService, private readonly cacheEventsPublisherService: CacheEventsPublisherService, private readonly marketplaceEventsIndexingService: MarketplaceEventsIndexingService, + private readonly marketplacesReindexService: MarketplacesReindexService, ) {} @Mutation(() => Boolean) @@ -154,15 +158,29 @@ export class AdminOperationsResolver { @Args('input') input: MarketplaceEventsIndexingArgs, ): Promise { - try { - await this.marketplaceEventsIndexingService.reindexMarketplaceEvents( + this.marketplaceEventsIndexingService + .reindexMarketplaceEvents( MarketplaceEventsIndexingRequest.fromMarketplaceEventsIndexingArgs( input, ), - ); - return true; - } catch (error) { - throw new ApolloError(error); - } + ) + .catch((error) => { + this.logger.error(error); + }); + return true; + } + + @Mutation(() => Boolean) + @UseGuards(JwtOrNativeAuthGuard, GqlAdminAuthGuard) + async reindexMarketplaceData( + @Args('input') + input: MarketplaceReindexDataArgs, + ): Promise { + this.marketplacesReindexService + .reindexMarketplaceData(input) + .catch((error) => { + this.logger.error(error); + }); + return true; } } diff --git a/src/modules/asset-history/assets-history-caching.service.ts b/src/modules/asset-history/assets-history-caching.service.ts index 6efc442a9..00cd2c7ed 100644 --- a/src/modules/asset-history/assets-history-caching.service.ts +++ b/src/modules/asset-history/assets-history-caching.service.ts @@ -3,7 +3,6 @@ import { RedisCacheService } from '@multiversx/sdk-nestjs'; import { AssetHistoryLog } from './models'; import { generateCacheKeyFromParams } from 'src/utils/generate-cache-key'; import { CacheInfo } from 'src/common/services/caching/entities/cache.info'; -import { getCollectionAndNonceFromIdentifier } from 'src/utils/helpers'; @Injectable() export class AssetsHistoryCachingService { @@ -29,17 +28,6 @@ export class AssetsHistoryCachingService { ); } - async invalidateCache(identifier: string): Promise { - const { collection, nonce } = - getCollectionAndNonceFromIdentifier(identifier); - const cacheKeyPattern = generateCacheKeyFromParams( - CacheInfo.AssetHistory.key, - collection, - nonce, - ); - await this.redisCacheService.deleteByPattern(`${cacheKeyPattern}*`); - } - getAssetHistoryCacheKey( collection: string, nonce: string, diff --git a/src/modules/assets/assets-getter.service.ts b/src/modules/assets/assets-getter.service.ts index 420f7a1ce..72951a0cb 100644 --- a/src/modules/assets/assets-getter.service.ts +++ b/src/modules/assets/assets-getter.service.ts @@ -8,7 +8,6 @@ import { generateCacheKeyFromParams } from 'src/utils/generate-cache-key'; import { AssetScamInfoProvider } from './loaders/assets-scam-info.loader'; import { AssetsSupplyLoader } from './loaders/assets-supply.loader'; import { AssetsFilter, Sort } from '../common/filters/filtersTypes'; - import { AssetRarityInfoProvider } from './loaders/assets-rarity-info.loader'; import { AssetByIdentifierService } from './asset-by-identifier.service'; import * as hash from 'object-hash'; @@ -26,12 +25,17 @@ import { RedisCacheService, } from '@multiversx/sdk-nestjs'; import { QueryPagination } from 'src/common/services/mx-communication/models/query-pagination'; +import { FeaturedService } from '../featured/featured.service'; +import { FeaturedCollectionsFilter } from '../featured/Featured-Collections.Filter'; +import { FeaturedCollectionTypeEnum } from '../featured/FeatureCollectionType.enum'; +import { constants } from 'src/config'; @Injectable() export class AssetsGetterService { constructor( private apiService: MxApiService, private collectionsService: CollectionsGetterService, + private featuredCollectionsService: FeaturedService, private elasticService: MxElasticService, private assetByIdentifierService: AssetByIdentifierService, private assetScamLoader: AssetScamInfoProvider, @@ -41,7 +45,7 @@ export class AssetsGetterService { private nftTraitsService: NftTraitsService, private readonly logger: Logger, private redisCacheService: RedisCacheService, - ) {} + ) { } async getAssetsForUser( address: string, @@ -58,13 +62,14 @@ export class AssetsGetterService { this.apiService.getNftsForUser(address, query), this.apiService.getNftsForUserCount(address, countQuery), ]); + const assets = nfts?.map((element) => Asset.fromNft(element, address)); return new CollectionType({ count, items: assets }); } async getAssets( - offset: number = 0, - limit: number = 10, + offset: number = constants.defaultPageOffset, + limit: number = constants.defaultPageSize, filters: AssetsFilter, sorting: AssetsSortingEnum, ): Promise> { @@ -73,6 +78,18 @@ export class AssetsGetterService { : this.getApiQuery(filters, offset, limit); const apiCountQuery = this.getApiQueryForCount(filters); + if (filters.ownerAddress && filters.customFilters) { + const [ticketsCollections] = await this.featuredCollectionsService.getFeaturedCollections(new FeaturedCollectionsFilter({ type: FeaturedCollectionTypeEnum.Tickets })) + if (!ticketsCollections || ticketsCollections?.length === 0) return new CollectionType() + + const ticketCollectionIdentifiers = ticketsCollections.map(x => x.collection).toString(); + return await this.getAssetsForUser( + filters.ownerAddress, + `?collections=${ticketCollectionIdentifiers}&from=${offset}&size=${limit}`, + `?collections=${ticketCollectionIdentifiers}`, + ); + } + if (sorting === AssetsSortingEnum.MostLikes) { const assets = await this.assetsLikedService.getMostLikedAssets(); return new CollectionType({ diff --git a/src/modules/assets/assets-mutations.resolver.ts b/src/modules/assets/assets-mutations.resolver.ts index 4ac4a45f4..67b1f17d4 100644 --- a/src/modules/assets/assets-mutations.resolver.ts +++ b/src/modules/assets/assets-mutations.resolver.ts @@ -18,6 +18,7 @@ import { CreateNftRequest, UpdateQuantityRequest, TransferNftRequest, + CreateNftWithMultipleFilesRequest, } from './models/requests'; import { AuthorizationHeader } from '../auth/authorization-header'; import { JwtOrNativeAuthGuard } from '../auth/jwt.or.native.auth-guard'; @@ -45,6 +46,20 @@ export class AssetsMutationsResolver extends BaseResolver(Asset) { return await this.assetsTransactionService.createNft(user.address, request); } + @Mutation(() => TransactionNode) + @UseGuards(JwtOrNativeAuthGuard) + async createNftWithMultipleFiles( + @Args('input', { type: () => CreateNftArgs }) input: CreateNftArgs, + @Args({ name: 'files', type: () => [GraphQLUpload] }) files: [FileUpload], + @AuthUser() user: UserAuthResult, + ): Promise { + const request = CreateNftWithMultipleFilesRequest.fromArgs(input, files); + return await this.assetsTransactionService.createNftWithMultipleFiles( + user.address, + request, + ); + } + @Mutation(() => Boolean) @UseGuards(JwtOrNativeAuthGuard) async verifyContent( diff --git a/src/modules/assets/assets-queries.resolver.ts b/src/modules/assets/assets-queries.resolver.ts index 9f97af4b9..e4c7b90a1 100644 --- a/src/modules/assets/assets-queries.resolver.ts +++ b/src/modules/assets/assets-queries.resolver.ts @@ -36,6 +36,7 @@ import { ArtistAddressProvider } from '../artists/artists.loader'; import { AssetsSortingEnum } from './models/Assets-Sorting.enum'; import { randomBetween } from 'src/utils/helpers'; import { ScamInfo } from './models/ScamInfo.dto'; +import { IsTicketProvider } from './loaders/asset-is-ticket.loader'; @Resolver(() => Asset) export class AssetsQueriesResolver extends BaseResolver(Asset) { @@ -55,6 +56,7 @@ export class AssetsQueriesResolver extends BaseResolver(Asset) { private assetRarityProvider: AssetRarityInfoProvider, private marketplaceProvider: FeaturedMarketplaceProvider, private internalMarketplaceProvider: InternalMarketplaceProvider, + private isTicketProvider: IsTicketProvider, ) { super(); } @@ -146,7 +148,9 @@ export class AssetsQueriesResolver extends BaseResolver(Asset) { const { identifier } = asset; const scamInfo = await this.assetScamProvider.load(identifier); const scamInfoValue = scamInfo.value; - return scamInfoValue && Object.keys(scamInfoValue).length > 1 + return scamInfoValue && + Object.keys(scamInfoValue).length > 1 && + ScamInfo.isScam(scamInfoValue) ? scamInfoValue : null; } @@ -255,6 +259,13 @@ export class AssetsQueriesResolver extends BaseResolver(Asset) { return asset.metadata; } + @ResolveField('isTicket', () => Boolean) + async isTicket(@Parent() asset: Asset) { + const { collection } = asset; + const isAssetTicket = await this.isTicketProvider.load(collection); + return isAssetTicket?.value ?? false; + } + private async getMarketplaceForNft( ownerAddress: string, collection: string, diff --git a/src/modules/assets/assets-query.ts b/src/modules/assets/assets-query.ts index f99bcfb86..a49f10155 100644 --- a/src/modules/assets/assets-query.ts +++ b/src/modules/assets/assets-query.ts @@ -126,13 +126,6 @@ export class AssetsQuery { return this; } - addComputeScamInfo(computeScamInfo: boolean): this { - if (!computeScamInfo) { - return this; - } - return this.addParamToQuery('computeScamInfo', true); - } - build(addDefaultQuery: boolean = true): string { // TODO(whiteListedStorage): handle whitelisting in a different way // then uncomment where TODO(whiteListedStorage) diff --git a/src/modules/assets/assets-transaction.service.ts b/src/modules/assets/assets-transaction.service.ts index 39859b491..dde9c4b57 100644 --- a/src/modules/assets/assets-transaction.service.ts +++ b/src/modules/assets/assets-transaction.service.ts @@ -24,11 +24,13 @@ import { UpdateQuantityRequest, CreateNftRequest, TransferNftRequest, + CreateNftWithMultipleFilesRequest, } from './models/requests'; import { generateCacheKeyFromParams } from 'src/utils/generate-cache-key'; - +import { FileUpload } from 'graphql-upload'; import { MxStats } from 'src/common/services/mx-communication/models/mx-stats.model'; import { Constants, RedisCacheService } from '@multiversx/sdk-nestjs'; +import { UploadToIpfsResult } from '../ipfs/ipfs.model'; @Injectable() export class AssetsTransactionService { @@ -97,45 +99,26 @@ export class AssetsTransactionService { ); } - async createNft( + async createNftWithMultipleFiles( ownerAddress: string, - request: CreateNftRequest, + request: CreateNftWithMultipleFilesRequest, ): Promise { - const file = await request.file; - const fileData = await this.pinataService.uploadFile(file); - const fileMetadata = new FileContent({ - description: request.attributes.description, - }); - const assetMetadata = await this.pinataService.uploadText(fileMetadata); - - await this.s3Service.upload(file, fileData.hash); - await this.s3Service.uploadText(fileMetadata, assetMetadata.hash); + let uploadFilePromises = []; + for (const file of request.files) { + uploadFilePromises.push(this.uploadFileToPinata(file)); + } + const filesToIpfs = await Promise.all(uploadFilePromises); - const attributes = `tags:${request.attributes.tags};metadata:${assetMetadata.hash}`; + return this.getCreateNftTransaction(ownerAddress, request, filesToIpfs); + } - const contract = getSmartContract(ownerAddress); - const transaction = contract.call({ - func: new ContractFunction('ESDTNFTCreate'), - value: TokenPayment.egldFromAmount(0), - args: [ - BytesValue.fromUTF8(request.collection), - new U64Value(new BigNumber(request.quantity)), - BytesValue.fromUTF8(request.name), - BytesValue.fromHex(nominateVal(parseFloat(request.royalties))), - BytesValue.fromUTF8(fileData.hash), - BytesValue.fromUTF8(attributes), - BytesValue.fromUTF8(fileData.url), - ], - gasLimit: gas.nftCreate, - chainID: mxConfig.chainID, - }); - let response = transaction.toPlainObject(new Address(ownerAddress)); + async createNft( + ownerAddress: string, + request: CreateNftRequest, + ): Promise { + const fileData = await this.uploadFileToPinata(request.file); - return { - ...response, - gasLimit: gas.nftCreate + response.data.length * mxConfig.pricePerByte, - chainID: mxConfig.chainID, - }; + return this.getCreateNftTransaction(ownerAddress, request, [fileData]); } async transferNft( @@ -169,6 +152,68 @@ export class AssetsTransactionService { }; } + private async getCreateNftTransaction( + ownerAddress: string, + request: CreateNftRequest | CreateNftWithMultipleFilesRequest, + filesData: UploadToIpfsResult[], + ) { + const assetMetadata = await this.uploadFileMetadata( + request.attributes.description, + ); + const attributes = `tags:${request.attributes.tags};metadata:${assetMetadata.hash}`; + const contract = getSmartContract(ownerAddress); + const transaction = contract.call({ + func: new ContractFunction('ESDTNFTCreate'), + value: TokenPayment.egldFromAmount(0), + args: this.getCreateNftsArgs(request, filesData, attributes), + gasLimit: gas.nftCreate, + chainID: mxConfig.chainID, + }); + let response = transaction.toPlainObject(new Address(ownerAddress)); + + return { + ...response, + gasLimit: gas.nftCreate + response.data.length * mxConfig.pricePerByte, + chainID: mxConfig.chainID, + }; + } + + private async uploadFileMetadata(description: string): Promise { + const fileMetadata = new FileContent({ + description: description, + }); + const assetMetadata = await this.pinataService.uploadText(fileMetadata); + + await this.s3Service.uploadText(fileMetadata, assetMetadata.hash); + return assetMetadata; + } + + private async uploadFileToPinata(fileUpload: FileUpload) { + const file = await fileUpload; + const fileData = await this.pinataService.uploadFile(file); + await this.s3Service.upload(file, fileData.hash); + return fileData; + } + + private getCreateNftsArgs( + request: CreateNftWithMultipleFilesRequest | CreateNftRequest, + filesToIpfs: any[], + attributes: string, + ) { + const args = [ + BytesValue.fromUTF8(request.collection), + new U64Value(new BigNumber(request.quantity)), + BytesValue.fromUTF8(request.name), + BytesValue.fromHex(nominateVal(parseFloat(request.royalties))), + BytesValue.fromUTF8(filesToIpfs[0].hash), + BytesValue.fromUTF8(attributes), + ]; + for (const file of filesToIpfs) { + args.push(BytesValue.fromUTF8(file.url)); + } + return args; + } + private async getOrSetAproximateMxStats(): Promise { try { const cacheKey = this.getApproximateMxStatsCacheKey(); diff --git a/src/modules/assets/assets.module.ts b/src/modules/assets/assets.module.ts index 3fe00874b..b4b621d7e 100644 --- a/src/modules/assets/assets.module.ts +++ b/src/modules/assets/assets.module.ts @@ -52,6 +52,9 @@ import { NftTraitsService } from '../nft-traits/nft-traits.service'; import { CollectionsModuleGraph } from '../nftCollections/collections.module'; import { NftTraitsElasticService } from '../nft-traits/nft-traits.elastic.service'; import { AuthModule } from '../auth/auth.module'; +import { FeaturedModuleGraph } from '../featured/featured.module'; +import { IsTicketProvider } from './loaders/asset-is-ticket.loader'; +import { IsTicketRedisHandler } from './loaders/asset-is-ticket.redis-handler'; @Module({ providers: [ @@ -100,12 +103,15 @@ import { AuthModule } from '../auth/auth.module'; InternalMarketplaceRedisHandler, NftTraitsService, NftTraitsElasticService, + IsTicketProvider, + IsTicketRedisHandler ], imports: [ MxCommunicationModule, CommonModule, forwardRef(() => AuctionsModuleGraph), forwardRef(() => CollectionsModuleGraph), + forwardRef(() => FeaturedModuleGraph), forwardRef(() => AuthModule), IpfsModule, PersistenceModule, @@ -124,6 +130,8 @@ import { AuthModule } from '../auth/auth.module'; AssetScamInfoProvider, AssetsRedisHandler, AssetsProvider, + IsTicketProvider, + IsTicketRedisHandler ], }) -export class AssetsModuleGraph {} +export class AssetsModuleGraph { } diff --git a/src/modules/assets/loaders/asset-is-ticket.loader.ts b/src/modules/assets/loaders/asset-is-ticket.loader.ts new file mode 100644 index 000000000..b0304aef5 --- /dev/null +++ b/src/modules/assets/loaders/asset-is-ticket.loader.ts @@ -0,0 +1,27 @@ +import DataLoader = require('dataloader'); +import { BaseProvider } from '../../common/base.loader'; +import { Injectable, Scope } from '@nestjs/common'; +import { PersistenceService } from 'src/common/persistence/persistence.service'; +import { IsTicketRedisHandler } from './asset-is-ticket.redis-handler'; + +@Injectable({ + scope: Scope.REQUEST, +}) +export class IsTicketProvider extends BaseProvider { + constructor( + isTicketRedisHandler: IsTicketRedisHandler, + private persistenceService: PersistenceService, + ) { + super( + isTicketRedisHandler, + new DataLoader(async (keys: string[]) => await this.batchLoad(keys)), + ); + } + + async getData(identifiers: string[]) { + const assetTickets = await this.persistenceService.getFeaturedCollectionsByIdentifiers( + identifiers, + ); + return assetTickets?.groupBy((collection) => collection.identifier); + } +} diff --git a/src/modules/assets/loaders/asset-is-ticket.redis-handler.ts b/src/modules/assets/loaders/asset-is-ticket.redis-handler.ts new file mode 100644 index 000000000..548d4de94 --- /dev/null +++ b/src/modules/assets/loaders/asset-is-ticket.redis-handler.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { Constants, RedisCacheService } from '@multiversx/sdk-nestjs'; +import { RedisKeyValueDataloaderHandler } from 'src/modules/common/redis-key-value-dataloader.handler'; +import { RedisValue } from 'src/modules/common/redis-value.dto'; + +@Injectable() +export class IsTicketRedisHandler extends RedisKeyValueDataloaderHandler { + constructor(redisCacheService: RedisCacheService) { + super(redisCacheService, 'isTicket'); + } + + mapValues( + returnValues: { key: string; value: any }[], + collectionIdentifiers: { [key: string]: any[] }, + ) { + const redisValues = []; + for (const item of returnValues) { + if (item.value === null) { + item.value = collectionIdentifiers[item.key] + ? true + : false; + redisValues.push(item); + } + } + + return [ + new RedisValue({ + values: redisValues, + ttl: Constants.oneWeek(), + }), + ]; + } +} diff --git a/src/modules/assets/models/Asset.dto.ts b/src/modules/assets/models/Asset.dto.ts index b22a37f68..0cb8f5056 100644 --- a/src/modules/assets/models/Asset.dto.ts +++ b/src/modules/assets/models/Asset.dto.ts @@ -97,6 +97,8 @@ export class Asset { rarity: Rarity; @Field(() => CollectionBranding, { nullable: true }) branding: CollectionBranding; + @Field(() => Boolean, { nullable: true }) + isTicket: boolean; constructor(init?: Partial) { Object.assign(this, init); @@ -105,30 +107,30 @@ export class Asset { static fromNft(nft: Nft, address: string = null) { return nft ? new Asset({ - collection: nft.collection, - type: NftTypeEnum[nft.type], - nonce: nft.nonce ?? 0, - identifier: nft.identifier, - creatorAddress: nft.creator ?? '', - ownerAddress: nft.owner ? nft.owner : address, - attributes: nft.attributes ?? '', - creationDate: nft.timestamp, - hash: nft.hash ?? '', - balance: nft.balance, - supply: nft.supply, - name: nft.name, - royalties: nft.royalties ?? '', - rarity: Rarity.fromNftRarity(nft), - uris: nft.uris || [''], - metadata: Metadata.fromNftMetadata(nft.metadata), - tags: nft.tags, - isWhitelistedStorage: nft.isWhitelistedStorage, - isNsfw: nft.isNsfw, - scamInfo: ScamInfo.fromScamInfoApi(nft.scamInfo), - media: nft.media?.map((m) => Media.fromNftMedia(m)), - verified: !!nft.assets ?? false, - branding: CollectionBranding.fromNftAssets(nft.assets), - }) + collection: nft.collection, + type: NftTypeEnum[nft.type], + nonce: nft.nonce ?? 0, + identifier: nft.identifier, + creatorAddress: nft.creator ?? '', + ownerAddress: nft.owner ? nft.owner : address, + attributes: nft.attributes ?? '', + creationDate: nft.timestamp, + hash: nft.hash ?? '', + balance: nft.balance, + supply: nft.supply, + name: nft.name, + royalties: nft.royalties ?? '', + rarity: Rarity.fromNftRarity(nft), + uris: nft.uris || [''], + metadata: Metadata.fromNftMetadata(nft.metadata), + tags: nft.tags, + isWhitelistedStorage: nft.isWhitelistedStorage, + isNsfw: nft.isNsfw, + scamInfo: ScamInfo.fromScamInfoApi(nft.scamInfo), + media: nft.media?.map((m) => Media.fromNftMedia(m)), + verified: !!nft.assets ?? false, + branding: CollectionBranding.fromNftAssets(nft.assets), + }) : null; } } diff --git a/src/modules/assets/models/AssetAction.enum.ts b/src/modules/assets/models/AssetAction.enum.ts index 4bc50a969..f6b9049ac 100644 --- a/src/modules/assets/models/AssetAction.enum.ts +++ b/src/modules/assets/models/AssetAction.enum.ts @@ -8,6 +8,9 @@ export enum AssetActionEnum { EndedAuction = 'EndedAuction', ClosedAuction = 'ClosedAuction', Bought = 'Bought', + Bid = 'Bid', + PriceUpdated = 'PriceUpdated', + Updated = 'Updated', } registerEnumType(AssetActionEnum, { diff --git a/src/modules/assets/models/AssetOfferEnum.ts b/src/modules/assets/models/AssetOfferEnum.ts new file mode 100644 index 000000000..146913a1d --- /dev/null +++ b/src/modules/assets/models/AssetOfferEnum.ts @@ -0,0 +1,12 @@ +import { registerEnumType } from '@nestjs/graphql'; + +export enum AssetOfferEnum { + Created = 'OfferCreated', + Accepted = 'OfferAccepted', + Closed = 'OfferClosed', + GloballyAccepted = 'GloballyAccepted', +} + +registerEnumType(AssetOfferEnum, { + name: 'AssetOfferEnum', +}); diff --git a/src/modules/assets/models/NftTypes.enum.ts b/src/modules/assets/models/NftTypes.enum.ts index 88d62ef0e..eadc2ce4d 100644 --- a/src/modules/assets/models/NftTypes.enum.ts +++ b/src/modules/assets/models/NftTypes.enum.ts @@ -10,6 +10,7 @@ registerEnumType(NftTypeEnum, { }); export enum ScamInfoTypeEnum { + none = 'none', potentialScam = 'potentialScam', scam = 'scam', } @@ -17,3 +18,12 @@ export enum ScamInfoTypeEnum { registerEnumType(ScamInfoTypeEnum, { name: 'ScamInfoTypeEnum', }); + + +export enum CustomFiltersEnum { + Tickets = 'Tickets', +} + +registerEnumType(CustomFiltersEnum, { + name: 'CustomFiltersEnum', +}); diff --git a/src/modules/assets/models/Price.dto.ts b/src/modules/assets/models/Price.dto.ts index df52648dc..28e921933 100644 --- a/src/modules/assets/models/Price.dto.ts +++ b/src/modules/assets/models/Price.dto.ts @@ -1,8 +1,8 @@ import { Field, ID, Int, ObjectType } from '@nestjs/graphql'; -import { Token } from 'src/common/services/mx-communication/models/Token.model'; import { mxConfig } from 'src/config'; import { OrderEntity } from 'src/db/orders'; import { DateUtils } from 'src/utils/date-utils'; +import { Token } from '../../usdPrice/Token.model'; @ObjectType() export class Price { diff --git a/src/modules/assets/models/ScamInfo.dto.ts b/src/modules/assets/models/ScamInfo.dto.ts index 182e4ee09..10b5a74f1 100644 --- a/src/modules/assets/models/ScamInfo.dto.ts +++ b/src/modules/assets/models/ScamInfo.dto.ts @@ -1,9 +1,8 @@ import { ObjectType, Field } from '@nestjs/graphql'; -import { Nft } from 'src/common'; import { ScamInfoApi } from 'src/common/services/mx-communication/models/scam-info.dto'; import { elasticDictionary } from 'src/config'; import { NftScamInfoModel } from 'src/modules/scam/models/nft-scam-info.model'; -import { ScamInfoTypeEnum } from '.'; +import { Asset, ScamInfoTypeEnum } from '.'; @ObjectType() export class ScamInfo { @Field(() => ScamInfoTypeEnum, { nullable: true }) @@ -25,7 +24,7 @@ export class ScamInfo { } static areApiAndElasticScamInfoDifferent( - nftFromApi: Nft, + nftFromApi: Asset, nftFromElastic: any, ): boolean { return ( @@ -37,7 +36,7 @@ export class ScamInfo { } static areApiAndDbScamInfoDifferent( - nftFromApi: Nft, + nftFromApi: Asset, nftFromDb: NftScamInfoModel, version: string, ): boolean { @@ -59,4 +58,22 @@ export class ScamInfo { (!nftFromDb.type && nftFromElastic?.[elasticDictionary.scamInfo.typeKey]) ); } + + static isScam(scamInfo: ScamInfo): boolean { + return scamInfo.type !== ScamInfoTypeEnum.none; + } + + static none(): ScamInfo { + return new ScamInfo({ + type: ScamInfoTypeEnum.none, + info: null, + }); + } + + static scam(): ScamInfo { + return new ScamInfo({ + type: ScamInfoTypeEnum.scam, + info: 'Scam report', + }); + } } diff --git a/src/modules/assets/models/requests/CreateNftRequest.ts b/src/modules/assets/models/requests/CreateNftRequest.ts index a19446870..477362cfd 100644 --- a/src/modules/assets/models/requests/CreateNftRequest.ts +++ b/src/modules/assets/models/requests/CreateNftRequest.ts @@ -29,3 +29,27 @@ export class Attribute { tags: string[]; description: string; } + +export class CreateNftWithMultipleFilesRequest { + collection: string; + quantity: string = '1'; + name: string; + royalties: string = '0'; + attributes: Attribute; + files: FileUpload[]; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + static fromArgs(nftArgs: CreateNftArgs, files: FileUpload[]) { + return new CreateNftWithMultipleFilesRequest({ + collection: nftArgs.collection, + quantity: nftArgs.quantity, + name: nftArgs.name, + royalties: nftArgs.royalties, + attributes: nftArgs.attributes, + files: files, + }); + } +} diff --git a/src/modules/auctions/auctions-getter.service.ts b/src/modules/auctions/auctions-getter.service.ts index 3b077bf9a..466996bf9 100644 --- a/src/modules/auctions/auctions-getter.service.ts +++ b/src/modules/auctions/auctions-getter.service.ts @@ -12,7 +12,6 @@ import { Constants } from '@multiversx/sdk-nestjs'; import { mxConfig } from 'src/config'; import { AuctionCustomEnum } from '../common/filters/AuctionCustomFilters'; import { PersistenceService } from 'src/common/persistence/persistence.service'; -import { Token } from 'src/common/services/mx-communication/models/Token.model'; import { UsdPriceService } from '../usdPrice/usd-price.service'; import { auctionsByNoBidsRequest, @@ -24,6 +23,7 @@ import { import { CurrentPaymentTokensFilters } from './models/CurrentPaymentTokens.Filter'; import { BigNumberUtils } from 'src/utils/bigNumber-utils'; import { DateUtils } from 'src/utils/date-utils'; +import { Token } from '../usdPrice/Token.model'; @Injectable() export class AuctionsGetterService { diff --git a/src/modules/auctions/auctions-queries.resolver.ts b/src/modules/auctions/auctions-queries.resolver.ts index 8b8f2e633..bf66361ab 100644 --- a/src/modules/auctions/auctions-queries.resolver.ts +++ b/src/modules/auctions/auctions-queries.resolver.ts @@ -33,11 +33,11 @@ import { MarketplaceProvider } from '../marketplaces/loaders/marketplace.loader' import { TokenFilter } from './models/Token.Filter'; import { mxConfig } from 'src/config'; import { XOXNO_KEY } from 'src/utils/constants'; -import { Token } from 'src/common/services/mx-communication/models/Token.model'; import { CurrentPaymentTokensFilters } from './models/CurrentPaymentTokens.Filter'; import { Fields } from '../common/fields.decorator'; import { JwtOrNativeAuthGuard } from '../auth/jwt.or.native.auth-guard'; import { AuthUser } from '../auth/authUser'; +import { Token } from '../usdPrice/Token.model'; @Resolver(() => Auction) export class AuctionsQueriesResolver extends BaseResolver(Auction) { diff --git a/src/modules/auctions/auctions-setter.service.ts b/src/modules/auctions/auctions-setter.service.ts index f411df25a..aaab9f84b 100644 --- a/src/modules/auctions/auctions-setter.service.ts +++ b/src/modules/auctions/auctions-setter.service.ts @@ -106,6 +106,10 @@ export class AuctionsSetterService { } } + async saveBulkAuctions(auctions: AuctionEntity[]): Promise { + return await this.persistenceService.saveBulkAuctions(auctions); + } + async saveAuctionEntity( auctionEntity: AuctionEntity, assetTags: string[], diff --git a/src/modules/auctions/caching/auctions-caching.service.ts b/src/modules/auctions/caching/auctions-caching.service.ts index 103b3be39..70a5e9257 100644 --- a/src/modules/auctions/caching/auctions-caching.service.ts +++ b/src/modules/auctions/caching/auctions-caching.service.ts @@ -16,7 +16,7 @@ import { Auction } from '../models'; import { QueryRequest } from 'src/modules/common/filters/QueryRequest'; import * as hash from 'object-hash'; import { InternalMarketplaceRedisHandler } from 'src/modules/assets/loaders/internal-marketplace.redis-handler'; -import { Token } from 'src/common/services/mx-communication/models/Token.model'; +import { Token } from 'src/modules/usdPrice/Token.model'; @Injectable() export class AuctionsCachingService { @@ -51,12 +51,6 @@ export class AuctionsCachingService { ]); } - public async invalidateCacheByPattern(address: string) { - await this.redisCacheService.deleteByPattern( - `${generateCacheKeyFromParams('claimable_auctions', address)}*`, - ); - } - public async getOrSetAuctions( queryRequest: QueryRequest, getAuctions: () => any, @@ -64,7 +58,7 @@ export class AuctionsCachingService { return this.redisCacheService.getOrSet( this.getAuctionsCacheKey(queryRequest), () => getAuctions(), - 30 * Constants.oneSecond(), + 5 * Constants.oneSecond(), ); } diff --git a/src/modules/common/api-config/api.config.service.ts b/src/modules/common/api-config/api.config.service.ts index 754b5bdff..5c4efeb5e 100644 --- a/src/modules/common/api-config/api.config.service.ts +++ b/src/modules/common/api-config/api.config.service.ts @@ -1,10 +1,14 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { mxConfig } from 'src/config'; @Injectable() export class ApiConfigService { constructor(private readonly configService: ConfigService) {} + getElasticUrl(): string { + return this.getGenericConfig('ELROND_ELASTICSEARCH'); + } private getGenericConfig(path: string, alias?: string): T { const value = this.configService.get(path); if (!value) { @@ -94,4 +98,22 @@ export class ApiConfigService { getExtrasApiUrl(): string { return this.getGenericConfig('MX_EXTRAS_API'); } + + getRateLimiterSecret(): string | undefined { + return this.configService.get('rateLimiterSecret'); + } + + getUseKeepAliveAgentFlag(): boolean { + return mxConfig.keepAlive ?? true; + } + + getAxiosTimeout(): number { + return ( + this.getGenericConfig('KEEPALIVE_TIMEOUT_DOWNSTREAM') ?? 61000 + ); + } + + getServerTimeout(): number { + return this.getGenericConfig('KEEPALIVE_TIMEOUT_UPSTREAM') ?? 60000; + } } diff --git a/src/modules/common/filters/filtersTypes.ts b/src/modules/common/filters/filtersTypes.ts index 69ce1c31a..77c30b1e1 100644 --- a/src/modules/common/filters/filtersTypes.ts +++ b/src/modules/common/filters/filtersTypes.ts @@ -9,7 +9,7 @@ import { NFT_IDENTIFIER_ERROR, NFT_IDENTIFIER_RGX, } from 'src/utils/constants'; -import { NftTypeEnum } from '../../assets/models/NftTypes.enum'; +import { CustomFiltersEnum, NftTypeEnum } from '../../assets/models/NftTypes.enum'; export enum Operator { AND, @@ -177,12 +177,17 @@ export class AssetsFilter { @IsOptional() @Field(() => NftTypeEnum, { nullable: true }) type: NftTypeEnum; - constructor(init?: Partial) { - Object.assign(this, init); - } + + @IsOptional() + @Field(() => CustomFiltersEnum, { nullable: true }) + customFilters: CustomFiltersEnum; @Field(() => [NftTrait], { nullable: 'itemsAndList' }) traits: NftTrait[]; + + constructor(init?: Partial) { + Object.assign(this, init); + } } @InputType() diff --git a/src/modules/featured/FeatureCollectionType.enum.ts b/src/modules/featured/FeatureCollectionType.enum.ts index 440422c38..aa41cae72 100644 --- a/src/modules/featured/FeatureCollectionType.enum.ts +++ b/src/modules/featured/FeatureCollectionType.enum.ts @@ -3,6 +3,7 @@ import { registerEnumType } from '@nestjs/graphql'; export enum FeaturedCollectionTypeEnum { Featured = 'Featured', Hero = 'Hero', + Tickets = 'Tickets', } registerEnumType(FeaturedCollectionTypeEnum, { diff --git a/src/modules/featured/featured-caching.service.ts b/src/modules/featured/featured-caching.service.ts index 88d9c731f..25fd0eb05 100644 --- a/src/modules/featured/featured-caching.service.ts +++ b/src/modules/featured/featured-caching.service.ts @@ -9,12 +9,12 @@ import { RedisCacheService } from '@multiversx/sdk-nestjs'; @Injectable() export class FeaturedCollectionsCachingService { - constructor(private readonly redisCacheService: RedisCacheService) {} + constructor(private readonly redisCacheService: RedisCacheService) { } async getOrSetFeaturedCollections( getFeaturedCollections: () => any, - limit?: number, - offset?: number, + _limit?: number, + _offset?: number, ): Promise<[FeaturedCollectionEntity[], number]> { const cacheKey = this.getFeaturedCollectionsCacheKey(); return await this.redisCacheService.getOrSet( @@ -38,9 +38,7 @@ export class FeaturedCollectionsCachingService { } async invalidateFeaturedCollectionsCache(): Promise { - await this.redisCacheService.deleteByPattern( - `${CacheInfo.FeaturedCollections.key}*`, - ); + await this.redisCacheService.delete(CacheInfo.FeaturedCollections.key); } private getFeaturedNftsCacheKey(limit?: number, offset?: number) { diff --git a/src/modules/featured/featured-collections.resolver.ts b/src/modules/featured/featured-collections.resolver.ts index 390a190c9..339659457 100644 --- a/src/modules/featured/featured-collections.resolver.ts +++ b/src/modules/featured/featured-collections.resolver.ts @@ -31,7 +31,7 @@ export class FeaturedCollectionsResolver extends BaseResolver(Collection) { ): Promise { const { limit, offset } = pagination.pagingParams(); const [collections, count] = - await this.featuredService.getFeaturedCollections(limit, offset, filters); + await this.featuredService.getFeaturedCollections(filters, limit, offset,); return PageResponse.mapResponse( collections || [], pagination, diff --git a/src/modules/featured/featured.module.ts b/src/modules/featured/featured.module.ts index edfa9affd..694fc7fd2 100644 --- a/src/modules/featured/featured.module.ts +++ b/src/modules/featured/featured.module.ts @@ -15,5 +15,6 @@ import { FeaturedService } from './featured.service'; FeaturedCollectionsResolver, FeaturedCollectionsCachingService, ], + exports: [FeaturedService] }) -export class FeaturedModuleGraph {} +export class FeaturedModuleGraph { } diff --git a/src/modules/featured/featured.service.ts b/src/modules/featured/featured.service.ts index a3934f068..43ebbd1a3 100644 --- a/src/modules/featured/featured.service.ts +++ b/src/modules/featured/featured.service.ts @@ -11,6 +11,7 @@ import { CacheEventTypeEnum, ChangedEvent, } from '../rabbitmq/cache-invalidation/events/changed.event'; +import { constants } from 'src/config'; @Injectable() export class FeaturedService { @@ -20,7 +21,7 @@ export class FeaturedService { private readonly logger: Logger, private readonly featuredCollectionsCachingService: FeaturedCollectionsCachingService, private cacheEventsPublisherService: CacheEventsPublisherService, - ) {} + ) { } async getFeaturedNfts( limit: number = 10, @@ -48,9 +49,9 @@ export class FeaturedService { } async getFeaturedCollections( - limit: number = 10, - offset: number, filters: FeaturedCollectionsFilter, + limit: number = constants.defaultPageSize, + offset: number = constants.defaultPageOffset, ): Promise<[Collection[], number]> { try { const getFeaturedCollections = () => diff --git a/src/modules/marketplaces/marketplaces-events-indexing.service.ts b/src/modules/marketplaces/marketplaces-events-indexing.service.ts index 941819837..81599b038 100644 --- a/src/modules/marketplaces/marketplaces-events-indexing.service.ts +++ b/src/modules/marketplaces/marketplaces-events-indexing.service.ts @@ -11,6 +11,11 @@ import { getMarketplaceEventsElasticQuery, getMarketplaceTransactionsElasticQuery, } from './marketplaces.elastic.queries'; +import { + AuctionEventEnum, + ElrondNftsSwapAuctionEventEnum, + ExternalAuctionEventEnum, +} from '../assets/models'; @Injectable() export class MarketplaceEventsIndexingService { @@ -75,36 +80,51 @@ export class MarketplaceEventsIndexingService { async reindexMarketplaceEvents( input: MarketplaceEventsIndexingRequest, ): Promise { - try { - if (input.beforeTimestamp < input.afterTimestamp) { - throw new Error(`beforeTimestamp can't be less than afterTimestamp`); - } + await Locker.lock( + `Reindex marketplace events for ${input.marketplaceAddress}`, + async () => { + try { + if (input.beforeTimestamp < input.afterTimestamp) { + throw new Error( + `beforeTimestamp can't be less than afterTimestamp`, + ); + } - const newestTxTimestamp = - await this.addElasticTxToDbAndReturnNewestTimestamp(input); - const newestEventTimestamp = - await this.addElasticEventsToDbAndReturnNewestTimestamp(input); - - const newestTimestamp = Math.max(newestTxTimestamp, newestEventTimestamp); - - if ( - newestTimestamp && - (!input.marketplaceLastIndexTimestamp || - newestTimestamp > input.marketplaceLastIndexTimestamp) - ) { - await this.marketplaceService.updateMarketplaceLastIndexTimestampByAddress( - input.marketplaceAddress, - newestTimestamp, - ); - await this.marketplacesCachingService.invalidateMarketplacesCache(); - } - } catch (error) { - this.logger.error('Error when reindexing marketplace events', { - path: `${MarketplaceEventsIndexingService.name}.${this.reindexMarketplaceEvents.name}`, - error: error.message, - marketplaceAddress: input.marketplaceAddress, - }); - } + const newestTxTimestamp = + await this.addElasticTxToDbAndReturnNewestTimestamp(input); + const newestEventTimestamp = + await this.addElasticEventsToDbAndReturnNewestTimestamp(input); + + const newestTimestamp = Math.max( + newestTxTimestamp, + newestEventTimestamp, + ); + + if ( + newestTimestamp && + (!input.marketplaceLastIndexTimestamp || + newestTimestamp > input.marketplaceLastIndexTimestamp) + ) { + await this.marketplaceService.updateMarketplaceLastIndexTimestampByAddress( + input.marketplaceAddress, + newestTimestamp, + ); + await this.marketplacesCachingService.invalidateMarketplacesCache(); + } + + this.logger.log( + `Reindexing marketplace events for ${input.marketplaceAddress} ended`, + ); + } catch (error) { + this.logger.error('Error when reindexing marketplace events', { + path: `${MarketplaceEventsIndexingService.name}.${this.reindexMarketplaceEvents.name}`, + error: error.message, + marketplaceAddress: input.marketplaceAddress, + }); + } + }, + true, + ); } private async addElasticTxToDbAndReturnNewestTimestamp( @@ -126,7 +146,7 @@ export class MarketplaceEventsIndexingService { getMarketplaceEventsElasticQuery(input), 'logs', input.marketplaceAddress, - this.saveEventsToDb.bind(this), + this.filterEventsAndSaveToDb.bind(this), input.stopIfDuplicates, ); } @@ -211,7 +231,7 @@ export class MarketplaceEventsIndexingService { return [savedRecordsCount, marketplaceEvents.length]; } - private async saveEventsToDb( + private async filterEventsAndSaveToDb( batch: any, marketplaceAddress: string, ): Promise<[number, number]> { @@ -221,6 +241,10 @@ export class MarketplaceEventsIndexingService { for (let j = 0; j < batch[i].events.length; j++) { const event = batch[i].events[j]; + if (this.isUnknownMarketplaceEvent(event.identifier)) { + continue; + } + const marketplaceEvent = new MarketplaceEventsEntity({ txHash: batch[i].identifier, originalTxHash: batch[i].originalTxHash, @@ -239,6 +263,15 @@ export class MarketplaceEventsIndexingService { await this.persistenceService.saveOrIgnoreMarketplacesBulk( marketplaceEvents, ); + return [savedRecordsCount, marketplaceEvents.length]; } + + private isUnknownMarketplaceEvent(eventIdentifier: any): boolean { + return ( + !Object.values(AuctionEventEnum).includes(eventIdentifier) && + !Object.values(ExternalAuctionEventEnum).includes(eventIdentifier) && + !Object.values(ElrondNftsSwapAuctionEventEnum).includes(eventIdentifier) + ); + } } diff --git a/src/modules/marketplaces/marketplaces-reindex-events-summary.service.ts b/src/modules/marketplaces/marketplaces-reindex-events-summary.service.ts new file mode 100644 index 000000000..a53caae66 --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex-events-summary.service.ts @@ -0,0 +1,182 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { Marketplace } from './models'; +import { + AuctionEventEnum, + ElrondNftsSwapAuctionEventEnum, + ExternalAuctionEventEnum, +} from '../assets/models'; +import { AuctionStartedSummary as AuctionStartedSummary } from './models/marketplaces-reindex-events-summaries/AuctionStartedSummary'; +import { ReindexAuctionClosedSummary as AuctionClosedSummary } from './models/marketplaces-reindex-events-summaries/AuctionClosedSummary'; +import { AuctionEndedSummary as AuctionEndedSummary } from './models/marketplaces-reindex-events-summaries/AuctionEndedSummary'; +import { AuctionBuySummary as AuctionBuySummary } from './models/marketplaces-reindex-events-summaries/AuctionBuySummary'; +import { AuctionBidSummary } from './models/marketplaces-reindex-events-summaries/AuctionBidSummary'; +import { OfferCreatedSummary } from './models/marketplaces-reindex-events-summaries/OfferCreatedSummary'; +import { OfferAcceptedSummary } from './models/marketplaces-reindex-events-summaries/OfferAcceptedSummary'; +import { OfferClosedSummary } from './models/marketplaces-reindex-events-summaries/OfferClosedSummary'; +import { AuctionUpdatedSummary } from './models/marketplaces-reindex-events-summaries/AuctionUpdatedSummary'; +import { GlobalOfferAcceptedSummary } from './models/marketplaces-reindex-events-summaries/GloballyOfferAcceptedSummary'; +import { AuctionPriceUpdatedSummary } from './models/marketplaces-reindex-events-summaries/AuctionPriceUpdated'; +import { MarketplaceTransactionData } from './models/marketplaceEventAndTxData.dto'; + +@Injectable() +export class MarketplacesReindexEventsSummaryService { + constructor(private readonly logger: Logger) {} + getEventsSetSummaries( + marketplace: Marketplace, + eventsSet: MarketplaceEventsEntity[], + ): any[] { + const [events, txData] = this.getEventsAndTxData(eventsSet); + + if (!events) { + return; + } + + return events.map((event) => { + try { + return this.getEventSummary(event, txData, marketplace); + } catch (error) { + this.logger.warn( + `Error when getting event summary for ${txData.blockHash} ${event.timestamp}`, + ); + } + }); + } + + private getEventsAndTxData( + eventsSet: MarketplaceEventsEntity[], + ): [MarketplaceEventsEntity[], MarketplaceTransactionData] { + const eventsOrderedByOrderAsc = eventsSet.sort((a, b) => { + return a.eventOrder - b.eventOrder; + }); + const tx = eventsSet[0].isTx ? eventsSet[0] : undefined; + + if (eventsOrderedByOrderAsc.length === 1 && tx) { + return [undefined, undefined]; + } + + const eventsStartIdx = tx ? 1 : 0; + + return [eventsOrderedByOrderAsc.slice(eventsStartIdx), tx?.data?.txData]; + } + + private getEventSummary( + event: MarketplaceEventsEntity, + txData: MarketplaceTransactionData, + marketplace: Marketplace, + ): any { + switch (event.getEventIdentifier()) { + case AuctionEventEnum.AuctionTokenEvent: + case ExternalAuctionEventEnum.Listing: + case ExternalAuctionEventEnum.ListNftOnMarketplace: + case ElrondNftsSwapAuctionEventEnum.NftSwap: { + return AuctionStartedSummary.fromAuctionTokenEventAndTx(event, txData); + } + case AuctionEventEnum.WithdrawEvent: + case ExternalAuctionEventEnum.ClaimBackNft: + case ElrondNftsSwapAuctionEventEnum.WithdrawSwap: + case ExternalAuctionEventEnum.ReturnListing: { + return AuctionClosedSummary.fromWithdrawAuctionEventAndTx( + event, + txData, + ); + } + case AuctionEventEnum.EndAuctionEvent: { + return AuctionEndedSummary.fromEndAuctionEventAndTx(event, txData); + } + case AuctionEventEnum.BuySftEvent: + case ExternalAuctionEventEnum.Buy: + case ExternalAuctionEventEnum.BuyFor: + case ExternalAuctionEventEnum.BuyNft: + case ElrondNftsSwapAuctionEventEnum.Purchase: + case ExternalAuctionEventEnum.BulkBuy: { + return AuctionBuySummary.fromBuySftEventAndTx( + event, + txData, + marketplace, + ); + } + case AuctionEventEnum.BidEvent: + case ElrondNftsSwapAuctionEventEnum.Bid: { + return AuctionBidSummary.fromBidEventAndTx( + event, + txData, + marketplace.key, + ); + } + case AuctionEventEnum.SendOffer: { + return OfferCreatedSummary.fromSendOfferEventAndTx( + event, + txData, + marketplace, + ); + } + case AuctionEventEnum.AcceptOffer: + case ExternalAuctionEventEnum.AcceptOffer: { + if ( + event.hasEventTopicIdentifier(ExternalAuctionEventEnum.EndTokenEvent) + ) { + return AuctionClosedSummary.fromWithdrawAuctionEventAndTx( + event, + txData, + ); + } + if ( + event.hasEventTopicIdentifier(ExternalAuctionEventEnum.UserDeposit) + ) { + return; + } + return OfferAcceptedSummary.fromAcceptOfferEventAndTx( + event, + txData, + marketplace, + ); + } + case ExternalAuctionEventEnum.ChangePrice: + case ExternalAuctionEventEnum.UpdatePrice: { + return AuctionPriceUpdatedSummary.fromUpdatePriceEventAndTx( + event, + txData, + marketplace, + ); + } + case ExternalAuctionEventEnum.UpdateListing: + case ElrondNftsSwapAuctionEventEnum.NftSwapUpdate: + case ElrondNftsSwapAuctionEventEnum.NftSwapExtend: { + return AuctionUpdatedSummary.fromUpdateListingEventAndTx(event, txData); + } + case ExternalAuctionEventEnum.AcceptGlobalOffer: { + return GlobalOfferAcceptedSummary.fromAcceptGlobalOfferEventAndTx( + event, + txData, + ); + } + case AuctionEventEnum.WithdrawOffer: { + return OfferClosedSummary.fromWithdrawOfferEventAndTx(event, txData); + } + case AuctionEventEnum.WithdrawAuctionAndAcceptOffer: { + if ( + event.hasEventTopicIdentifier( + AuctionEventEnum.Accept_offer_token_event, + ) + ) { + return OfferAcceptedSummary.fromAcceptOfferEventAndTx( + event, + txData, + marketplace, + ); + } else { + return AuctionClosedSummary.fromWithdrawAuctionEventAndTx( + event, + txData, + ); + } + } + default: { + throw new Error( + `Unhandled marketplace event - ${event.data.eventData.identifier}`, + ); + } + } + } +} diff --git a/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-bid.handler.ts b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-bid.handler.ts new file mode 100644 index 000000000..ec74c96c2 --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-bid.handler.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { AuctionStatusEnum } from 'src/modules/auctions/models'; +import { OrderStatusEnum } from 'src/modules/orders/models'; +import { Token } from 'src/modules/usdPrice/Token.model'; +import { DateUtils } from 'src/utils/date-utils'; +import { MarketplaceReindexState } from '../models/MarketplaceReindexState'; +import { AuctionBidSummary } from '../models/marketplaces-reindex-events-summaries/AuctionBidSummary'; + +@Injectable() +export class ReindexAuctionBidHandler { + constructor() {} + + handle( + marketplaceReindexState: MarketplaceReindexState, + input: AuctionBidSummary, + paymentToken: Token, + paymentNonce: number, + ): void { + const auctionIndex = marketplaceReindexState.getAuctionIndexByAuctionId( + input.auctionId, + ); + + if (auctionIndex === -1) { + return; + } + + const modifiedDate = DateUtils.getUtcDateFromTimestamp(input.timestamp); + + marketplaceReindexState.setInactiveOrdersForAuction( + marketplaceReindexState.auctions[auctionIndex].id, + modifiedDate, + ); + + let order = marketplaceReindexState.createOrder( + auctionIndex, + input, + OrderStatusEnum.Active, + paymentToken, + paymentNonce, + ); + + if ( + order.priceAmount === + marketplaceReindexState.auctions[auctionIndex].maxBid + ) { + order.status = OrderStatusEnum.Bought; + marketplaceReindexState.auctions[auctionIndex].status = + AuctionStatusEnum.Ended; + marketplaceReindexState.auctions[auctionIndex].modifiedDate = + modifiedDate; + } + + marketplaceReindexState.orders.push(order); + } +} diff --git a/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-bought.handler.ts b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-bought.handler.ts new file mode 100644 index 000000000..8082e7e76 --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-bought.handler.ts @@ -0,0 +1,81 @@ +import { Injectable } from '@nestjs/common'; +import { OrderEntity } from 'src/db/orders'; +import { AuctionStatusEnum } from 'src/modules/auctions/models'; +import { OrderStatusEnum } from 'src/modules/orders/models'; +import { Token } from 'src/modules/usdPrice/Token.model'; +import { ELRONDNFTSWAP_KEY } from 'src/utils/constants'; +import { DateUtils } from 'src/utils/date-utils'; +import { MarketplaceReindexState } from '../models/MarketplaceReindexState'; +import { AuctionBuySummary } from '../models/marketplaces-reindex-events-summaries/AuctionBuySummary'; + +@Injectable() +export class ReindexAuctionBoughtHandler { + constructor() {} + + handle( + marketplaceReindexState: MarketplaceReindexState, + input: AuctionBuySummary, + paymentToken: Token, + paymentNonce: number, + ): void { + const auctionIndex = + marketplaceReindexState.marketplace.key !== ELRONDNFTSWAP_KEY + ? marketplaceReindexState.getAuctionIndexByAuctionId(input.auctionId) + : marketplaceReindexState.getAuctionIndexByIdentifier(input.identifier); + + if (auctionIndex === -1) { + return; + } + + const modifiedDate = DateUtils.getUtcDateFromTimestamp(input.timestamp); + + marketplaceReindexState.setInactiveOrdersForAuction( + marketplaceReindexState.auctions[auctionIndex].id, + modifiedDate, + ); + + const order = marketplaceReindexState.createOrder( + auctionIndex, + input, + OrderStatusEnum.Bought, + paymentToken, + paymentNonce, + ); + marketplaceReindexState.orders.push(order); + + const totalBought = this.getTotalBoughtTokensForAuction( + marketplaceReindexState.auctions[auctionIndex].id, + marketplaceReindexState.orders, + ); + + if ( + marketplaceReindexState.auctions[auctionIndex].nrAuctionedTokens === + totalBought + ) { + marketplaceReindexState.auctions[auctionIndex].status = + AuctionStatusEnum.Ended; + marketplaceReindexState.auctions[auctionIndex].modifiedDate = + modifiedDate; + marketplaceReindexState.auctions[auctionIndex].blockHash = + marketplaceReindexState.auctions[auctionIndex].blockHash ?? + input.blockHash; + } + } + + private getTotalBoughtTokensForAuction( + auctionId: number, + orders: OrderEntity[], + ): number { + let totalBought = 0; + orders + .filter( + (o) => o.auctionId === auctionId && o.status === OrderStatusEnum.Bought, + ) + .forEach((o) => { + totalBought += parseInt(o.boughtTokensNo) + ? parseInt(o.boughtTokensNo) + : 1; + }); + return totalBought; + } +} diff --git a/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-closed.handler.ts b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-closed.handler.ts new file mode 100644 index 000000000..eda0fc061 --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-closed.handler.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { AuctionStatusEnum } from 'src/modules/auctions/models'; +import { ELRONDNFTSWAP_KEY } from 'src/utils/constants'; +import { DateUtils } from 'src/utils/date-utils'; +import { MarketplaceReindexState } from '../models/MarketplaceReindexState'; +import { ReindexAuctionClosedSummary } from '../models/marketplaces-reindex-events-summaries/AuctionClosedSummary'; + +@Injectable() +export class ReindexAuctionClosedHandler { + constructor() {} + + handle( + marketplaceReindexState: MarketplaceReindexState, + input: ReindexAuctionClosedSummary, + ): void { + const auctionIndex = + marketplaceReindexState.marketplace.key !== ELRONDNFTSWAP_KEY + ? marketplaceReindexState.getAuctionIndexByAuctionId(input.auctionId) + : marketplaceReindexState.getAuctionIndexByIdentifier(input.identifier); + const modifiedDate = DateUtils.getUtcDateFromTimestamp(input.timestamp); + + if (auctionIndex === -1) { + return; + } + + marketplaceReindexState.auctions[auctionIndex].status = + AuctionStatusEnum.Closed; + marketplaceReindexState.auctions[auctionIndex].blockHash = + marketplaceReindexState.auctions[auctionIndex].blockHash ?? + input.blockHash; + marketplaceReindexState.auctions[auctionIndex].modifiedDate = modifiedDate; + + marketplaceReindexState.setInactiveOrdersForAuction( + marketplaceReindexState.auctions[auctionIndex].id, + modifiedDate, + ); + } +} diff --git a/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-ended.handler.ts b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-ended.handler.ts new file mode 100644 index 000000000..fe1507d50 --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-ended.handler.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@nestjs/common'; +import { AuctionStatusEnum } from 'src/modules/auctions/models'; +import { OrderStatusEnum } from 'src/modules/orders/models'; +import { Token } from 'src/modules/usdPrice/Token.model'; +import { DateUtils } from 'src/utils/date-utils'; +import { MarketplaceReindexState } from '../models/MarketplaceReindexState'; +import { AuctionEndedSummary } from '../models/marketplaces-reindex-events-summaries/AuctionEndedSummary'; + +@Injectable() +export class ReindexAuctionEndedHandler { + constructor() {} + + handle( + marketplaceReindexState: MarketplaceReindexState, + input: AuctionEndedSummary, + paymentToken: Token, + ): void { + const auctionIndex = marketplaceReindexState.getAuctionIndexByAuctionId( + input.auctionId, + ); + const modifiedDate = DateUtils.getUtcDateFromTimestamp(input.timestamp); + + if (auctionIndex === -1) { + return; + } + + marketplaceReindexState.auctions[auctionIndex].status = + AuctionStatusEnum.Ended; + marketplaceReindexState.auctions[auctionIndex].blockHash = + marketplaceReindexState.auctions[auctionIndex].blockHash ?? + input.blockHash; + marketplaceReindexState.auctions[auctionIndex].modifiedDate = modifiedDate; + + const winnerOrderId = + marketplaceReindexState.setAuctionOrderWinnerStatusAndReturnId( + marketplaceReindexState.auctions[auctionIndex].id, + OrderStatusEnum.Bought, + modifiedDate, + ); + + if (winnerOrderId !== -1) { + marketplaceReindexState.setInactiveOrdersForAuction( + marketplaceReindexState.auctions[auctionIndex].id, + modifiedDate, + winnerOrderId, + ); + } else if (input.currentBid !== '0') { + const order = marketplaceReindexState.createOrder( + auctionIndex, + input, + OrderStatusEnum.Bought, + paymentToken, + ); + marketplaceReindexState.orders.push(order); + } + } +} diff --git a/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-price-updated.handler.ts b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-price-updated.handler.ts new file mode 100644 index 000000000..801c01187 --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-price-updated.handler.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { BigNumberUtils } from 'src/utils/bigNumber-utils'; +import { DateUtils } from 'src/utils/date-utils'; +import { MarketplaceReindexState } from '../models/MarketplaceReindexState'; +import { AuctionPriceUpdatedSummary } from '../models/marketplaces-reindex-events-summaries/AuctionPriceUpdated'; + +@Injectable() +export class ReindexAuctionPriceUpdatedHandler { + constructor() {} + + handle( + marketplaceReindexState: MarketplaceReindexState, + input: AuctionPriceUpdatedSummary, + decimals: number, + ): void { + const auctionIndex = marketplaceReindexState.getAuctionIndexByAuctionId( + input.auctionId, + ); + const modifiedDate = DateUtils.getUtcDateFromTimestamp(input.timestamp); + if (auctionIndex === -1) { + return; + } + + marketplaceReindexState.auctions[auctionIndex].blockHash = + marketplaceReindexState.auctions[auctionIndex].blockHash ?? + input.blockHash; + marketplaceReindexState.auctions[auctionIndex].modifiedDate = modifiedDate; + + marketplaceReindexState.auctions[auctionIndex].minBid = input.minBid; + marketplaceReindexState.auctions[auctionIndex].minBidDenominated = + BigNumberUtils.denominateAmount(input.minBid, decimals); + + if (input.maxBid) { + marketplaceReindexState.auctions[auctionIndex].maxBid = input.maxBid; + marketplaceReindexState.auctions[auctionIndex].maxBidDenominated = + BigNumberUtils.denominateAmount(input.maxBid, decimals); + } else { + marketplaceReindexState.auctions[auctionIndex].maxBid = input.minBid; + marketplaceReindexState.auctions[auctionIndex].maxBidDenominated = + marketplaceReindexState.auctions[auctionIndex].minBidDenominated; + } + + if (input.paymentToken) { + marketplaceReindexState.auctions[auctionIndex].paymentToken = + input.paymentToken; + } + + if (input.itemsCount) { + marketplaceReindexState.auctions[auctionIndex].nrAuctionedTokens = + input.itemsCount; + } + } +} diff --git a/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-started.handler.ts b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-started.handler.ts new file mode 100644 index 000000000..a22930367 --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-started.handler.ts @@ -0,0 +1,64 @@ +import { BinaryUtils } from '@multiversx/sdk-nestjs'; +import { Injectable } from '@nestjs/common'; +import { AuctionEntity } from 'src/db/auctions'; +import { AuctionStatusEnum } from 'src/modules/auctions/models'; +import { Token } from 'src/modules/usdPrice/Token.model'; +import { BigNumberUtils } from 'src/utils/bigNumber-utils'; +import { DateUtils } from 'src/utils/date-utils'; +import { MarketplaceReindexState } from '../models/MarketplaceReindexState'; +import { AuctionStartedSummary } from '../models/marketplaces-reindex-events-summaries/AuctionStartedSummary'; + +@Injectable() +export class ReindexAuctionStartedHandler { + constructor() {} + + handle( + input: AuctionStartedSummary, + marketplaceReindexState: MarketplaceReindexState, + paymentToken: Token, + paymentNonce: number, + ): void { + const nonce = BinaryUtils.hexToNumber(input.nonce); + const itemsCount = parseInt(input.itemsCount); + const modifiedDate = DateUtils.getUtcDateFromTimestamp(input.timestamp); + const startTime = Number.isNaN(input.startTime) + ? input.timestamp + : input.startTime; + const auction = new AuctionEntity({ + creationDate: modifiedDate, + modifiedDate, + id: marketplaceReindexState.auctions.length, + marketplaceAuctionId: + input.auctionId !== 0 + ? input.auctionId + : marketplaceReindexState.auctions.length + 1, + identifier: input.identifier, + collection: input.collection, + nonce: nonce, + nrAuctionedTokens: itemsCount, + status: AuctionStatusEnum.Running, + type: input.auctionType, + paymentToken: paymentToken.identifier, + paymentNonce, + ownerAddress: input.sender, + minBid: input.minBid, + maxBid: input.maxBid !== 'NaN' ? input.maxBid : '0', + minBidDenominated: BigNumberUtils.denominateAmount( + input.minBid, + paymentToken.decimals, + ), + maxBidDenominated: BigNumberUtils.denominateAmount( + input.maxBid !== 'NaN' ? input.maxBid : '0', + paymentToken.decimals, + ), + minBidDiff: input.minBidDiff ?? '0', + startDate: startTime, + endDate: input.endTime > 0 ? input.endTime : 0, + tags: '', + blockHash: input.blockHash ?? '', + marketplaceKey: marketplaceReindexState.marketplace.key, + }); + + marketplaceReindexState.auctions.push(auction); + } +} diff --git a/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-updated.handler.ts b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-updated.handler.ts new file mode 100644 index 000000000..c400a88cf --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-auction-updated.handler.ts @@ -0,0 +1,49 @@ +import { BinaryUtils } from '@multiversx/sdk-nestjs'; +import { Injectable } from '@nestjs/common'; +import { BigNumberUtils } from 'src/utils/bigNumber-utils'; +import { DateUtils } from 'src/utils/date-utils'; +import { MarketplaceReindexState } from '../models/MarketplaceReindexState'; +import { AuctionUpdatedSummary } from '../models/marketplaces-reindex-events-summaries/AuctionUpdatedSummary'; + +@Injectable() +export class ReindexAuctionUpdatedHandler { + constructor() {} + + handle( + marketplaceReindexState: MarketplaceReindexState, + input: AuctionUpdatedSummary, + decimals: number, + ): void { + const auctionIndex = marketplaceReindexState.getAuctionIndexByAuctionId( + input.auctionId, + ); + const modifiedDate = DateUtils.getUtcDateFromTimestamp(input.timestamp); + + if (auctionIndex === -1) { + return; + } + + marketplaceReindexState.auctions[auctionIndex].blockHash = + marketplaceReindexState.auctions[auctionIndex].blockHash ?? + input.blockHash; + marketplaceReindexState.auctions[auctionIndex].modifiedDate = modifiedDate; + + marketplaceReindexState.auctions[auctionIndex].minBid = input.minBid; + marketplaceReindexState.auctions[auctionIndex].minBidDenominated = + BigNumberUtils.denominateAmount(input.minBid, decimals); + marketplaceReindexState.auctions[auctionIndex].maxBid = input.minBid; + marketplaceReindexState.auctions[auctionIndex].maxBidDenominated = + marketplaceReindexState.auctions[auctionIndex].minBidDenominated; + + if (input.paymentToken) { + marketplaceReindexState.auctions[auctionIndex].paymentToken = + input.paymentToken; + marketplaceReindexState.auctions[auctionIndex].paymentNonce = + BinaryUtils.hexToNumber(input.paymentNonce); + } + + if (input.deadline > 0) { + marketplaceReindexState.auctions[auctionIndex].endDate = input.deadline; + } + } +} diff --git a/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-global-offer-accepted.handler.ts b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-global-offer-accepted.handler.ts new file mode 100644 index 000000000..3ed85b8b6 --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-global-offer-accepted.handler.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { AuctionStatusEnum } from 'src/modules/auctions/models'; +import { DateUtils } from 'src/utils/date-utils'; +import { MarketplaceReindexState } from '../models/MarketplaceReindexState'; +import { GlobalOfferAcceptedSummary } from '../models/marketplaces-reindex-events-summaries/GloballyOfferAcceptedSummary'; + +@Injectable() +export class ReindexGlobalOfferAcceptedHandler { + constructor() {} + + handle( + marketplaceReindexState: MarketplaceReindexState, + input: GlobalOfferAcceptedSummary, + ): void { + const modifiedDate = DateUtils.getUtcDateFromTimestamp(input.timestamp); + const auctionIndex = marketplaceReindexState.getAuctionIndexByAuctionId( + input.auctionId, + ); + + if (auctionIndex === -1) { + return; + } + + marketplaceReindexState.auctions[auctionIndex].status = + AuctionStatusEnum.Closed; + marketplaceReindexState.auctions[auctionIndex].modifiedDate = modifiedDate; + } +} diff --git a/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-offer-accepted.handler.ts b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-offer-accepted.handler.ts new file mode 100644 index 000000000..765fc621a --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-offer-accepted.handler.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { AuctionStatusEnum } from 'src/modules/auctions/models'; +import { OfferStatusEnum } from 'src/modules/offers/models'; +import { ELRONDNFTSWAP_KEY } from 'src/utils/constants'; +import { DateUtils } from 'src/utils/date-utils'; +import { MarketplaceReindexState } from '../models/MarketplaceReindexState'; +import { OfferAcceptedSummary } from '../models/marketplaces-reindex-events-summaries/OfferAcceptedSummary'; + +@Injectable() +export class ReindexOfferAcceptedHandler { + constructor() {} + + handle( + marketplaceReindexState: MarketplaceReindexState, + input: OfferAcceptedSummary, + ): void { + const modifiedDate = DateUtils.getUtcDateFromTimestamp(input.timestamp); + const offerIndex = marketplaceReindexState.getOfferIndexByOfferId( + input.offerId, + ); + const auctionIndex = + marketplaceReindexState.marketplace.key !== ELRONDNFTSWAP_KEY + ? marketplaceReindexState.getAuctionIndexByAuctionId(input.auctionId) + : marketplaceReindexState.getAuctionIndexByIdentifier(input.identifier); + + if (offerIndex !== -1) { + marketplaceReindexState.offers[offerIndex].status = + OfferStatusEnum.Accepted; + marketplaceReindexState.offers[offerIndex].modifiedDate = modifiedDate; + return; + } + + if (auctionIndex !== -1) { + marketplaceReindexState.auctions[auctionIndex].status = + AuctionStatusEnum.Closed; + marketplaceReindexState.auctions[auctionIndex].modifiedDate = + modifiedDate; + } + } +} diff --git a/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-offer-closed.handler.ts b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-offer-closed.handler.ts new file mode 100644 index 000000000..446a0cd5b --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-offer-closed.handler.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { OfferStatusEnum } from 'src/modules/offers/models'; +import { DateUtils } from 'src/utils/date-utils'; +import { MarketplaceReindexState } from '../models/MarketplaceReindexState'; +import { OfferClosedSummary } from '../models/marketplaces-reindex-events-summaries/OfferClosedSummary'; + +@Injectable() +export class ReindexOfferClosedHandler { + constructor() {} + + handle( + marketplaceReindexState: MarketplaceReindexState, + input: OfferClosedSummary, + ): void { + const modifiedDate = DateUtils.getUtcDateFromTimestamp(input.timestamp); + const offerIndex = marketplaceReindexState.getOfferIndexByOfferId( + input.offerId, + ); + if (offerIndex === -1) { + return; + } + marketplaceReindexState.offers[offerIndex].status = OfferStatusEnum.Closed; + marketplaceReindexState.offers[offerIndex].modifiedDate = modifiedDate; + } +} diff --git a/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-offer-created.hander.ts b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-offer-created.hander.ts new file mode 100644 index 000000000..3e0dee89b --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex-handlers/reindex-offer-created.hander.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { OfferEntity } from 'src/db/offers'; +import { OfferStatusEnum } from 'src/modules/offers/models'; +import { BigNumberUtils } from 'src/utils/bigNumber-utils'; +import { DateUtils } from 'src/utils/date-utils'; +import { MarketplaceReindexState } from '../models/MarketplaceReindexState'; +import { OfferCreatedSummary } from '../models/marketplaces-reindex-events-summaries/OfferCreatedSummary'; + +@Injectable() +export class ReindexOfferCreatedHandler { + constructor() {} + + handle( + marketplaceReindexState: MarketplaceReindexState, + input: OfferCreatedSummary, + decimals: number, + ): void { + const modifiedDate = DateUtils.getUtcDateFromTimestamp(input.timestamp); + const offer = new OfferEntity({ + id: marketplaceReindexState.offers.length, + creationDate: modifiedDate, + modifiedDate, + marketplaceOfferId: input.offerId, + blockHash: input.blockHash, + collection: input.collection, + identifier: input.identifier, + priceToken: input.paymentToken, + priceNonce: input.paymentNonce, + priceAmount: input.price, + priceAmountDenominated: BigNumberUtils.denominateAmount( + input.price, + decimals, + ), + ownerAddress: input.address, + endDate: input.endTime, + boughtTokensNo: input.itemsCount, + marketplaceKey: marketplaceReindexState.marketplace.key, + status: OfferStatusEnum.Active, + }); + marketplaceReindexState.offers.push(offer); + } +} diff --git a/src/modules/marketplaces/marketplaces-reindex.service.ts b/src/modules/marketplaces/marketplaces-reindex.service.ts new file mode 100644 index 000000000..9b3259014 --- /dev/null +++ b/src/modules/marketplaces/marketplaces-reindex.service.ts @@ -0,0 +1,657 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PersistenceService } from 'src/common/persistence/persistence.service'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { AuctionEntity } from 'src/db/auctions'; +import { MarketplacesService } from './marketplaces.service'; +import { AuctionsSetterService } from '../auctions'; +import { AssetActionEnum } from '../assets/models'; +import { AssetOfferEnum } from '../assets/models/AssetOfferEnum'; +import { UsdPriceService } from '../usdPrice/usd-price.service'; +import { MarketplacesReindexEventsSummaryService } from './marketplaces-reindex-events-summary.service'; +import { ReindexAuctionStartedHandler } from './marketplaces-reindex-handlers/reindex-auction-started.handler'; +import { ReindexAuctionBidHandler } from './marketplaces-reindex-handlers/reindex-auction-bid.handler'; +import { ReindexAuctionBoughtHandler } from './marketplaces-reindex-handlers/reindex-auction-bought.handler'; +import { ReindexAuctionEndedHandler } from './marketplaces-reindex-handlers/reindex-auction-ended.handler'; +import { ReindexAuctionClosedHandler } from './marketplaces-reindex-handlers/reindex-auction-closed.handler'; +import { ReindexOfferAcceptedHandler } from './marketplaces-reindex-handlers/reindex-offer-accepted.handler'; +import { ReindexOfferClosedHandler } from './marketplaces-reindex-handlers/reindex-offer-closed.handler'; +import { ReindexOfferCreatedHandler } from './marketplaces-reindex-handlers/reindex-offer-created.hander'; +import { ReindexAuctionPriceUpdatedHandler } from './marketplaces-reindex-handlers/reindex-auction-price-updated.handler'; +import { ReindexGlobalOfferAcceptedHandler } from './marketplaces-reindex-handlers/reindex-global-offer-accepted.handler'; +import { ReindexAuctionUpdatedHandler } from './marketplaces-reindex-handlers/reindex-auction-updated.handler'; +import { constants, mxConfig } from 'src/config'; +import { MarketplaceReindexState } from './models/MarketplaceReindexState'; +import { MxApiService } from 'src/common'; +import { MarketplaceReindexDataArgs } from './models/MarketplaceReindexDataArgs'; +import { ELRONDNFTSWAP_KEY } from 'src/utils/constants'; +import { DateUtils } from 'src/utils/date-utils'; +import { Locker } from '@multiversx/sdk-nestjs'; +import { TagEntity } from 'src/db/auctions/tags.entity'; +import { MarketplaceTypeEnum } from './models/MarketplaceType.enum'; +import { Token } from '../usdPrice/Token.model'; + +@Injectable() +export class MarketplacesReindexService { + constructor( + private readonly persistenceService: PersistenceService, + private readonly marketplacesService: MarketplacesService, + private readonly usdPriceService: UsdPriceService, + private readonly auctionSetterService: AuctionsSetterService, + private readonly marketplacesReindexEventsSummaryService: MarketplacesReindexEventsSummaryService, + private readonly reindexAuctionStartedHandler: ReindexAuctionStartedHandler, + private readonly reindexAuctionBidHandler: ReindexAuctionBidHandler, + private readonly reindexAuctionBoughtHandler: ReindexAuctionBoughtHandler, + private readonly reindexAuctionEndedHandler: ReindexAuctionEndedHandler, + private readonly reindexAuctionClosedHandler: ReindexAuctionClosedHandler, + private readonly reindexAuctionPriceUpdatedHandler: ReindexAuctionPriceUpdatedHandler, + private readonly reindexAuctionUpdatedHandler: ReindexAuctionUpdatedHandler, + private readonly reindexOfferCreatedHandler: ReindexOfferCreatedHandler, + private readonly reindexOfferAcceptedHandler: ReindexOfferAcceptedHandler, + private readonly reindexOfferClosedHandler: ReindexOfferClosedHandler, + private readonly reindexGlobalOfferAcceptedHandler: ReindexGlobalOfferAcceptedHandler, + private readonly mxApiService: MxApiService, + private readonly logger: Logger, + ) {} + + async reindexMarketplaceData( + input: MarketplaceReindexDataArgs, + ): Promise { + await Locker.lock( + `Reindex marketplace data/state for ${input.marketplaceAddress}`, + async () => { + try { + let marketplaceReindexStates: MarketplaceReindexState[] = + await this.getInitialMarketplaceReindexStates(input); + + await this.processMarketplaceEventsInBatches( + marketplaceReindexStates, + input, + ); + + marketplaceReindexStates.map((s) => + s.setStateItemsToExpiredIfOlderThanTimestamp( + input.beforeTimestamp ?? DateUtils.getCurrentTimestamp(), + ), + ); + + for (let state of marketplaceReindexStates) { + await this.populateAuctionAssetTags(state.auctions); + await this.addMarketplaceStateToDb(state); + } + + this.logger.log( + `Reindexing marketplace data/state for ${input.marketplaceAddress} ended`, + ); + } catch (error) { + this.logger.error( + 'An error occurred while reindexing marketplace data', + { + path: `${MarketplacesReindexService.name}.${this.reindexMarketplaceData.name}`, + marketplaceAddress: input.marketplaceAddress, + exception: error, + }, + ); + } + }, + true, + ); + } + + private async getInitialMarketplaceReindexStates( + input: MarketplaceReindexDataArgs, + ): Promise { + let marketplaceReindexStates: MarketplaceReindexState[] = []; + + const marketplace = await this.marketplacesService.getMarketplaceByAddress( + input.marketplaceAddress, + ); + + if (marketplace.type === MarketplaceTypeEnum.External) { + return [ + new MarketplaceReindexState({ + marketplace, + isReindexFromTheBeginning: input.afterTimestamp ? false : true, + }), + ]; + } + + const internalMarketplaces = + await this.marketplacesService.getInternalMarketplacesByAddress( + marketplace.address, + ); + + for (let i = 0; i < internalMarketplaces.length; i++) { + const marketplaceCollections = + await this.marketplacesService.getCollectionsByMarketplace( + internalMarketplaces[i].key, + ); + + marketplaceReindexStates.push( + new MarketplaceReindexState({ + marketplace: internalMarketplaces[i], + isReindexFromTheBeginning: input.afterTimestamp ? false : true, + listedCollections: marketplaceCollections, + }), + ); + } + + return marketplaceReindexStates; + } + + private async processMarketplaceEventsInBatches( + marketplaceReindexStates: MarketplaceReindexState[], + input: MarketplaceReindexDataArgs, + ): Promise { + let afterTimestamp = input.afterTimestamp; + let processInNextBatch: MarketplaceEventsEntity[] = []; + let nextBatchPromise: Promise; + + nextBatchPromise = this.persistenceService.getMarketplaceEventsAsc( + marketplaceReindexStates[0].marketplace.address, + afterTimestamp, + ); + + do { + let batch = await nextBatchPromise; + if (!batch || batch.length === 0) { + break; + } + [batch, afterTimestamp] = + this.getSlicedBatchAndNewestTimestampIfPartialEventsSet( + batch, + input.beforeTimestamp, + ); + + nextBatchPromise = this.persistenceService.getMarketplaceEventsAsc( + marketplaceReindexStates[0].marketplace.address, + afterTimestamp, + ); + + processInNextBatch = + await this.processEventsBatchAndReturnUnprocessedEvents( + marketplaceReindexStates, + processInNextBatch.concat(batch), + ); + } while ( + input.beforeTimestamp ? afterTimestamp < input.beforeTimestamp : true + ); + + const isFinalBatch = true; + processInNextBatch = + await this.processEventsBatchAndReturnUnprocessedEvents( + marketplaceReindexStates, + processInNextBatch, + isFinalBatch, + ); + + if (processInNextBatch.length > 0) { + this.logger.warn(`Could not handle ${processInNextBatch.length} events`); + } + } + + private getSlicedBatchAndNewestTimestampIfPartialEventsSet( + eventsBatch: MarketplaceEventsEntity[], + beforeTimestamp: number, + ): [MarketplaceEventsEntity[], number] { + const oldestTimestamp = eventsBatch[0].timestamp; + let newestTimestamp = eventsBatch[eventsBatch.length - 1].timestamp; + + if (newestTimestamp !== oldestTimestamp) { + eventsBatch = eventsBatch.slice( + 0, + eventsBatch.findIndex((event) => event.timestamp === newestTimestamp), + ); + newestTimestamp = eventsBatch[eventsBatch.length - 1].timestamp; + } + + if (newestTimestamp > beforeTimestamp) { + return [ + eventsBatch.filter((e) => e.timestamp < beforeTimestamp), + beforeTimestamp, + ]; + } + + return [eventsBatch, newestTimestamp]; + } + + private async processEventsBatchAndReturnUnprocessedEvents( + marketplaceReindexStates: MarketplaceReindexState[], + batch: MarketplaceEventsEntity[], + isFinalBatch?: boolean, + ): Promise { + let unprocessedEvents: MarketplaceEventsEntity[] = [...batch]; + + while (unprocessedEvents.length > 0) { + const txHash = unprocessedEvents[0].txHash; + + const eventOrdersAndTx = unprocessedEvents.filter( + (event) => event.txHash === txHash || event.originalTxHash === txHash, + ); + + const isAnotherEventsSetInBatch = unprocessedEvents.find( + (e) => e.timestamp > unprocessedEvents[0].timestamp, + ); + + if ( + !isFinalBatch && + (eventOrdersAndTx.length === unprocessedEvents.length || + !isAnotherEventsSetInBatch) + ) { + return unprocessedEvents; + } + + unprocessedEvents = unprocessedEvents.filter( + (event) => event.txHash !== txHash && event.originalTxHash !== txHash, + ); + + await this.processEventsSet(marketplaceReindexStates, eventOrdersAndTx); + } + + return []; + } + + private async processEventsSet( + marketplaceReindexStates: MarketplaceReindexState[], + eventOrdersAndTx: MarketplaceEventsEntity[], + ): Promise { + const eventsSetSummaries = + this.marketplacesReindexEventsSummaryService.getEventsSetSummaries( + marketplaceReindexStates[0].marketplace, + eventOrdersAndTx, + ); + + if (!marketplaceReindexStates[0].isReindexFromTheBeginning) { + await Promise.all( + marketplaceReindexStates.map((state) => { + return this.getStateFromDbIfMissing(state, eventsSetSummaries); + }), + ); + } + + const areMultipleMarketplaces = marketplaceReindexStates.length !== 1; + + for (let i = 0; i < eventsSetSummaries?.length; i++) { + try { + if (!areMultipleMarketplaces) { + await this.processEvent( + marketplaceReindexStates[0], + eventsSetSummaries[i], + ); + continue; + } + + const stateIndex = marketplaceReindexStates.findIndex((s) => + s.isCollectionListed(eventsSetSummaries[i].collection), + ); + if (stateIndex !== -1) { + await this.processEvent( + marketplaceReindexStates[stateIndex], + eventsSetSummaries[i], + ); + } + } catch (error) { + this.logger.warn( + `Error reprocessing marketplace event ${JSON.stringify( + eventsSetSummaries[i], + )} - ${JSON.stringify(error)}`, + ); + } + } + } + + private async getStateFromDbIfMissing( + marketplaceReindexState: MarketplaceReindexState, + eventsSetSummaries: any[], + ): Promise { + const [missingAuctionIds, missingOfferIds, missingStateForIdentifiers] = + this.getMissingStateIds(marketplaceReindexState, eventsSetSummaries); + + let auctions: AuctionEntity[]; + if (missingAuctionIds.length > 0) { + auctions = + await this.persistenceService.getBulkAuctionsByAuctionIdsAndMarketplace( + missingAuctionIds, + marketplaceReindexState.marketplace.key, + ); + } else if (missingStateForIdentifiers.length > 0) { + auctions = + await this.persistenceService.getBulkAuctionsByIdentifierAndMarketplace( + missingStateForIdentifiers, + marketplaceReindexState.marketplace.key, + ); + } + + const auctionIds = auctions?.map((a) => a.id); + const [orders, offers] = await Promise.all([ + auctionIds?.length > 0 + ? this.persistenceService.getOrdersByAuctionIds(auctionIds) + : [], + missingOfferIds?.length > 0 + ? this.persistenceService.getBulkOffersByOfferIdsAndMarketplace( + missingOfferIds, + marketplaceReindexState.marketplace.key, + ) + : [], + ]); + + if (orders?.length > 0) { + marketplaceReindexState.orders = + marketplaceReindexState.orders.concat(orders); + } + if (offers?.length > 0) { + marketplaceReindexState.offers = + marketplaceReindexState.offers.concat(offers); + } + } + + private getMissingStateIds( + marketplaceReindexState: MarketplaceReindexState, + eventsSetSummaries: any[], + ): [number[], number[], string[]] { + let missingAuctionIds: number[] = []; + let missingOfferIds: number[] = []; + let missingStateForIdentifiers: string[] = []; + + for (let i = 0; i < eventsSetSummaries?.length; i++) { + if (!eventsSetSummaries[i]) { + continue; + } + + if ( + eventsSetSummaries[i].auctionId && + eventsSetSummaries[i].action !== AssetActionEnum.StartedAuction + ) { + const auctionIndex = + marketplaceReindexState.marketplace.key !== ELRONDNFTSWAP_KEY + ? marketplaceReindexState.getAuctionIndexByAuctionId( + eventsSetSummaries[i].auctionId, + ) + : marketplaceReindexState.getAuctionIndexByIdentifier( + eventsSetSummaries[i].identifier, + ); + if (auctionIndex === -1) { + marketplaceReindexState.marketplace.key !== ELRONDNFTSWAP_KEY + ? missingAuctionIds.push(eventsSetSummaries[i].auctionId) + : missingStateForIdentifiers.push(eventsSetSummaries[i].identifier); + } + } + + if ( + eventsSetSummaries[i].offerId && + eventsSetSummaries[i].action !== AssetOfferEnum.Created + ) { + const offerIndex = marketplaceReindexState.getOfferIndexByOfferId( + eventsSetSummaries[i].offerId, + ); + if (offerIndex === -1) { + missingOfferIds.push(eventsSetSummaries[i].offerId); + } + } + } + + return [ + [...new Set(missingAuctionIds)], + [...new Set(missingOfferIds)], + [...new Set(missingStateForIdentifiers)], + ]; + } + + private async processEvent( + marketplaceReindexState: MarketplaceReindexState, + eventsSetSummary: any, + ): Promise { + if (!eventsSetSummary) { + return; + } + switch (eventsSetSummary.action) { + case AssetActionEnum.StartedAuction: { + const [paymentToken, paymentNonce] = await this.getPaymentTokenAndNonce( + marketplaceReindexState, + eventsSetSummary, + ); + this.reindexAuctionStartedHandler.handle( + eventsSetSummary, + marketplaceReindexState, + paymentToken, + paymentNonce, + ); + break; + } + case AssetActionEnum.Bid: { + const [paymentToken, paymentNonce] = await this.getPaymentTokenAndNonce( + marketplaceReindexState, + eventsSetSummary, + ); + this.reindexAuctionBidHandler.handle( + marketplaceReindexState, + eventsSetSummary, + paymentToken, + paymentNonce, + ); + break; + } + case AssetActionEnum.Bought: { + const [paymentToken, paymentNonce] = await this.getPaymentTokenAndNonce( + marketplaceReindexState, + eventsSetSummary, + ); + this.reindexAuctionBoughtHandler.handle( + marketplaceReindexState, + eventsSetSummary, + paymentToken, + paymentNonce, + ); + break; + } + case AssetActionEnum.EndedAuction: { + const [paymentToken] = await this.getPaymentTokenAndNonce( + marketplaceReindexState, + eventsSetSummary, + ); + this.reindexAuctionEndedHandler.handle( + marketplaceReindexState, + eventsSetSummary, + paymentToken, + ); + break; + } + case AssetActionEnum.ClosedAuction: { + this.reindexAuctionClosedHandler.handle( + marketplaceReindexState, + eventsSetSummary, + ); + break; + } + case AssetActionEnum.PriceUpdated: { + const [paymentToken] = await this.getPaymentTokenAndNonce( + marketplaceReindexState, + eventsSetSummary, + ); + this.reindexAuctionPriceUpdatedHandler.handle( + marketplaceReindexState, + eventsSetSummary, + paymentToken.decimals, + ); + break; + } + case AssetActionEnum.Updated: { + const paymentToken = await this.usdPriceService.getToken( + eventsSetSummary.paymentToken, + ); + this.reindexAuctionUpdatedHandler.handle( + marketplaceReindexState, + eventsSetSummary, + paymentToken.decimals, + ); + break; + } + case AssetOfferEnum.Created: { + const [paymentToken] = await this.getPaymentTokenAndNonce( + marketplaceReindexState, + eventsSetSummary, + ); + this.reindexOfferCreatedHandler.handle( + marketplaceReindexState, + eventsSetSummary, + paymentToken.decimals, + ); + break; + } + case AssetOfferEnum.Accepted: { + this.reindexOfferAcceptedHandler.handle( + marketplaceReindexState, + eventsSetSummary, + ); + break; + } + case AssetOfferEnum.GloballyAccepted: { + this.reindexGlobalOfferAcceptedHandler.handle( + marketplaceReindexState, + eventsSetSummary, + ); + break; + } + case AssetOfferEnum.Closed: { + this.reindexOfferClosedHandler.handle( + marketplaceReindexState, + eventsSetSummary, + ); + break; + } + default: { + if (eventsSetSummary.auctionType) { + throw new Error(`Case not handled ${eventsSetSummary.auctionType}`); + } + } + } + } + + private async getPaymentTokenAndNonce( + marketplaceReindexState: MarketplaceReindexState, + input: any, + ): Promise<[Token, number]> { + try { + const auctionIndex = + marketplaceReindexState.marketplace.key !== ELRONDNFTSWAP_KEY + ? marketplaceReindexState.getAuctionIndexByAuctionId(input.auctionId) + : marketplaceReindexState.getAuctionIndexByIdentifier( + input.identifier, + ); + const paymentNonceValue = + input.paymentNonce ?? + marketplaceReindexState.auctions[auctionIndex]?.paymentNonce; + const paymentNonce = !Number.isNaN(paymentNonceValue) + ? paymentNonceValue + : 0; + const paymentTokenIdentifier = + input.paymentToken ?? + marketplaceReindexState.auctions[auctionIndex]?.paymentToken; + if (paymentTokenIdentifier === mxConfig.egld) { + return [ + new Token({ + identifier: mxConfig.egld, + decimals: mxConfig.decimals, + }), + 0, + ]; + } + const paymentToken = await this.usdPriceService.getToken( + paymentTokenIdentifier, + ); + if (!paymentToken) { + return [ + new Token({ + identifier: paymentTokenIdentifier ?? mxConfig.egld, + decimals: mxConfig.decimals, + }), + paymentNonce, + ]; + } + return [paymentToken, paymentNonce]; + } catch { + return [undefined, undefined]; + } + } + + private async addMarketplaceStateToDb( + marketplaceReindexState: MarketplaceReindexState, + ): Promise { + marketplaceReindexState.auctions.map((a) => { + delete a.id; + if (a.startDate > constants.dbMaxTimestamp) { + a.startDate = constants.dbMaxTimestamp; + } + if (a.endDate > constants.dbMaxTimestamp) { + a.endDate = constants.dbMaxTimestamp; + } + }); + marketplaceReindexState.orders.map((o) => delete o.id); + marketplaceReindexState.offers.map((o) => delete o.id); + + await this.auctionSetterService.saveBulkAuctions( + marketplaceReindexState.auctions, + ); + + for (let i = 0; i < marketplaceReindexState.orders.length; i++) { + marketplaceReindexState.orders[i].auction = + marketplaceReindexState.auctions[ + marketplaceReindexState.orders[i].auctionId + ]; + marketplaceReindexState.orders[i].auctionId = + marketplaceReindexState.auctions[ + marketplaceReindexState.orders[i].auctionId + ].id; + } + + let tags: TagEntity[] = []; + marketplaceReindexState.auctions.map((auction) => { + const assetTags = auction.tags.split(','); + assetTags.map((assetTag) => { + if (assetTag !== '') { + tags.push( + new TagEntity({ + auctionId: auction.id, + tag: assetTag.trim().slice(0, 20), + auction: auction, + }), + ); + } + }); + }); + + await this.persistenceService.saveTags(tags); + await this.persistenceService.saveBulkOrders( + marketplaceReindexState.orders, + ); + await this.persistenceService.saveBulkOffers( + marketplaceReindexState.offers, + ); + } + + private async populateAuctionAssetTags( + auctions: AuctionEntity[], + ): Promise { + const batchSize = constants.getNftsFromApiBatchSize; + for (let i = 0; i < auctions.length; i += batchSize) { + const assetsWithNoTagsIdentifiers = [ + ...new Set( + auctions + .slice(i, i + batchSize) + .filter((a) => a.tags === '') + .map((a) => a.identifier), + ), + ]; + const assets = await this.mxApiService.getNftsByIdentifiers( + assetsWithNoTagsIdentifiers, + 0, + 'fields=identifier,tags', + ); + const tags = assets.filter((a) => a.tags); + for (let j = 0; j < assets?.length; j++) { + auctions + .filter((a) => a.identifier === assets[j].identifier) + .map((a) => (a.tags = assets[j]?.tags?.join(',') ?? '')); + } + } + } +} diff --git a/src/modules/marketplaces/marketplaces.elastic.queries.ts b/src/modules/marketplaces/marketplaces.elastic.queries.ts index c5dca061c..7af80fa16 100644 --- a/src/modules/marketplaces/marketplaces.elastic.queries.ts +++ b/src/modules/marketplaces/marketplaces.elastic.queries.ts @@ -27,6 +27,7 @@ export const getMarketplaceTransactionsElasticQuery = ( 'timestamp', new RangeLowerThan(input.txTimestampDelimiter), ) + .withRangeFilter('timestamp', new RangeLowerThan(input.beforeTimestamp)) .withRangeFilter('timestamp', new RangeGreaterThan(input.afterTimestamp)) .withSort([{ name: 'timestamp', order: ElasticSortOrder.descending }]) .withFields([ diff --git a/src/modules/marketplaces/marketplaces.module.ts b/src/modules/marketplaces/marketplaces.module.ts index 8a14ca622..2beba6472 100644 --- a/src/modules/marketplaces/marketplaces.module.ts +++ b/src/modules/marketplaces/marketplaces.module.ts @@ -11,6 +11,20 @@ import { MarketplaceProvider } from './loaders/marketplace.loader'; import { MarketplaceRedisHandler } from './loaders/marketplace.redis-handler'; import { MarketplaceEventsIndexingService } from './marketplaces-events-indexing.service'; import { OffersModuleGraph } from '../offers/offers.module'; +import { AssetByIdentifierService } from '../assets'; +import { MarketplacesReindexService } from './marketplaces-reindex.service'; +import { ReindexAuctionStartedHandler } from './marketplaces-reindex-handlers/reindex-auction-started.handler'; +import { ReindexAuctionBidHandler } from './marketplaces-reindex-handlers/reindex-auction-bid.handler'; +import { ReindexAuctionBoughtHandler } from './marketplaces-reindex-handlers/reindex-auction-bought.handler'; +import { ReindexAuctionEndedHandler } from './marketplaces-reindex-handlers/reindex-auction-ended.handler'; +import { ReindexAuctionClosedHandler } from './marketplaces-reindex-handlers/reindex-auction-closed.handler'; +import { ReindexOfferCreatedHandler } from './marketplaces-reindex-handlers/reindex-offer-created.hander'; +import { ReindexOfferAcceptedHandler } from './marketplaces-reindex-handlers/reindex-offer-accepted.handler'; +import { ReindexOfferClosedHandler } from './marketplaces-reindex-handlers/reindex-offer-closed.handler'; +import { MarketplacesReindexEventsSummaryService } from './marketplaces-reindex-events-summary.service'; +import { ReindexAuctionPriceUpdatedHandler } from './marketplaces-reindex-handlers/reindex-auction-price-updated.handler'; +import { ReindexGlobalOfferAcceptedHandler } from './marketplaces-reindex-handlers/reindex-global-offer-accepted.handler'; +import { ReindexAuctionUpdatedHandler } from './marketplaces-reindex-handlers/reindex-auction-updated.handler'; @Module({ providers: [ @@ -22,6 +36,20 @@ import { OffersModuleGraph } from '../offers/offers.module'; MarketplaceProvider, MarketplaceRedisHandler, MarketplaceEventsIndexingService, + AssetByIdentifierService, + MarketplacesReindexService, + MarketplacesReindexEventsSummaryService, + ReindexAuctionStartedHandler, + ReindexAuctionBidHandler, + ReindexAuctionBoughtHandler, + ReindexAuctionEndedHandler, + ReindexAuctionClosedHandler, + ReindexAuctionPriceUpdatedHandler, + ReindexAuctionUpdatedHandler, + ReindexOfferCreatedHandler, + ReindexOfferAcceptedHandler, + ReindexOfferClosedHandler, + ReindexGlobalOfferAcceptedHandler, ], imports: [ PubSubListenerModule, @@ -30,6 +58,10 @@ import { OffersModuleGraph } from '../offers/offers.module'; forwardRef(() => AuctionsModuleGraph), forwardRef(() => OffersModuleGraph), ], - exports: [MarketplacesService, MarketplaceEventsIndexingService], + exports: [ + MarketplacesService, + MarketplaceEventsIndexingService, + MarketplacesReindexService, + ], }) export class MarketplacesModuleGraph {} diff --git a/src/modules/marketplaces/marketplaces.resolver.ts b/src/modules/marketplaces/marketplaces.resolver.ts index e29f1a853..2c4fa2ab8 100644 --- a/src/modules/marketplaces/marketplaces.resolver.ts +++ b/src/modules/marketplaces/marketplaces.resolver.ts @@ -8,8 +8,8 @@ import { MarketplacesResponse } from './models'; import { NftMarketplaceAbiService } from '../auctions/nft-marketplace.abi.service'; import { MarketplaceFilters } from './models/Marketplace.Filter'; import { MarketplaceTypeEnum } from './models/MarketplaceType.enum'; -import { Token } from 'src/common/services/mx-communication/models/Token.model'; import { UsdPriceService } from '../usdPrice/usd-price.service'; +import { Token } from '../usdPrice/Token.model'; @Resolver(() => Marketplace) export class MarketplacesResolver extends BaseResolver(Marketplace) { diff --git a/src/modules/marketplaces/marketplaces.service.ts b/src/modules/marketplaces/marketplaces.service.ts index 319dcf589..fb5eeea8c 100644 --- a/src/modules/marketplaces/marketplaces.service.ts +++ b/src/modules/marketplaces/marketplaces.service.ts @@ -63,13 +63,15 @@ export class MarketplacesService { }); } - async getInternalMarketplacesAddreses(): Promise { - let allMarketplaces = await this.getAllMarketplaces(); - - const internalMarketplaces = allMarketplaces?.items?.filter( - (m) => m.type === MarketplaceTypeEnum.Internal, - ); + async getInternalMarketplacesByAddress( + address: string, + ): Promise { + const internalMarketplaces = await this.getInternalMarketplaces(); + return internalMarketplaces.filter((m) => m.address === address); + } + async getInternalMarketplacesAddreses(): Promise { + const internalMarketplaces = await this.getInternalMarketplaces(); return internalMarketplaces.map((m) => m.address); } @@ -154,6 +156,14 @@ export class MarketplacesService { ); } + private async getInternalMarketplaces(): Promise { + let allMarketplaces = await this.getAllMarketplaces(); + const internalMarketplaces = allMarketplaces?.items?.filter( + (m) => m.type === MarketplaceTypeEnum.Internal, + ); + return internalMarketplaces; + } + async getMarketplacesFromDb(): Promise> { let [campaigns, count]: [MarketplaceEntity[], number] = await this.persistenceService.getMarketplaces(); diff --git a/src/modules/marketplaces/models/Marketplace.dto.ts b/src/modules/marketplaces/models/Marketplace.dto.ts index 1f5154d12..b48beab62 100644 --- a/src/modules/marketplaces/models/Marketplace.dto.ts +++ b/src/modules/marketplaces/models/Marketplace.dto.ts @@ -1,12 +1,13 @@ import { ObjectType, Field, ID } from '@nestjs/graphql'; -import { Token } from 'src/common/services/mx-communication/models/Token.model'; import { mxConfig } from 'src/config'; import { MarketplaceEntity } from 'src/db/marketplaces'; import { NftTypeEnum } from 'src/modules/assets/models'; +import { Token } from 'src/modules/usdPrice/Token.model'; import { DEADRARE_KEY, ELRONDNFTSWAP_KEY, FRAMEIT_KEY, + ICI_KEY, XOXNO_KEY, } from 'src/utils/constants'; import { getCollectionAndNonceFromIdentifier } from 'src/utils/helpers'; @@ -110,6 +111,8 @@ export class Marketplace { ? `${entity.url}${identifier}/${marketplaceAuctionId}` : entity.url; + case ICI_KEY: + return `${entity.url}${identifier}`; case DEADRARE_KEY: return `${entity.url}${identifier}`; default: diff --git a/src/modules/marketplaces/models/MarketplaceEventLogInput.ts b/src/modules/marketplaces/models/MarketplaceEventLogInput.ts new file mode 100644 index 000000000..dbf1ccd14 --- /dev/null +++ b/src/modules/marketplaces/models/MarketplaceEventLogInput.ts @@ -0,0 +1,63 @@ +import { ObjectType } from '@nestjs/graphql'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { AssetActionEnum } from 'src/modules/assets/models'; +import { AssetOfferEnum } from 'src/modules/assets/models/AssetOfferEnum'; +import { AuctionTypeEnum } from 'src/modules/auctions/models'; + +@ObjectType() +export class MarketplaceEventLogInput { + action: AssetActionEnum | AssetOfferEnum; + blockHash?: string; + timestamp: number; + address: string; + sender: string; + + identifier: string; + collection: string; + nonce: string; + itemsCount: string; + + startTime?: number; + endTime?: number; + + auctionId: string; + auctionType?: AuctionTypeEnum; + + offerId: string; + + paymentToken?: string; + paymentNonce?: number; + + price: string; + minBid?: string; + maxBid?: string; + minBidDiff?: string; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + static fromInternalMarketplaceEventAndTx( + events: MarketplaceEventsEntity[], + eventType: string, + index: number, + ): MarketplaceEventLogInput { + throw new Error('Not implemented yet'); + } + + static fromExternalMarketplaceEventAndTx( + events: MarketplaceEventsEntity[], + eventType: string, + index: number, + ): MarketplaceEventLogInput { + throw new Error('Not implemented yet'); + } + + static fromElrondNftSwapMarketplaceEventAndTx( + events: MarketplaceEventsEntity[], + eventType: string, + index: number, + ): MarketplaceEventLogInput { + throw new Error('Not implemented yet'); + } +} diff --git a/src/modules/marketplaces/models/MarketplaceReindexDataArgs.ts b/src/modules/marketplaces/models/MarketplaceReindexDataArgs.ts new file mode 100644 index 000000000..a04fce773 --- /dev/null +++ b/src/modules/marketplaces/models/MarketplaceReindexDataArgs.ts @@ -0,0 +1,13 @@ +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class MarketplaceReindexDataArgs { + @Field(() => String) + marketplaceAddress: string; + + @Field(() => Number, { nullable: true }) + beforeTimestamp: number; + + @Field(() => Number, { nullable: true }) + afterTimestamp: number; +} diff --git a/src/modules/marketplaces/models/MarketplaceReindexState.ts b/src/modules/marketplaces/models/MarketplaceReindexState.ts new file mode 100644 index 000000000..269602e7f --- /dev/null +++ b/src/modules/marketplaces/models/MarketplaceReindexState.ts @@ -0,0 +1,168 @@ +import { ObjectType } from '@nestjs/graphql'; +import BigNumber from 'bignumber.js'; +import { AuctionEntity } from 'src/db/auctions'; +import { OfferEntity } from 'src/db/offers'; +import { OrderEntity } from 'src/db/orders'; +import { + AuctionStatusEnum, + AuctionTypeEnum, +} from 'src/modules/auctions/models'; +import { OfferStatusEnum } from 'src/modules/offers/models'; +import { OrderStatusEnum } from 'src/modules/orders/models'; +import { Token } from 'src/modules/usdPrice/Token.model'; +import { BigNumberUtils } from 'src/utils/bigNumber-utils'; +import { DateUtils } from 'src/utils/date-utils'; +import { Marketplace } from './Marketplace.dto'; + +@ObjectType() +export class MarketplaceReindexState { + marketplace: Marketplace; + isReindexFromTheBeginning: boolean; + listedCollections: string[] = []; + auctions: AuctionEntity[] = []; + orders: OrderEntity[] = []; + offers: OfferEntity[] = []; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + isCollectionListed(collection: string): boolean { + return this.listedCollections.includes(collection); + } + + getAuctionIndexByAuctionId(auctionId: number): number { + return this.auctions.findIndex((a) => a.marketplaceAuctionId === auctionId); + } + + getAuctionIndexByIdentifier(identifier: string): number { + return this.auctions.findIndex( + (a) => + a.identifier === identifier && a.status === AuctionStatusEnum.Running, + ); + } + + getOfferIndexByOfferId(offerId: number): number { + return this.offers.findIndex((o) => o.marketplaceOfferId === offerId); + } + + createOrder( + auctionIndex: number, + input: any, + status: OrderStatusEnum, + paymentToken: Token, + paymentNonce?: number, + ): OrderEntity { + const modifiedDate = DateUtils.getUtcDateFromTimestamp(input.timestamp); + const price = input.price ?? input.currentBid; + return new OrderEntity({ + id: this.orders.length, + creationDate: modifiedDate, + modifiedDate, + auctionId: this.auctions[auctionIndex].id, + ownerAddress: input.address, + priceToken: paymentToken.identifier, + priceNonce: paymentNonce ?? 0, + priceAmount: price !== '0' ? price : this.auctions[auctionIndex].maxBid, + priceAmountDenominated: + price !== '0' + ? BigNumberUtils.denominateAmount(price, paymentToken.decimals) + : this.auctions[auctionIndex].maxBidDenominated, + blockHash: input.blockHash ?? '', + marketplaceKey: this.marketplace.key, + boughtTokensNo: + this.auctions[auctionIndex].type === AuctionTypeEnum.Nft + ? null + : input.itemsCount, + status: status, + }); + } + + setAuctionOrderWinnerStatusAndReturnId( + auctionId: number, + status: OrderStatusEnum, + modifiedDate?: Date, + ): number { + const bids = this.orders + .filter( + (o) => o.auctionId === auctionId && o.status === OrderStatusEnum.Active, + ) + .map((o) => new BigNumber(o.priceAmount)); + + if (bids.length) { + const maxBid = BigNumber.max(...bids); + const winnerOrderIndex = this.orders.findIndex( + (o) => + o.auctionId === auctionId && + o.status === OrderStatusEnum.Active && + o.priceAmount === maxBid.toString(), + ); + this.orders[winnerOrderIndex].status = status; + if (modifiedDate) { + this.orders[winnerOrderIndex].modifiedDate = modifiedDate; + } + return this.orders[winnerOrderIndex].id; + } + return -1; + } + + setInactiveOrdersForAuction( + auctionId: number, + modifiedDate: Date, + exceptWinnerId?: number, + ): void { + this.orders + ?.filter( + (o) => + o.auctionId === auctionId && + o.status === OrderStatusEnum.Active && + o.id !== exceptWinnerId, + ) + ?.map((o) => { + o.status = OrderStatusEnum.Inactive; + o.modifiedDate = modifiedDate; + }); + } + + setStateItemsToExpiredIfOlderThanTimestamp(timestamp: number): void { + this.setAuctionsAndOrdersToExpiredIfOlderThanTimestamp(timestamp); + this.setOffersToExpiredIfOlderThanTimestamp(timestamp); + } + + private setAuctionsAndOrdersToExpiredIfOlderThanTimestamp( + timestamp: number, + ): void { + const runningAuctions = this.auctions?.filter( + (a) => a.status === AuctionStatusEnum.Running, + ); + for (let i = 0; i < runningAuctions.length; i++) { + if ( + runningAuctions[i].endDate > 0 && + runningAuctions[i].endDate < timestamp + ) { + runningAuctions[i].status = AuctionStatusEnum.Claimable; + const winnerOrderId = this.setAuctionOrderWinnerStatusAndReturnId( + runningAuctions[i].id, + OrderStatusEnum.Active, + ); + this.setInactiveOrdersForAuction( + runningAuctions[i].id, + DateUtils.getUtcDateFromTimestamp(runningAuctions[i].endDate), + winnerOrderId, + ); + } + } + } + + private setOffersToExpiredIfOlderThanTimestamp(timestamp: number): void { + for (let i = 0; i < this.offers.length; i++) { + if ( + this.offers[i].status === OfferStatusEnum.Active && + this.offers[i].endDate && + this.offers[i].endDate < timestamp + ) { + this.offers[i].status = OfferStatusEnum.Expired; + } + } + } +} diff --git a/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionBidSummary.ts b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionBidSummary.ts new file mode 100644 index 000000000..bab1d1aa6 --- /dev/null +++ b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionBidSummary.ts @@ -0,0 +1,69 @@ +import { BinaryUtils } from '@multiversx/sdk-nestjs'; +import { ObjectType } from '@nestjs/graphql'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { AssetActionEnum } from 'src/modules/assets/models'; +import { BidEvent } from 'src/modules/rabbitmq/entities/auction'; +import { ElrondSwapBidEvent } from 'src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-bid.event'; +import { GenericEvent } from 'src/modules/rabbitmq/entities/generic.event'; +import { ELRONDNFTSWAP_KEY } from 'src/utils/constants'; +import { MarketplaceTransactionData } from '../marketplaceEventAndTxData.dto'; +import { ReindexGenericSummary } from './ReindexGenericSummary'; + +@ObjectType() +export class AuctionBidSummary extends ReindexGenericSummary { + auctionId: number; + + identifier: string; + collection: string; + nonce: string; + itemsCount: string; + + price: string; + + constructor(init?: Partial) { + super(init); + Object.assign(this, init); + } + + static fromBidEventAndTx( + event: MarketplaceEventsEntity, + tx: MarketplaceTransactionData, + marketplaceKey: string, + ): AuctionBidSummary { + if (!event || event.hasEventTopicIdentifier('end_auction_event')) { + return; + } + + const address = event.data.eventData?.address ?? tx.receiver; + const topics = this.getTopics(event, marketplaceKey); + + return new AuctionBidSummary({ + timestamp: event.timestamp, + blockHash: tx?.blockHash, + collection: topics.collection, + nonce: topics.nonce, + identifier: `${topics.collection}-${topics.nonce}`, + auctionId: BinaryUtils.hexToNumber(topics.auctionId), + itemsCount: topics.nrAuctionTokens, + address: topics.currentWinner, + sender: address, + price: topics.currentBid, + action: AssetActionEnum.Bid, + }); + } + + private static getTopics( + event: MarketplaceEventsEntity, + marketplaceKey: string, + ): any { + const genericEvent = event.data.eventData + ? GenericEvent.fromEventResponse(event.data.eventData) + : undefined; + + if (marketplaceKey === ELRONDNFTSWAP_KEY) { + return new ElrondSwapBidEvent(genericEvent).getTopics(); + } + + return new BidEvent(genericEvent).getTopics(); + } +} diff --git a/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionBuySummary.ts b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionBuySummary.ts new file mode 100644 index 000000000..c4abf956c --- /dev/null +++ b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionBuySummary.ts @@ -0,0 +1,94 @@ +import { BinaryUtils } from '@multiversx/sdk-nestjs'; +import { ObjectType } from '@nestjs/graphql'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { + AssetActionEnum, + ElrondNftsSwapAuctionEventEnum, + ExternalAuctionEventEnum, +} from 'src/modules/assets/models'; +import { BuySftEvent } from 'src/modules/rabbitmq/entities/auction'; +import { ClaimEvent } from 'src/modules/rabbitmq/entities/auction/claim.event'; +import { ElrondSwapBuyEvent } from 'src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-buy.event'; +import { GenericEvent } from 'src/modules/rabbitmq/entities/generic.event'; +import { DEADRARE_KEY } from 'src/utils/constants'; +import { Marketplace } from '../Marketplace.dto'; +import { MarketplaceTransactionData } from '../marketplaceEventAndTxData.dto'; +import { ReindexGenericSummary } from './ReindexGenericSummary'; + +@ObjectType() +export class AuctionBuySummary extends ReindexGenericSummary { + auctionId: number; + + identifier: string; + collection: string; + nonce: string; + itemsCount: string; + + price: string; + + constructor(init?: Partial) { + super(init); + Object.assign(this, init); + } + + static fromBuySftEventAndTx( + event: MarketplaceEventsEntity, + tx: MarketplaceTransactionData, + marketplace: Marketplace, + ): AuctionBuySummary { + if ( + event.hasOneOfEventTopicIdentifiers([ + ExternalAuctionEventEnum.UpdateOffer, + ElrondNftsSwapAuctionEventEnum.UpdateListing, + ]) + ) { + return; + } + + const address = event.data.eventData?.address ?? tx.receiver; + const topics = this.getTopics(event, marketplace); + + if (!topics) { + return; + } + + return new AuctionBuySummary({ + timestamp: event.timestamp, + blockHash: tx?.blockHash, + collection: topics.collection, + nonce: topics.nonce, + identifier: `${topics.collection}-${topics.nonce}`, + auctionId: BinaryUtils.hexToNumber(topics.auctionId), + itemsCount: topics.boughtTokens, + action: AssetActionEnum.Bought, + address: topics.currentWinner, + sender: address, + price: topics.bid, + }); + } + + private static getTopics( + event: MarketplaceEventsEntity, + marketplace: Marketplace, + ): any { + const genericEvent = event.data + ? GenericEvent.fromEventResponse(event.data.eventData) + : undefined; + try { + if ( + event.hasEventIdentifier(ExternalAuctionEventEnum.BuyNft) && + marketplace.key !== DEADRARE_KEY + ) { + return new ClaimEvent(genericEvent).getTopics(); + } + + if (event.hasEventIdentifier(ElrondNftsSwapAuctionEventEnum.Purchase)) { + return new ElrondSwapBuyEvent(genericEvent).getTopics(); + } + + return new BuySftEvent(genericEvent).getTopics(); + } catch { + return; + } + } +} diff --git a/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionClosedSummary.ts b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionClosedSummary.ts new file mode 100644 index 000000000..5631691f7 --- /dev/null +++ b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionClosedSummary.ts @@ -0,0 +1,72 @@ +import { BinaryUtils } from '@multiversx/sdk-nestjs'; +import { ObjectType } from '@nestjs/graphql'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { + AssetActionEnum, + ElrondNftsSwapAuctionEventEnum, + ExternalAuctionEventEnum, +} from 'src/modules/assets/models'; +import { AuctionTypeEnum } from 'src/modules/auctions/models'; +import { WithdrawEvent } from 'src/modules/rabbitmq/entities/auction'; +import { ClaimEvent } from 'src/modules/rabbitmq/entities/auction/claim.event'; +import { ElrondSwapWithdrawEvent } from 'src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-withdraw.event'; +import { GenericEvent } from 'src/modules/rabbitmq/entities/generic.event'; +import { MarketplaceTransactionData } from '../marketplaceEventAndTxData.dto'; +import { ReindexGenericSummary } from './ReindexGenericSummary'; + +@ObjectType() +export class ReindexAuctionClosedSummary extends ReindexGenericSummary { + auctionId: number; + auctionType: AuctionTypeEnum; + + identifier: string; + collection: string; + nonce: string; + itemsCount: string; + + constructor(init?: Partial) { + super(init); + Object.assign(this, init); + } + + static fromWithdrawAuctionEventAndTx( + event: MarketplaceEventsEntity, + tx: MarketplaceTransactionData, + ): ReindexAuctionClosedSummary { + if (event.hasEventTopicIdentifier(ExternalAuctionEventEnum.UpdateOffer)) { + return; + } + + const address = event.data.eventData?.address ?? tx.receiver; + const topics = this.getTopics(event); + + return new ReindexAuctionClosedSummary({ + timestamp: event.timestamp, + blockHash: tx?.blockHash, + collection: topics.collection, + nonce: topics.nonce, + identifier: `${topics.collection}-${topics.nonce}`, + auctionId: BinaryUtils.hexToNumber(topics.auctionId), + itemsCount: topics.nrAuctionTokens ?? topics.boughtTokensNo, + address, + sender: topics.originalOwner, + action: AssetActionEnum.ClosedAuction, + }); + } + + private static getTopics(event: MarketplaceEventsEntity): any { + const genericEvent = event.data + ? GenericEvent.fromEventResponse(event.data.eventData) + : undefined; + + if (event.hasEventIdentifier(ExternalAuctionEventEnum.ClaimBackNft)) { + return new ClaimEvent(genericEvent).getTopics(); + } + + if (event.hasEventIdentifier(ElrondNftsSwapAuctionEventEnum.WithdrawSwap)) { + return new ElrondSwapWithdrawEvent(genericEvent).getTopics(); + } + + return new WithdrawEvent(genericEvent).getTopics(); + } +} diff --git a/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionEndedSummary.ts b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionEndedSummary.ts new file mode 100644 index 000000000..9a646f544 --- /dev/null +++ b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionEndedSummary.ts @@ -0,0 +1,53 @@ +import { BinaryUtils } from '@multiversx/sdk-nestjs'; +import { ObjectType } from '@nestjs/graphql'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { AssetActionEnum } from 'src/modules/assets/models'; +import { AuctionTypeEnum } from 'src/modules/auctions/models'; +import { EndAuctionEvent } from 'src/modules/rabbitmq/entities/auction'; +import { GenericEvent } from 'src/modules/rabbitmq/entities/generic.event'; +import { MarketplaceTransactionData } from '../marketplaceEventAndTxData.dto'; +import { ReindexGenericSummary } from './ReindexGenericSummary'; + +@ObjectType() +export class AuctionEndedSummary extends ReindexGenericSummary { + auctionId: number; + auctionType: AuctionTypeEnum; + + identifier: string; + collection: string; + nonce: string; + itemsCount: string; + + currentBid: string; + currentWinner: string; + + constructor(init?: Partial) { + super(init); + Object.assign(this, init); + } + + static fromEndAuctionEventAndTx( + event: MarketplaceEventsEntity, + tx: MarketplaceTransactionData, + ): AuctionEndedSummary { + const address = event.data.eventData?.address ?? tx.receiver; + const genericEvent = event.data + ? GenericEvent.fromEventResponse(event.data.eventData) + : undefined; + const topics = new EndAuctionEvent(genericEvent).getTopics(); + + return new AuctionEndedSummary({ + timestamp: event.timestamp, + blockHash: tx?.blockHash, + collection: topics.collection, + nonce: topics.nonce, + identifier: `${topics.collection}-${topics.nonce}`, + auctionId: BinaryUtils.hexToNumber(topics.auctionId), + itemsCount: topics.nrAuctionTokens, + address, + currentBid: topics.currentBid, + currentWinner: topics.currentWinner, + action: AssetActionEnum.EndedAuction, + }); + } +} diff --git a/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionPriceUpdated.ts b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionPriceUpdated.ts new file mode 100644 index 000000000..dde6c5e84 --- /dev/null +++ b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionPriceUpdated.ts @@ -0,0 +1,70 @@ +import { BinaryUtils } from '@multiversx/sdk-nestjs'; +import { ObjectType } from '@nestjs/graphql'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { AssetActionEnum } from 'src/modules/assets/models'; +import { UpdatePriceEvent } from 'src/modules/rabbitmq/entities/auction/updatePrice.event'; +import { UpdatePriceDeadrareEvent } from 'src/modules/rabbitmq/entities/auction/updatePriceDeadrare.event'; +import { GenericEvent } from 'src/modules/rabbitmq/entities/generic.event'; +import { DEADRARE_KEY } from 'src/utils/constants'; +import { Marketplace } from '../Marketplace.dto'; +import { MarketplaceTransactionData } from '../marketplaceEventAndTxData.dto'; +import { ReindexGenericSummary } from './ReindexGenericSummary'; + +@ObjectType() +export class AuctionPriceUpdatedSummary extends ReindexGenericSummary { + auctionId: number; + + identifier: string; + collection: string; + nonce: string; + + minBid: string; + maxBid?: string; + itemsCount?: number; + paymentToken?: string; + + constructor(init?: Partial) { + super(init); + Object.assign(this, init); + } + + static fromUpdatePriceEventAndTx( + event: MarketplaceEventsEntity, + tx: MarketplaceTransactionData, + marketplace: Marketplace, + ): AuctionPriceUpdatedSummary { + const address = event.data.eventData?.address ?? tx.receiver; + + const topics = this.getTopics(event, marketplace); + + return new AuctionPriceUpdatedSummary({ + timestamp: event.timestamp, + blockHash: tx?.blockHash, + collection: topics.collection, + nonce: topics.nonce, + identifier: `${topics.collection}-${topics.nonce}`, + auctionId: BinaryUtils.hexToNumber(topics.auctionId), + sender: address, + minBid: topics.newBid ?? topics.minBid, + maxBid: topics.maxBid, + paymentToken: topics.paymentToken, + itemsCount: topics.itemsCount, + action: AssetActionEnum.PriceUpdated, + }); + } + + private static getTopics( + event: MarketplaceEventsEntity, + marketplace: Marketplace, + ): any { + const genericEvent = event.data.eventData + ? GenericEvent.fromEventResponse(event.data.eventData) + : undefined; + + if (marketplace.key === DEADRARE_KEY) { + return new UpdatePriceDeadrareEvent(genericEvent).getTopics(); + } + + return new UpdatePriceEvent(genericEvent).getTopics(); + } +} diff --git a/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionStartedSummary.ts b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionStartedSummary.ts new file mode 100644 index 000000000..f412cd81a --- /dev/null +++ b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionStartedSummary.ts @@ -0,0 +1,115 @@ +import { BinaryUtils } from '@multiversx/sdk-nestjs'; +import { ObjectType } from '@nestjs/graphql'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { + AssetActionEnum, + ElrondNftsSwapAuctionEventEnum, + ExternalAuctionEventEnum, +} from 'src/modules/assets/models'; +import { + AuctionTypeEnum, + ElrondSwapAuctionTypeEnum, +} from 'src/modules/auctions/models'; +import { AuctionTokenEvent } from 'src/modules/rabbitmq/entities/auction'; +import { ElrondSwapAuctionEvent } from 'src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-auction.event'; +import { ListNftEvent } from 'src/modules/rabbitmq/entities/auction/listNft.event'; +import { GenericEvent } from 'src/modules/rabbitmq/entities/generic.event'; +import { MarketplaceTransactionData } from '../marketplaceEventAndTxData.dto'; +import { ReindexGenericSummary } from './ReindexGenericSummary'; + +@ObjectType() +export class AuctionStartedSummary extends ReindexGenericSummary { + auctionId: number; + auctionType: AuctionTypeEnum; + + identifier: string; + collection: string; + nonce: string; + itemsCount: string; + + startTime: number; + endTime: number; + + paymentToken: string; + paymentNonce: number; + + minBid: string; + maxBid: string; + minBidDiff: string; + + constructor(init?: Partial) { + super(init); + Object.assign(this, init); + } + + static fromAuctionTokenEventAndTx( + event: MarketplaceEventsEntity, + tx: MarketplaceTransactionData, + ): AuctionStartedSummary { + const txTopics = tx?.data?.split('@'); + const minBidDiff = txTopics?.[10] + ? BinaryUtils.hexToNumber(txTopics?.[10]).toString() + : '0'; + + const address = event.data.eventData?.address ?? tx.receiver; + const topics = this.getTopics(event); + + if (!topics || (!topics.price && !topics.minBid)) { + return; + } + + return new AuctionStartedSummary({ + timestamp: event.timestamp, + blockHash: tx?.blockHash, + collection: topics.collection, + nonce: topics.nonce, + identifier: `${topics.collection}-${topics.nonce}`, + auctionId: BinaryUtils.hexToNumber(topics.auctionId), + itemsCount: topics.nrAuctionTokens, + address, + sender: topics.originalOwner, + action: AssetActionEnum.StartedAuction, + minBid: topics.minBid ?? topics.price, + maxBid: topics.maxBid ?? '0', + minBidDiff: minBidDiff, + startTime: topics.startTime, + endTime: topics.endTime ?? topics.deadline ?? 0, + paymentToken: topics.paymentToken, + paymentNonce: topics.paymentNonce ?? topics.paymentTokenNonce ?? 0, + auctionType: + Object.values(AuctionTypeEnum)[ + BinaryUtils.hexToNumber(topics.auctionType) + ], + }); + } + + private static getTopics(event: MarketplaceEventsEntity): any { + const genericEvent = event.data + ? GenericEvent.fromEventResponse(event.data.eventData) + : undefined; + + if (event.hasEventIdentifier(ElrondNftsSwapAuctionEventEnum.NftSwap)) { + try { + const topics = new ElrondSwapAuctionEvent(genericEvent).getTopics(); + if (parseInt(topics.auctionType) === ElrondSwapAuctionTypeEnum.Swap) { + return; + } + return topics; + } catch { + return; + } + } + + if ( + event.hasEventIdentifier(ExternalAuctionEventEnum.ListNftOnMarketplace) + ) { + const topics = new ListNftEvent(genericEvent).getTopics(); + return { + ...topics, + maxBid: topics.price, + }; + } + + return new AuctionTokenEvent(genericEvent).getTopics(); + } +} diff --git a/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionUpdatedSummary.ts b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionUpdatedSummary.ts new file mode 100644 index 000000000..59bc98bb2 --- /dev/null +++ b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/AuctionUpdatedSummary.ts @@ -0,0 +1,71 @@ +import { BinaryUtils } from '@multiversx/sdk-nestjs'; +import { ObjectType } from '@nestjs/graphql'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { + AssetActionEnum, + ElrondNftsSwapAuctionEventEnum, +} from 'src/modules/assets/models'; +import { ElrondSwapUpdateEvent } from 'src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-updateAuction.event'; +import { UpdateListingEvent } from 'src/modules/rabbitmq/entities/auction/updateListing.event'; +import { GenericEvent } from 'src/modules/rabbitmq/entities/generic.event'; +import { MarketplaceTransactionData } from '../marketplaceEventAndTxData.dto'; +import { ReindexGenericSummary } from './ReindexGenericSummary'; + +@ObjectType() +export class AuctionUpdatedSummary extends ReindexGenericSummary { + auctionId: number; + + identifier: string; + collection: string; + nonce: string; + + minBid: string; + deadline: number; + paymentToken?: string; + paymentNonce?: string; + + constructor(init?: Partial) { + super(init); + Object.assign(this, init); + } + + static fromUpdateListingEventAndTx( + event: MarketplaceEventsEntity, + tx: MarketplaceTransactionData, + ): AuctionUpdatedSummary { + const address = event.data.eventData?.address ?? tx.receiver; + const topics = this.getTopics(event); + + return new AuctionUpdatedSummary({ + timestamp: event.timestamp, + blockHash: tx?.blockHash, + collection: topics.collection, + nonce: topics.nonce, + identifier: `${topics.collection}-${topics.nonce}`, + auctionId: BinaryUtils.hexToNumber(topics.auctionId), + sender: address, + minBid: topics.newBid ?? topics.price, + paymentToken: topics.paymentToken, + paymentNonce: topics.paymentTokenNonce, + deadline: topics.deadline, + action: AssetActionEnum.Updated, + }); + } + + private static getTopics(event: MarketplaceEventsEntity): any { + const genericEvent = event.data.eventData + ? GenericEvent.fromEventResponse(event.data.eventData) + : undefined; + + if ( + event.hasOneOfEventIdentifiers([ + ElrondNftsSwapAuctionEventEnum.NftSwapUpdate, + ElrondNftsSwapAuctionEventEnum.NftSwapExtend, + ]) + ) { + return new ElrondSwapUpdateEvent(genericEvent).getTopics(); + } + + return new UpdateListingEvent(genericEvent).getTopics(); + } +} diff --git a/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/GloballyOfferAcceptedSummary.ts b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/GloballyOfferAcceptedSummary.ts new file mode 100644 index 000000000..012fc2993 --- /dev/null +++ b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/GloballyOfferAcceptedSummary.ts @@ -0,0 +1,35 @@ +import { ObjectType } from '@nestjs/graphql'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { AssetOfferEnum } from 'src/modules/assets/models/AssetOfferEnum'; +import { AcceptGlobalOfferEvent } from 'src/modules/rabbitmq/entities/auction/acceptGlobalOffer.event'; +import { GenericEvent } from 'src/modules/rabbitmq/entities/generic.event'; +import { MarketplaceTransactionData } from '../marketplaceEventAndTxData.dto'; +import { ReindexGenericSummary } from './ReindexGenericSummary'; + +@ObjectType() +export class GlobalOfferAcceptedSummary extends ReindexGenericSummary { + auctionId: number; + auctionType: AssetOfferEnum; + + constructor(init?: Partial) { + super(init); + Object.assign(this, init); + } + + static fromAcceptGlobalOfferEventAndTx( + event: MarketplaceEventsEntity, + tx: MarketplaceTransactionData, + ): GlobalOfferAcceptedSummary { + const genericEvent = event.data + ? GenericEvent.fromEventResponse(event.data.eventData) + : undefined; + const topics = new AcceptGlobalOfferEvent(genericEvent).getTopics(); + + return new GlobalOfferAcceptedSummary({ + timestamp: event.timestamp, + blockHash: tx?.blockHash, + auctionId: topics.auctionId, + action: AssetOfferEnum.GloballyAccepted, + }); + } +} diff --git a/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/OfferAcceptedSummary.ts b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/OfferAcceptedSummary.ts new file mode 100644 index 000000000..9dc58b757 --- /dev/null +++ b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/OfferAcceptedSummary.ts @@ -0,0 +1,102 @@ +import { ObjectType } from '@nestjs/graphql'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { ElrondNftsSwapAuctionEventEnum } from 'src/modules/assets/models'; +import { AssetOfferEnum } from 'src/modules/assets/models/AssetOfferEnum'; +import { AcceptOfferEvent } from 'src/modules/rabbitmq/entities/auction/acceptOffer.event'; +import { AcceptOfferDeadrareEvent } from 'src/modules/rabbitmq/entities/auction/acceptOfferDeadrare.event'; +import { AcceptOfferFrameitEvent } from 'src/modules/rabbitmq/entities/auction/acceptOfferFrameit.event'; +import { AcceptOfferXoxnoEvent } from 'src/modules/rabbitmq/entities/auction/acceptOfferXoxno.event'; +import { ElrondSwapAcceptOfferEvent } from 'src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-acceptOffer.event'; +import { GenericEvent } from 'src/modules/rabbitmq/entities/generic.event'; +import { + DEADRARE_KEY, + ELRONDNFTSWAP_KEY, + FRAMEIT_KEY, + XOXNO_KEY, +} from 'src/utils/constants'; +import { Marketplace } from '../Marketplace.dto'; +import { MarketplaceTransactionData } from '../marketplaceEventAndTxData.dto'; +import { ReindexGenericSummary } from './ReindexGenericSummary'; + +@ObjectType() +export class OfferAcceptedSummary extends ReindexGenericSummary { + offerId: number; + auctionId?: number; + auctionType: AssetOfferEnum; + + identifier: string; + collection: string; + nonce: string; + itemsCount: string; + + startTime: number; + endTime: number; + + paymentToken: string; + paymentNonce: number; + + price: string; + + constructor(init?: Partial) { + super(init); + Object.assign(this, init); + } + + static fromAcceptOfferEventAndTx( + event: MarketplaceEventsEntity, + tx: MarketplaceTransactionData, + marketplace: Marketplace, + ): OfferAcceptedSummary { + const topics = this.getTopics(event, marketplace); + + if (!topics) { + return; + } + + return new OfferAcceptedSummary({ + timestamp: event.timestamp, + blockHash: tx?.blockHash, + collection: topics.collection, + nonce: topics.nonce, + identifier: `${topics.collection}-${topics.nonce}`, + itemsCount: topics.nrOfferTokens.toString(), + offerId: topics.offerId.toString(), + auctionId: topics.auctionId, + address: topics.offerOwner, + sender: topics.nftOwner, + price: topics.paymentAmount, + paymentToken: topics.paymentTokenIdentifier, + paymentNonce: topics.paymentTokenNonce, + startTime: parseInt(topics.startdate), + endTime: parseInt(topics.enddate), + action: AssetOfferEnum.Accepted, + }); + } + + private static getTopics( + event: MarketplaceEventsEntity, + marketplace: Marketplace, + ): any { + const genericEvent = event.data + ? GenericEvent.fromEventResponse(event.data.eventData) + : undefined; + + if (marketplace.key === XOXNO_KEY) { + return new AcceptOfferXoxnoEvent(genericEvent).getTopics(); + } + + if (marketplace.key === DEADRARE_KEY) { + return new AcceptOfferDeadrareEvent(genericEvent).getTopics(); + } + + if (marketplace.key === FRAMEIT_KEY) { + return new AcceptOfferFrameitEvent(genericEvent).getTopics(); + } + + if (marketplace.key === ELRONDNFTSWAP_KEY) { + return new ElrondSwapAcceptOfferEvent(genericEvent).getTopics(); + } + + return new AcceptOfferEvent(genericEvent).getTopics(); + } +} diff --git a/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/OfferClosedSummary.ts b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/OfferClosedSummary.ts new file mode 100644 index 000000000..858c7131a --- /dev/null +++ b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/OfferClosedSummary.ts @@ -0,0 +1,42 @@ +import { ObjectType } from '@nestjs/graphql'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { AssetOfferEnum } from 'src/modules/assets/models/AssetOfferEnum'; +import { WithdrawOfferEvent } from 'src/modules/rabbitmq/entities/auction/withdrawOffer.event'; +import { GenericEvent } from 'src/modules/rabbitmq/entities/generic.event'; +import { MarketplaceTransactionData } from '../marketplaceEventAndTxData.dto'; +import { ReindexGenericSummary } from './ReindexGenericSummary'; + +@ObjectType() +export class OfferClosedSummary extends ReindexGenericSummary { + offerId: number; + auctionType: AssetOfferEnum; + + identifier: string; + collection: string; + nonce: string; + + constructor(init?: Partial) { + super(init); + Object.assign(this, init); + } + + static fromWithdrawOfferEventAndTx( + event: MarketplaceEventsEntity, + tx: MarketplaceTransactionData, + ): OfferClosedSummary { + const genericEvent = event.data + ? GenericEvent.fromEventResponse(event.data.eventData) + : undefined; + const topics = new WithdrawOfferEvent(genericEvent).getTopics(); + + return new OfferClosedSummary({ + timestamp: event.timestamp, + blockHash: tx?.blockHash, + collection: topics.collection, + nonce: topics.nonce, + identifier: `${topics.collection}-${topics.nonce}`, + offerId: topics.offerId, + action: AssetOfferEnum.Closed, + }); + } +} diff --git a/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/OfferCreatedSummary.ts b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/OfferCreatedSummary.ts new file mode 100644 index 000000000..f06294098 --- /dev/null +++ b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/OfferCreatedSummary.ts @@ -0,0 +1,65 @@ +import { ObjectType } from '@nestjs/graphql'; +import { MarketplaceEventsEntity } from 'src/db/marketplaces/marketplace-events.entity'; +import { AssetOfferEnum } from 'src/modules/assets/models/AssetOfferEnum'; +import { SendOfferEvent } from 'src/modules/rabbitmq/entities/auction/sendOffer.event'; +import { GenericEvent } from 'src/modules/rabbitmq/entities/generic.event'; +import { Marketplace } from '../Marketplace.dto'; +import { MarketplaceTransactionData } from '../marketplaceEventAndTxData.dto'; +import { MarketplaceTypeEnum } from '../MarketplaceType.enum'; +import { ReindexGenericSummary } from './ReindexGenericSummary'; + +@ObjectType() +export class OfferCreatedSummary extends ReindexGenericSummary { + offerId: number; + auctionType: AssetOfferEnum; + + identifier: string; + collection: string; + nonce: string; + itemsCount: string; + + startTime: number; + endTime: number; + + paymentToken: string; + paymentNonce: number; + price: string; + + constructor(init?: Partial) { + super(init); + Object.assign(this, init); + } + + static fromSendOfferEventAndTx( + event: MarketplaceEventsEntity, + tx: MarketplaceTransactionData, + marketplace: Marketplace, + ): OfferCreatedSummary { + if (marketplace.type === MarketplaceTypeEnum.External) { + return; + } + const address = event.data.eventData?.address ?? tx.receiver; + const genericEvent = event.data + ? GenericEvent.fromEventResponse(event.data.eventData) + : undefined; + const topics = new SendOfferEvent(genericEvent).getTopics(); + + return new OfferCreatedSummary({ + timestamp: event.timestamp, + blockHash: tx?.blockHash, + collection: topics.collection, + nonce: topics.nonce, + identifier: `${topics.collection}-${topics.nonce}`, + offerId: topics.offerId, + itemsCount: topics.nrOfferTokens.toString(), + address: topics.offerOwner, + sender: address, + price: topics.paymentAmount, + paymentToken: topics.paymentTokenIdentifier, + paymentNonce: topics.paymentTokenNonce, + startTime: parseInt(topics.startdate), + endTime: parseInt(topics.enddate), + action: AssetOfferEnum.Created, + }); + } +} diff --git a/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/ReindexGenericSummary.ts b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/ReindexGenericSummary.ts new file mode 100644 index 000000000..63728de77 --- /dev/null +++ b/src/modules/marketplaces/models/marketplaces-reindex-events-summaries/ReindexGenericSummary.ts @@ -0,0 +1,16 @@ +import { ObjectType } from '@nestjs/graphql'; +import { AssetActionEnum } from 'src/modules/assets/models'; +import { AssetOfferEnum } from 'src/modules/assets/models/AssetOfferEnum'; + +@ObjectType() +export class ReindexGenericSummary { + action: AssetActionEnum | AssetOfferEnum; + blockHash?: string; + timestamp: number; + address: string; + sender: string; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} diff --git a/src/modules/nftCollections/base-collection-assets.redis-handler.ts b/src/modules/nftCollections/base-collection-assets.redis-handler.ts index 3289de0c2..39c444ac8 100644 --- a/src/modules/nftCollections/base-collection-assets.redis-handler.ts +++ b/src/modules/nftCollections/base-collection-assets.redis-handler.ts @@ -68,10 +68,6 @@ export abstract class BaseCollectionsAssetsRedisHandler { await this.redisCacheService.delete(this.getCacheKey(key)); } - async clearKeyByPattern(key: string): Promise { - await this.redisCacheService.deleteByPattern(`${this.getCacheKey(key)}*`); - } - private getCacheKeys(key: string[]) { return key.map((id) => this.getCacheKey(id)); } diff --git a/src/modules/nftCollections/collection-assets-model.resolver.ts b/src/modules/nftCollections/collection-assets-model.resolver.ts index ebc5c5737..f59f3828c 100644 --- a/src/modules/nftCollections/collection-assets-model.resolver.ts +++ b/src/modules/nftCollections/collection-assets-model.resolver.ts @@ -20,7 +20,10 @@ export class CollectionAssetsModelResolver extends BaseResolver( const { identifier } = collectionAssetModel; const scamInfo = await this.assetScamProvider.load(identifier); const scamInfoValue = scamInfo.value; - return scamInfoValue?.type && Object.keys(scamInfoValue).length > 1 + + return scamInfoValue && + Object.keys(scamInfoValue).length > 1 && + ScamInfo.isScam(scamInfoValue) ? scamInfoValue : null; } diff --git a/src/modules/nftCollections/collections-getter.service.ts b/src/modules/nftCollections/collections-getter.service.ts index 785bec0d5..c3c0f89ca 100644 --- a/src/modules/nftCollections/collections-getter.service.ts +++ b/src/modules/nftCollections/collections-getter.service.ts @@ -82,27 +82,48 @@ export class CollectionsGetterService { filters?: CollectionsFilter, ): Promise<[Collection[], number]> { let trendingCollections = []; + const [collections] = await this.getOrSetFullCollections(); if (process.env.ENABLE_TRENDING_BY_VOLUME === 'true') { - const collections = await this.analyticsService.getTrendingByVolume(); - if (collections) { - [trendingCollections] = await this.addCollectionsDetails(collections); + const trendingByVolume = + await this.analyticsService.getTrendingByVolume(); + if (trendingByVolume) { + [trendingCollections] = await this.addCollectionsDetails( + trendingByVolume, + collections, + ); } } else { [trendingCollections] = await this.getOrSetTrendingByAuctionsCollections(); } - trendingCollections = this.applyFilters(filters, trendingCollections); const blacklistedCollections = await this.blacklistedCollectionsService.getBlacklistedCollectionIds(); + const collectionIdentifiers = new Set( + trendingCollections.map((x: { collection: any }) => x.collection), + ); + let activeWithoutTrending = collections.filter( + (x) => + !collectionIdentifiers.has(x.collection) && + !blacklistedCollections.includes(x.collection), + ); + trendingCollections = trendingCollections?.filter( (x) => !blacklistedCollections.includes(x.collection), ); - const count = trendingCollections.length; + activeWithoutTrending = orderBy( + activeWithoutTrending, + ['verified'], + ['desc'], + ); trendingCollections = orderBy( trendingCollections, ['verified', 'last24Volume'], ['desc', 'desc'], ); + + trendingCollections = [...trendingCollections, ...activeWithoutTrending]; + trendingCollections = this.applyFilters(filters, trendingCollections); + const count = trendingCollections.length; trendingCollections = trendingCollections?.slice(offset, offset + limit); return [trendingCollections, count]; } @@ -127,9 +148,12 @@ export class CollectionsGetterService { private async addCollectionsDetails( trendingCollections: any[], + collections?: Collection[], ): Promise<[Collection[], number]> { const mappedCollections: Collection[] = []; - const [collections] = await this.getOrSetFullCollections(); + if (!collections) { + [collections] = await this.getOrSetFullCollections(); + } for (const trendingCollection of trendingCollections) { const mappedCollection = collections.find( (c) => c.collection === trendingCollection.collection, diff --git a/src/modules/nftCollections/models/Collection.dto.ts b/src/modules/nftCollections/models/Collection.dto.ts index b81e7290c..65b24846e 100644 --- a/src/modules/nftCollections/models/Collection.dto.ts +++ b/src/modules/nftCollections/models/Collection.dto.ts @@ -131,7 +131,7 @@ export class Collection { @ObjectType() export class CollectionRole { - @Field() + @Field({ nullable: true }) address?: string; @Field({ nullable: true }) canCreate: boolean; diff --git a/src/modules/rabbitmq/blockchain-events/feed-events.service.ts b/src/modules/rabbitmq/blockchain-events/feed-events.service.ts index 668e3a621..e5185de85 100644 --- a/src/modules/rabbitmq/blockchain-events/feed-events.service.ts +++ b/src/modules/rabbitmq/blockchain-events/feed-events.service.ts @@ -12,8 +12,8 @@ import { Marketplace } from 'src/modules/marketplaces/models'; import { Order } from 'src/modules/orders/models'; import { UsdPriceService } from 'src/modules/usdPrice/usd-price.service'; import { MintEvent } from '../entities/auction/mint.event'; -import { Token } from 'src/common/services/mx-communication/models/Token.model'; import { OfferEntity } from 'src/db/offers'; +import { Token } from 'src/modules/usdPrice/Token.model'; @Injectable() export class FeedEventsSenderService { @@ -81,8 +81,8 @@ export class FeedEventsSenderService { tokenData: tokenData ?? undefined, }, marketplaceKey: auctionTokenMarketplace.key, - isNsfw: nftData.isNsfw, - scamInfo: nftData.scamInfo, + isNsfw: nftData?.isNsfw, + scamInfo: nftData?.scamInfo, }, }), ); @@ -129,8 +129,8 @@ export class FeedEventsSenderService { usdAmount: usdAmount ?? undefined, tokenData: tokenData ?? undefined, marketplaceKey: endMarketplace.key, - isNsfw: endAuctionNftData.isNsfw, - scamInfo: endAuctionNftData.scamInfo, + isNsfw: endAuctionNftData?.isNsfw, + scamInfo: endAuctionNftData?.scamInfo, }, }), ); @@ -172,8 +172,8 @@ export class FeedEventsSenderService { auctionId: buyAuction.id, boughtTokens: boughtTokens, marketplaceKey: buyMarketplace.key, - isNsfw: buySftNftData.isNsfw, - scamInfo: buySftNftData.scamInfo, + isNsfw: buySftNftData?.isNsfw, + scamInfo: buySftNftData?.scamInfo, }, }), ); @@ -196,8 +196,8 @@ export class FeedEventsSenderService { nftName: nftData?.name, verified: nftData?.verified ? true : false, collectionName: collection?.name, - isNsfw: nftData.isNsfw, - scamInfo: nftData.scamInfo, + isNsfw: nftData?.isNsfw, + scamInfo: nftData?.scamInfo, }, }), ); @@ -245,8 +245,8 @@ export class FeedEventsSenderService { tokenData: tokenData ?? undefined, auctionId: auction.id, marketplaceKey: auction.marketplaceKey, - isNsfw: bidNftData.isNsfw, - scamInfo: bidNftData.scamInfo, + isNsfw: bidNftData?.isNsfw, + scamInfo: bidNftData?.scamInfo, }, }), ); @@ -275,8 +275,8 @@ export class FeedEventsSenderService { usdAmount: usdAmount ?? undefined, tokenData: tokenData ?? undefined, marketplaceKey: offer.marketplaceKey, - isNsfw: nft.isNsfw, - scamInfo: nft.scamInfo, + isNsfw: nft?.isNsfw, + scamInfo: nft?.scamInfo, }, }), ); @@ -306,8 +306,8 @@ export class FeedEventsSenderService { tokenData: tokenData ?? undefined, marketplaceKey: offer.marketplaceKey, offerOwner: offer.ownerAddress, - isNsfw: nft.isNsfw, - scamInfo: nft.scamInfo, + isNsfw: nft?.isNsfw, + scamInfo: nft?.scamInfo, }, }), ); diff --git a/src/modules/rabbitmq/blockchain-events/handlers/updateListing-event.handler.ts b/src/modules/rabbitmq/blockchain-events/handlers/updateListing-event.handler.ts index 462c0c9b9..108ca255e 100644 --- a/src/modules/rabbitmq/blockchain-events/handlers/updateListing-event.handler.ts +++ b/src/modules/rabbitmq/blockchain-events/handlers/updateListing-event.handler.ts @@ -1,6 +1,5 @@ import { BinaryUtils } from '@multiversx/sdk-nestjs'; import { Injectable, Logger } from '@nestjs/common'; -import { Token } from 'src/common/services/mx-communication/models/Token.model'; import { AuctionEntity } from 'src/db/auctions'; import { ExternalAuctionEventEnum } from 'src/modules/assets/models'; import { @@ -9,6 +8,7 @@ import { } from 'src/modules/auctions'; import { MarketplacesService } from 'src/modules/marketplaces/marketplaces.service'; import { MarketplaceTypeEnum } from 'src/modules/marketplaces/models/MarketplaceType.enum'; +import { Token } from 'src/modules/usdPrice/Token.model'; import { UsdPriceService } from 'src/modules/usdPrice/usd-price.service'; import { BigNumberUtils } from 'src/utils/bigNumber-utils'; import { UpdateListingEvent } from '../../entities/auction/updateListing.event'; diff --git a/src/modules/rabbitmq/blockchain-events/marketplace-events.service.ts b/src/modules/rabbitmq/blockchain-events/marketplace-events.service.ts index e91fd7d17..8276d1f9e 100644 --- a/src/modules/rabbitmq/blockchain-events/marketplace-events.service.ts +++ b/src/modules/rabbitmq/blockchain-events/marketplace-events.service.ts @@ -38,7 +38,7 @@ export class MarketplaceEventsService { private readonly slackReportService: SlackReportService, private sendOfferEventHandler: SendOfferEventHandler, private withdrawOfferEventHandler: WithdrawOfferEventHandler, - ) {} + ) { } public async handleNftAuctionEvents( auctionEvents: any[], @@ -66,7 +66,7 @@ export class MarketplaceEventsService { this.logger.log( `${eventName} event detected for hash '${hash}' for marketplace ${event.address}, ignore it for the moment`, ); - return; + continue; } await this.buyEventHandler.handle(event, hash, marketplaceType); break; @@ -81,7 +81,7 @@ export class MarketplaceEventsService { this.logger.log( `${event.topics[0]} event detected for hash '${hash}' for marketplace ${event.addreses}, ignore it for the moment`, ); - return; + continue; } await this.withdrawAuctionEventHandler.handle( event, @@ -128,7 +128,7 @@ export class MarketplaceEventsService { 'base64', ).toString(); if (acceptOfferEventName === ExternalAuctionEventEnum.UserDeposit) { - return; + continue; } if (acceptOfferEventName === ExternalAuctionEventEnum.EndTokenEvent) { await this.withdrawAuctionEventHandler.handle( diff --git a/src/modules/rabbitmq/blockchain-events/nft-events.module.ts b/src/modules/rabbitmq/blockchain-events/nft-events.module.ts index 654d0591a..6e25c9d57 100644 --- a/src/modules/rabbitmq/blockchain-events/nft-events.module.ts +++ b/src/modules/rabbitmq/blockchain-events/nft-events.module.ts @@ -6,7 +6,6 @@ import { RevertEventsService } from './revert.events.service'; import { MxCommunicationModule } from 'src/common'; import { MinterEventsService } from './minter-events.service'; import { CommonModule } from 'src/common.module'; -import { CacheModule } from 'src/common/services/caching/caching.module'; import { RarityUpdaterService } from 'src/crons/elastic.updater/rarity.updater.service'; import { NsfwUpdaterService } from 'src/crons/elastic.updater/nsfw.updater.service'; import { FlagNftService } from 'src/modules/admins/flag-nft.service'; @@ -42,6 +41,7 @@ import { OffersModuleGraph } from 'src/modules/offers/offers.module'; import { AcceptOfferEventHandler } from './handlers/acceptOffer-event.handler'; import { WithdrawOfferEventHandler } from './handlers/withdrawOffer-event.handler'; import { UpdateListingEventHandler } from './handlers/updateListing-event.handler'; +import { PluginModule } from 'src/plugins/plugin.module'; @Module({ imports: [ @@ -54,6 +54,7 @@ import { UpdateListingEventHandler } from './handlers/updateListing-event.handle forwardRef(() => MarketplacesModuleGraph), forwardRef(() => MxCommunicationModule), forwardRef(() => OffersModuleGraph), + forwardRef(() => PluginModule), UsdPriceModuleGraph, NftRarityModuleGraph, ScamModule, diff --git a/src/modules/rabbitmq/cache-invalidation/cache-events.consumer.ts b/src/modules/rabbitmq/cache-invalidation/cache-events.consumer.ts index 25d69c243..4cebcfe82 100644 --- a/src/modules/rabbitmq/cache-invalidation/cache-events.consumer.ts +++ b/src/modules/rabbitmq/cache-invalidation/cache-events.consumer.ts @@ -45,7 +45,6 @@ export class CacheEventsConsumer { const collectionIdentifier = event.id.split('-').slice(0, 2).join('-'); await Promise.all([ this.assetsRedisHandler.clearKey(event.id), - this.cacheInvalidationEventsService.invalidateAssetHistory(event.id), this.collectionAssetsRedisHandler.clearKey(collectionIdentifier), this.collectionAssetsForOwnerRedisHandler.clearKey( `${collectionIdentifier}_${event.address}`, @@ -85,7 +84,6 @@ export class CacheEventsConsumer { ); await Promise.all([ this.assetsRedisHandler.clearKey(event.id), - this.assetScamInfoRedisHandler.clearKey(event.id), this.collectionAssets.clearKey(collection), this.collectionAssetsRedisHandler.clearKey(collection), collectionsAssetForOnwerPromise, @@ -114,7 +112,6 @@ export class CacheEventsConsumer { const profilerUpdateAuction = new CpuProfiler(); await Promise.all([ this.cacheInvalidationEventsService.invalidateAuction(event), - this.cacheInvalidationEventsService.invalidateAssetHistory(event.id), ]); profilerUpdateAuction.stop('UpdateAuction'); break; @@ -180,6 +177,12 @@ export class CacheEventsConsumer { profilerUpdateOffer.stop('UpdateOffer'); break; + case CacheEventTypeEnum.ScamUpdate: + const profileScamUpdate = new CpuProfiler(); + this.assetScamInfoRedisHandler.clearKey(event.id), + profileScamUpdate.stop('ScamUpdate'); + break; + // case CacheEventTypeEnum.RefreshTrending: // await this.cacheInvalidationEventsService.invalidateTrendingAuctions( // event, diff --git a/src/modules/rabbitmq/cache-invalidation/cache-invalidation-module/cache-invalidation-events.service.ts b/src/modules/rabbitmq/cache-invalidation/cache-invalidation-module/cache-invalidation-events.service.ts index bfc9230c1..86105431c 100644 --- a/src/modules/rabbitmq/cache-invalidation/cache-invalidation-module/cache-invalidation-events.service.ts +++ b/src/modules/rabbitmq/cache-invalidation/cache-invalidation-module/cache-invalidation-events.service.ts @@ -1,6 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { TrendingCollectionsWarmerService } from 'src/crons/cache.warmer/trendingCollections.warmer.service'; -import { AssetsHistoryCachingService } from 'src/modules/asset-history/assets-history-caching.service'; import { AssetsLikesCachingService } from 'src/modules/assets/assets-likes.caching.service'; import { AssetAvailableTokensCountRedisHandler } from 'src/modules/assets/loaders/asset-available-tokens-count.redis-handler'; import { AuctionsCachingService } from 'src/modules/auctions/caching/auctions-caching.service'; @@ -19,7 +17,6 @@ export class CacheInvalidationEventsService { private notificationsCachingService: NotificationsCachingService, private availableTokensCount: AssetAvailableTokensCountRedisHandler, private assetsLikesCachingService: AssetsLikesCachingService, - private assetsHistoryCachingService: AssetsHistoryCachingService, private featuredCollectionsCachingService: FeaturedCollectionsCachingService, private blacklistedCollectionsCachingService: BlacklistedCollectionsCachingService, // private analyticsService: TrendingCollectionsWarmerService, @@ -33,9 +30,6 @@ export class CacheInvalidationEventsService { payload.address, payload.extraInfo?.marketplaceKey, ), - await this.auctionsCachingService.invalidateCacheByPattern( - payload.address, - ), await this.availableTokensCount.clearKey(payload.id), ]); } @@ -66,17 +60,12 @@ export class CacheInvalidationEventsService { this.assetsLikesCachingService.invalidateCache(payload.id, payload.address); } - async invalidateAssetHistory(identifier: string) { - await this.assetsHistoryCachingService.invalidateCache(identifier); - } - async invalidateFeaturedCollectionsCache(): Promise { await this.featuredCollectionsCachingService.invalidateFeaturedCollectionsCache(); } async invalidateBlacklistedCollectionsCache(): Promise { await this.blacklistedCollectionsCachingService.invalidateBlacklistedCollectionsCache(); - await this.featuredCollectionsCachingService.invalidateFeaturedCollectionsCache(); } async invalidateOffers(payload: ChangedEvent) { diff --git a/src/modules/rabbitmq/cache-invalidation/events/changed.event.ts b/src/modules/rabbitmq/cache-invalidation/events/changed.event.ts index d7132f66b..2b5bce39c 100644 --- a/src/modules/rabbitmq/cache-invalidation/events/changed.event.ts +++ b/src/modules/rabbitmq/cache-invalidation/events/changed.event.ts @@ -22,6 +22,7 @@ export enum CacheEventTypeEnum { SetCacheKey = 'SetCacheKey', UpdateOffer = 'UpdateOffer', AssetRefresh = 'AssetRefresh', + ScamUpdate = 'ScamUpdate', BlacklistedCollections = 'BlacklistedCollections', RefreshTrending = 'RefreshTrending', MarkCollection = 'MarkCollection', diff --git a/src/modules/rabbitmq/elastic-updates/elastic-updates-events.service.ts b/src/modules/rabbitmq/elastic-updates/elastic-updates-events.service.ts index 679ec06f0..c45e52957 100644 --- a/src/modules/rabbitmq/elastic-updates/elastic-updates-events.service.ts +++ b/src/modules/rabbitmq/elastic-updates/elastic-updates-events.service.ts @@ -138,7 +138,7 @@ export class ElasticUpdatesEventsService { ): Promise { await new Promise((resolve) => setTimeout(resolve, 5000)); - let nftsToUpdate: string[] = []; + let nftsToUpdate: Asset[] = []; let nftsToDelete: string[] = []; let collectionTypes: { [key: string]: string } = {}; @@ -174,7 +174,7 @@ export class ElasticUpdatesEventsService { } collectionTypes[collection] = nft.type; - nftsToUpdate.push(identifier); + nftsToUpdate.push(nft); } nftsToUpdate = [...new Set(nftsToUpdate)]; @@ -183,10 +183,7 @@ export class ElasticUpdatesEventsService { return this.documentDbService.deleteNftScamInfo(n); }); - nftsToUpdate.map( - async (collection) => - await this.nftScamInfoService.validateOrUpdateNftScamInfo(collection), - ); + await this.nftScamInfoService.validateNftsScamInfoArray(nftsToUpdate); await Promise.all(deletes); } diff --git a/src/modules/rabbitmq/entities/auction/acceptOfferFrameit.event.topics.ts b/src/modules/rabbitmq/entities/auction/acceptOfferFrameit.event.topics.ts index aa03307f8..fc5e1f089 100644 --- a/src/modules/rabbitmq/entities/auction/acceptOfferFrameit.event.topics.ts +++ b/src/modules/rabbitmq/entities/auction/acceptOfferFrameit.event.topics.ts @@ -32,13 +32,15 @@ export class AcceptOfferFrameitEventsTopics { this.paymentAmount = Buffer.from(rawTopics[8], 'base64') .toString('hex') .hexBigNumberToString(); - this.nftOwner = new Address(Buffer.from(rawTopics[10], 'base64')); + if (rawTopics.length > 10) { + this.nftOwner = new Address(Buffer.from(rawTopics[10], 'base64')); + } } toPlainObject() { return { offerOwner: this.offerOwner.bech32(), - nftOwner: this.nftOwner.bech32(), + nftOwner: this.nftOwner?.bech32(), collection: this.collection, nonce: this.nonce, offerId: this.offerId, diff --git a/src/modules/rabbitmq/entities/auction/auctionToken.event.topics.ts b/src/modules/rabbitmq/entities/auction/auctionToken.event.topics.ts index 5081dde24..3fd618e61 100644 --- a/src/modules/rabbitmq/entities/auction/auctionToken.event.topics.ts +++ b/src/modules/rabbitmq/entities/auction/auctionToken.event.topics.ts @@ -1,4 +1,5 @@ -import { Address } from '@multiversx/sdk-core'; +import { Address } from '@multiversx/sdk-core/out'; +import { BinaryUtils } from '@multiversx/sdk-nestjs'; export class AuctionTokenEventsTopics { private collection: string; @@ -6,6 +7,13 @@ export class AuctionTokenEventsTopics { private auctionId: string; private nrAuctionTokens: string; private originalOwner: Address; + private minBid: string; + private maxBid: string; + private startTime: number; + private endTime: number; + private paymentToken: string; + private paymentNonce: number; + private auctionType: string; constructor(rawTopics: string[]) { this.collection = Buffer.from(rawTopics[1], 'base64').toString(); @@ -16,6 +24,33 @@ export class AuctionTokenEventsTopics { 16, ).toString(); this.originalOwner = new Address(Buffer.from(rawTopics[5], 'base64')); + + this.minBid = BinaryUtils.hexToNumber( + BinaryUtils.base64ToHex(rawTopics[6]), + ).toString(); + this.maxBid = BinaryUtils.hexToNumber( + BinaryUtils.base64ToHex(rawTopics[7]), + ).toString(); + this.startTime = BinaryUtils.hexToNumber( + BinaryUtils.base64ToHex(rawTopics[8]), + ); + this.endTime = BinaryUtils.hexToNumber( + BinaryUtils.base64ToHex(rawTopics[9]), + ); + this.paymentToken = BinaryUtils.base64Decode(rawTopics[10]); + this.paymentNonce = BinaryUtils.hexToNumber( + BinaryUtils.base64ToHex(rawTopics[11]), + ); + this.auctionType = BinaryUtils.hexToNumber( + BinaryUtils.base64ToHex(rawTopics[12]), + ).toString(); + + if (this.startTime.toString().length > 10) { + this.startTime = parseInt(this.startTime.toString().substring(0, 10)); + } + if (this.endTime.toString().length > 10) { + this.endTime = parseInt(this.endTime.toString().substring(0, 10)); + } } toPlainObject() { @@ -25,6 +60,13 @@ export class AuctionTokenEventsTopics { nonce: this.nonce, auctionId: this.auctionId, nrAuctionTokens: this.nrAuctionTokens, + minBid: this.minBid, + maxBid: this.maxBid, + startTime: this.startTime, + endTime: this.endTime, + paymentToken: this.paymentToken, + paymentNonce: this.paymentNonce, + auctionType: this.auctionType, }; } } diff --git a/src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-acceptOffer.event.topics.ts b/src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-acceptOffer.event.topics.ts new file mode 100644 index 000000000..fc6bf6260 --- /dev/null +++ b/src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-acceptOffer.event.topics.ts @@ -0,0 +1,63 @@ +import { + Address, + BinaryCodec, + FieldDefinition, + StructType, + TokenIdentifierType, + U64Type, +} from '@multiversx/sdk-core/out'; + +export class ElrondSwapAcceptOfferTopics { + private offerId: number; + private collection: string; + private nonce: string; + private nrOfferTokens: string; + private originalOwner: Address; + private paymentAmount: string; + private paymentToken: string; + private paymentTokenNonce: string; + + constructor(rawTopics: string[]) { + this.offerId = parseInt( + Buffer.from(rawTopics[1], 'base64').toString('hex'), + 16, + ); + this.collection = Buffer.from(rawTopics[2], 'base64').toString(); + this.nonce = Buffer.from(rawTopics[3], 'base64').toString('hex'); + this.nrOfferTokens = parseInt( + Buffer.from(rawTopics[4], 'base64').toString('hex'), + 16, + ).toString(); + this.originalOwner = new Address(Buffer.from(rawTopics[5], 'base64')); + this.paymentAmount = Buffer.from(rawTopics[6], 'base64') + .toString('hex') + .hexBigNumberToString(); + let token = decodeToken(Buffer.from(rawTopics[7], 'base64')); + this.paymentToken = token.token_type; + this.paymentTokenNonce = token.nonce; + } + + toPlainObject() { + return { + originalOwner: this.originalOwner.bech32(), + collection: this.collection, + nonce: this.nonce, + offerId: this.offerId, + nrOfferTokens: this.nrOfferTokens, + paymentAmount: this.paymentAmount, + paymentToken: this.paymentToken, + paymentTokenNonce: this.paymentTokenNonce, + }; + } +} + +function decodeToken(bufer: Buffer): any { + const codec = new BinaryCodec(); + const type = new StructType('EsdtToken', [ + new FieldDefinition('token_type', '', new TokenIdentifierType()), + new FieldDefinition('nonce', '', new U64Type()), + ]); + + const [decoded] = codec.decodeNested(bufer, type); + return decoded.valueOf(); +} diff --git a/src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-acceptOffer.event.ts b/src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-acceptOffer.event.ts new file mode 100644 index 000000000..072749119 --- /dev/null +++ b/src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-acceptOffer.event.ts @@ -0,0 +1,15 @@ +import { GenericEvent } from '../../generic.event'; +import { ElrondSwapAcceptOfferTopics } from './elrondswap-acceptOffer.event.topics'; + +export class ElrondSwapAcceptOfferEvent extends GenericEvent { + private decodedTopics: ElrondSwapAcceptOfferTopics; + + constructor(init?: Partial) { + super(init); + this.decodedTopics = new ElrondSwapAcceptOfferTopics(this.topics); + } + + getTopics() { + return this.decodedTopics.toPlainObject(); + } +} diff --git a/src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-updateAuction.event.topics.ts b/src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-updateAuction.event.topics.ts index 0b47365ce..195afea37 100644 --- a/src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-updateAuction.event.topics.ts +++ b/src/modules/rabbitmq/entities/auction/elrondnftswap/elrondswap-updateAuction.event.topics.ts @@ -1,6 +1,11 @@ -import { Address } from '@multiversx/sdk-core'; -import { NumberUtils } from '@multiversx/sdk-nestjs'; - +import { + Address, + BinaryCodec, + FieldDefinition, + StructType, + TokenIdentifierType, + U64Type, +} from '@multiversx/sdk-core/out'; export class ElrondSwapUpdateTopics { private auctionId: string; private collection: string; @@ -9,6 +14,8 @@ export class ElrondSwapUpdateTopics { private seller: Address; private price: string; private deadline: number; + private paymentToken: string; + private paymentTokenNonce: string; constructor(rawTopics: string[]) { this.auctionId = Buffer.from(rawTopics[1], 'base64').toString('hex'); @@ -22,7 +29,13 @@ export class ElrondSwapUpdateTopics { this.price = Buffer.from(rawTopics[6], 'base64') .toString('hex') .hexBigNumberToString(); - this.deadline = parseInt(NumberUtils.numberDecode(rawTopics[9] ?? '00')); + this.deadline = parseInt( + Buffer.from(rawTopics[9], 'base64').toString('hex'), + 16, + ); + let token = decodeToken(Buffer.from(rawTopics[7], 'base64')); + this.paymentToken = token.token_type; + this.paymentTokenNonce = token.nonce; } toPlainObject() { @@ -34,6 +47,19 @@ export class ElrondSwapUpdateTopics { nrAuctionTokens: this.nrAuctionTokens, price: this.price, deadline: this.deadline, + paymentToken: this.paymentToken, + paymentTokenNonce: this.paymentTokenNonce, }; } } + +function decodeToken(bufer: Buffer): any { + const codec = new BinaryCodec(); + const type = new StructType('EsdtToken', [ + new FieldDefinition('token_type', '', new TokenIdentifierType()), + new FieldDefinition('nonce', '', new U64Type()), + ]); + + const [decoded] = codec.decodeNested(bufer, type); + return decoded.valueOf(); +} diff --git a/src/modules/rabbitmq/entities/auction/updatePriceDeadrare.event.topics.ts b/src/modules/rabbitmq/entities/auction/updatePriceDeadrare.event.topics.ts new file mode 100644 index 000000000..9efd81fa0 --- /dev/null +++ b/src/modules/rabbitmq/entities/auction/updatePriceDeadrare.event.topics.ts @@ -0,0 +1,38 @@ +export class UpdatePriceDeadrareEventsTopics { + private collection: string; + private nonce: string; + private auctionId: string; + private minBid: string; + private maxBid: string; + private paymentToken: string; + private itemsCount: number; + + constructor(rawTopics: string[]) { + this.collection = Buffer.from(rawTopics[1], 'base64').toString(); + this.nonce = Buffer.from(rawTopics[2], 'base64').toString('hex'); + this.auctionId = Buffer.from(rawTopics[3], 'base64').toString('hex'); + this.itemsCount = parseInt( + Buffer.from(rawTopics[4], 'base64').toString('hex'), + 16, + ); + this.minBid = Buffer.from(rawTopics[6], 'base64') + .toString('hex') + .hexBigNumberToString(); + this.maxBid = Buffer.from(rawTopics[7], 'base64') + .toString('hex') + .hexBigNumberToString(); + this.paymentToken = Buffer.from(rawTopics[8], 'base64').toString(); + } + + toPlainObject() { + return { + collection: this.collection, + nonce: this.nonce, + auctionId: this.auctionId, + itemsCount: this.itemsCount, + minBid: this.minBid, + maxBid: this.maxBid, + paymentToken: this.paymentToken, + }; + } +} diff --git a/src/modules/rabbitmq/entities/auction/updatePriceDeadrare.event.ts b/src/modules/rabbitmq/entities/auction/updatePriceDeadrare.event.ts new file mode 100644 index 000000000..39a791d48 --- /dev/null +++ b/src/modules/rabbitmq/entities/auction/updatePriceDeadrare.event.ts @@ -0,0 +1,15 @@ +import { GenericEvent } from '../generic.event'; +import { UpdatePriceDeadrareEventsTopics } from './updatePriceDeadrare.event.topics'; + +export class UpdatePriceDeadrareEvent extends GenericEvent { + private decodedTopics: UpdatePriceDeadrareEventsTopics; + + constructor(init?: Partial) { + super(init); + this.decodedTopics = new UpdatePriceDeadrareEventsTopics(this.topics); + } + + getTopics() { + return this.decodedTopics.toPlainObject(); + } +} diff --git a/src/modules/rabbitmq/entities/generic.event.ts b/src/modules/rabbitmq/entities/generic.event.ts index 10adf05ff..99a0d7b13 100644 --- a/src/modules/rabbitmq/entities/generic.event.ts +++ b/src/modules/rabbitmq/entities/generic.event.ts @@ -1,3 +1,5 @@ +import { EventResponse } from 'src/common/services/mx-communication/models/elastic-search/event.response'; + export class GenericEvent { private address = ''; private identifier = ''; @@ -24,4 +26,13 @@ export class GenericEvent { topics: this.topics, }; } + + static fromEventResponse(eventResponse: EventResponse): GenericEvent { + let event = new GenericEvent(); + event.address = eventResponse.address; + event.identifier = eventResponse.identifier; + event.topics = eventResponse.topics; + event.data = eventResponse.data; + return event; + } } diff --git a/src/modules/scam/collection-scam.service.ts b/src/modules/scam/collection-scam.service.ts index fd0053052..d4aa2cf7d 100644 --- a/src/modules/scam/collection-scam.service.ts +++ b/src/modules/scam/collection-scam.service.ts @@ -1,27 +1,63 @@ import { Injectable } from '@nestjs/common'; -import { MxApiService } from 'src/common'; -import { MxExtrasApiService } from 'src/common/services/mx-communication/mx-extras-api.service'; +import { DocumentDbService } from 'src/document-db/document-db.service'; +import { ScamInfoTypeEnum } from '../assets/models'; +import { ScamInfo } from '../assets/models/ScamInfo.dto'; import { CacheEventsPublisherService } from '../rabbitmq/cache-invalidation/cache-invalidation-publisher/change-events-publisher.service'; import { CacheEventTypeEnum, ChangedEvent, } from '../rabbitmq/cache-invalidation/events/changed.event'; +import { NftScamElasticService } from './nft-scam.elastic.service'; +import { NftScamService } from './nft-scam.service'; @Injectable() export class CollectionScamService { constructor( - private mxExtrasApiService: MxExtrasApiService, + private readonly documentDbService: DocumentDbService, private readonly cacheEventsPublisher: CacheEventsPublisherService, + private readonly nftScamElasticService: NftScamElasticService, + private readonly nftScamService: NftScamService, ) {} async manuallySetCollectionScamInfo(collection: string): Promise { - //await this.mxExtrasApiService.setCollectionScam(collection); + await Promise.all([ + this.documentDbService.saveOrUpdateCollectionScamInfo( + collection, + 'manual', + ScamInfo.scam(), + ), + this.nftScamElasticService.setNftScamInfoManuallyInElastic( + collection, + ScamInfo.scam(), + ), + await this.nftScamService.markAllNftsForCollection( + collection, + 'manual', + ScamInfo.scam(), + ), + ]); + await this.triggerCacheInvalidation(collection); return true; } async manuallyClearCollectionScamInfo(collection: string): Promise { - //await this.mxExtrasApiService.clearCollectionScam(collection); + await Promise.all([ + this.documentDbService.saveOrUpdateCollectionScamInfo( + collection, + 'manual', + ScamInfo.none(), + ), + this.nftScamElasticService.setCollectionScamInfoManuallyInElastic( + collection, + ScamInfo.none(), + ), + await this.nftScamService.markAllNftsForCollection( + collection, + 'manual', + ScamInfo.none(), + ), + ]); await this.triggerCacheInvalidation(collection); return true; } diff --git a/src/modules/scam/models/collection-scam-info.model.ts b/src/modules/scam/models/collection-scam-info.model.ts new file mode 100644 index 000000000..f7d3e6e25 --- /dev/null +++ b/src/modules/scam/models/collection-scam-info.model.ts @@ -0,0 +1,29 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; + +export type CollectionScamInfoDocument = CollectionScamInfoModel & Document; + +@Schema({ collection: 'collection_scam_info' }) +export class CollectionScamInfoModel { + @Prop() + collectionIdentifier: string; + @Prop({ type: String }) + version: string; + @Prop({ type: String, nullable: true }) + type?: string; + @Prop({ type: String, nullable: true }) + info?: string; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} + +export const CollectionScamInfoSchema = SchemaFactory.createForClass( + CollectionScamInfoModel, +).index( + { + collectionIdentifier: 1, + }, + { unique: true }, +); diff --git a/src/modules/scam/models/nft-scam-data.model.ts b/src/modules/scam/models/nft-scam-data.model.ts index be038bb97..daa162512 100644 --- a/src/modules/scam/models/nft-scam-data.model.ts +++ b/src/modules/scam/models/nft-scam-data.model.ts @@ -1,10 +1,14 @@ -import { Nft } from 'src/common'; import { MxApiAbout } from 'src/common/services/mx-communication/models/mx-api-about.model'; +import { Asset } from 'src/modules/assets/models'; import { NftScamInfoModel } from './nft-scam-info.model'; export class NftScamRelatedData { mxApiAbout?: MxApiAbout; - nftFromApi?: Nft; + nftFromApi?: Asset; nftFromElastic?: any; nftFromDb?: NftScamInfoModel; + + constructor(init?: Partial) { + Object.assign(this, init); + } } diff --git a/src/modules/scam/nft-scam.elastic.service.ts b/src/modules/scam/nft-scam.elastic.service.ts index 1e54144ee..38ff16599 100644 --- a/src/modules/scam/nft-scam.elastic.service.ts +++ b/src/modules/scam/nft-scam.elastic.service.ts @@ -1,7 +1,8 @@ import { Injectable, Logger } from '@nestjs/common'; -import { MxElasticService, Nft } from 'src/common'; +import { MxElasticService } from 'src/common'; import { elasticDictionary } from 'src/config'; -import { ScamInfoTypeEnum } from '../assets/models'; +import { Asset } from '../assets/models'; +import { ScamInfo } from '../assets/models/ScamInfo.dto'; import { NftScamInfoModel } from './models/nft-scam-info.model'; import { getAllCollectionsFromElasticQuery, @@ -40,7 +41,7 @@ export class NftScamElasticService { } async setBulkNftScamInfoInElastic( - nfts: Nft[], + nfts: Asset[], clearScamInfoIfEmpty?: boolean, ): Promise { if (nfts.length > 0) { @@ -73,8 +74,7 @@ export class NftScamElasticService { async setNftScamInfoManuallyInElastic( identifier: string, - type?: ScamInfoTypeEnum, - info?: string, + scamInfo: ScamInfo, ): Promise { try { const updates = [ @@ -82,13 +82,13 @@ export class NftScamElasticService { 'tokens', identifier, elasticDictionary.scamInfo.typeKey, - type ?? null, + scamInfo?.type ?? null, ), this.mxService.buildBulkUpdate( 'tokens', identifier, elasticDictionary.scamInfo.infoKey, - info ?? null, + scamInfo?.info ?? null, ), ]; await this.mxService.bulkRequest('tokens', updates); @@ -103,6 +103,37 @@ export class NftScamElasticService { } } + async setCollectionScamInfoManuallyInElastic( + collection: string, + scamInfo: ScamInfo, + ): Promise { + try { + const updates = [ + this.mxService.buildBulkUpdate( + 'tokens', + collection, + elasticDictionary.scamInfo.typeKey, + scamInfo?.type ?? null, + ), + this.mxService.buildBulkUpdate( + 'tokens', + collection, + elasticDictionary.scamInfo.infoKey, + scamInfo?.info ?? null, + ), + ]; + await this.mxService.bulkRequest('tokens', updates); + } catch (error) { + this.logger.error( + 'Error when manually setting collection scam info in Elastic', + { + path: `${NftScamElasticService.name}.${this.setCollectionScamInfoManuallyInElastic.name}`, + exception: error?.message, + }, + ); + } + } + async getAllCollectionsFromElastic(): Promise { const query = getAllCollectionsFromElasticQuery(); let collections: string[] = []; @@ -119,7 +150,7 @@ export class NftScamElasticService { return collections; } - buildNftScamInfoBulkUpdate(nfts: Nft[], clearScamInfo?: boolean): string[] { + buildNftScamInfoBulkUpdate(nfts: Asset[], clearScamInfo?: boolean): string[] { let updates: string[] = []; for (const nft of nfts) { if (nft.scamInfo) { diff --git a/src/modules/scam/nft-scam.queries.ts b/src/modules/scam/nft-scam.queries.ts index e593cc2fb..78d10dbcb 100644 --- a/src/modules/scam/nft-scam.queries.ts +++ b/src/modules/scam/nft-scam.queries.ts @@ -36,9 +36,7 @@ export const getAllCollectionsFromElasticQuery = (): ElasticQuery => { }); }; -export const getAllCollectionNftsFromElasticQuery = ( - collection: string, -): ElasticQuery => { +export const getCollectionNftsQuery = (collection: string): ElasticQuery => { return ElasticQuery.create() .withMustExistCondition('nonce') .withMustCondition(QueryType.Match('token', collection, QueryOperator.AND)) diff --git a/src/modules/scam/nft-scam.resolver.ts b/src/modules/scam/nft-scam.resolver.ts index 4b816015a..73f6dcd4e 100644 --- a/src/modules/scam/nft-scam.resolver.ts +++ b/src/modules/scam/nft-scam.resolver.ts @@ -16,7 +16,9 @@ export class NftScamResolver { @Args('identifier') identifier: string, ): Promise { try { - return await this.nftScamService.validateOrUpdateNftScamInfo(identifier); + return await this.nftScamService.validateNftScamInfoForIdentifier( + identifier, + ); } catch (error) { throw new ApolloError(error); } diff --git a/src/modules/scam/nft-scam.service.ts b/src/modules/scam/nft-scam.service.ts index 9954af829..0e68c48d3 100644 --- a/src/modules/scam/nft-scam.service.ts +++ b/src/modules/scam/nft-scam.service.ts @@ -1,86 +1,48 @@ import { Injectable, Logger } from '@nestjs/common'; -import { MxApiService, MxElasticService, Nft } from 'src/common'; +import { MxApiService, MxElasticService } from 'src/common'; import { ScamInfo } from '../assets/models/ScamInfo.dto'; -import { ScamInfoTypeEnum } from '../assets/models'; +import { Asset, ScamInfoTypeEnum } from '../assets/models'; import { NftScamElasticService } from './nft-scam.elastic.service'; import { NftScamRelatedData } from './models/nft-scam-data.model'; import { elasticDictionary } from 'src/config'; import { NftScamInfoModel } from './models/nft-scam-info.model'; import { DocumentDbService } from 'src/document-db/document-db.service'; -import { MxApiAbout } from 'src/common/services/mx-communication/models/mx-api-about.model'; import { CacheEventsPublisherService } from '../rabbitmq/cache-invalidation/cache-invalidation-publisher/change-events-publisher.service'; import { CacheEventTypeEnum, ChangedEvent, } from '../rabbitmq/cache-invalidation/events/changed.event'; -import { getAllCollectionNftsFromElasticQuery } from './nft-scam.queries'; +import { getCollectionNftsQuery } from './nft-scam.queries'; import { AssetByIdentifierService } from '../assets'; import { Locker } from '@multiversx/sdk-nestjs'; +import { PluginService } from 'src/common/pluggins/plugin.service'; @Injectable() export class NftScamService { constructor( - private documentDbService: DocumentDbService, - private assetByIdentifierService: AssetByIdentifierService, - private nftScamElasticService: NftScamElasticService, - private mxElasticService: MxElasticService, - private mxApiService: MxApiService, + private readonly documentDbService: DocumentDbService, + private readonly assetByIdentifierService: AssetByIdentifierService, + private readonly nftScamElasticService: NftScamElasticService, + private readonly mxElasticService: MxElasticService, + private readonly mxApiService: MxApiService, + private readonly pluginsService: PluginService, private readonly cacheEventsPublisher: CacheEventsPublisherService, private readonly logger: Logger, ) {} - async validateOrUpdateNftScamInfo( - identifier: string, - nftScamRelatedData?: NftScamRelatedData, - clearManualScamInfo: boolean = false, - ): Promise { - const [nftFromApi, nftFromElastic, nftFromDb, mxApiAbout]: [ - Nft, - any, - NftScamInfoModel, - MxApiAbout, - ] = await this.getNftsAndMxAbout(identifier, nftScamRelatedData); - const scamEngineVersion = mxApiAbout.scamEngineVersion; - - if ( - nftFromDb?.version === elasticDictionary.scamInfo.manualVersionValue && - !clearManualScamInfo - ) { - return true; - } - - if (!nftFromApi.scamInfo) { - await this.validateOrUpdateScamInfoDataForNoScamNft( - scamEngineVersion, - nftFromApi, - nftFromElastic, - nftFromDb, - ); - } else if (nftFromApi.scamInfo) { - await this.validateOrUpdateScamInfoDataForScamNft( - scamEngineVersion, - nftFromApi, - nftFromElastic, - nftFromDb, - ); - } - + async validateNftScamInfoForIdentifier(identifier: string): Promise { + const nft = await this.assetByIdentifierService.getAsset(identifier); + await this.pluginsService.computeScamInfo([nft]); + await this.validateNftScamInfo(nft); return true; } - async getNftsAndMxAbout( - identifier: string, - nftScamRelatedData?: NftScamRelatedData, - ): Promise<[Nft, any, NftScamInfoModel, MxApiAbout]> { - return await Promise.all([ - nftScamRelatedData?.nftFromApi ?? - this.mxApiService.getNftScamInfo(identifier, true), - nftScamRelatedData?.nftFromElastic ?? - this.nftScamElasticService.getNftWithScamInfoFromElastic(identifier), - nftScamRelatedData?.nftFromDb ?? - this.documentDbService.getNftScamInfo(identifier), - nftScamRelatedData?.mxApiAbout ?? this.mxApiService.getMxApiAbout(), - ]); + async validateNftsScamInfoArray(nfts: Asset[]): Promise { + await this.pluginsService.computeScamInfo(nfts); + for (const nft of nfts) { + await this.validateNftScamInfo(nft); + } + return true; } async validateOrUpdateAllNftsScamInfo(): Promise { @@ -126,7 +88,7 @@ export class NftScamService { scamEngineVersion: string, ): Promise { this.logger.log(`Processing scamInfo for ${collection}...`); - const nftsQuery = getAllCollectionNftsFromElasticQuery(collection); + const nftsQuery = getCollectionNftsQuery(collection); await this.mxElasticService.getScrollableList( 'tokens', 'identifier', @@ -140,6 +102,27 @@ export class NftScamService { ); } + async markAllNftsForCollection( + collection: string, + scamEngineVersion: string, + scamInfo: ScamInfo, + ): Promise { + this.logger.log(`Processing scamInfo for ${collection}...`); + const nftsQuery = getCollectionNftsQuery(collection); + await this.mxElasticService.getScrollableList( + 'tokens', + 'identifier', + nftsQuery, + async (nftsBatch) => { + await this.markNftsScamInfoBatch( + nftsBatch, + scamEngineVersion, + scamInfo, + ); + }, + ); + } + async manuallySetNftScamInfo( identifier: string, type: ScamInfoTypeEnum, @@ -157,8 +140,10 @@ export class NftScamService { ), this.nftScamElasticService.setNftScamInfoManuallyInElastic( identifier, - type, - info, + new ScamInfo({ + type: ScamInfoTypeEnum[type], + info: info, + }), ), ]); this.triggerCacheInvalidation(identifier, nft?.ownerAddress); @@ -167,54 +152,82 @@ export class NftScamService { async manuallyClearNftScamInfo(identifier: string): Promise { const nft = await this.assetByIdentifierService.getAsset(identifier); - const cleared = await this.validateOrUpdateNftScamInfo( - identifier, - {}, - true, - ); + await Promise.all([ + this.documentDbService.saveOrUpdateNftScamInfo( + identifier, + 'manual', + ScamInfo.none(), + ), + this.nftScamElasticService.setNftScamInfoManuallyInElastic( + identifier, + ScamInfo.none(), + ), + ]); this.triggerCacheInvalidation(identifier, nft?.ownerAddress); - return cleared; + return true; } - private async triggerCacheInvalidation( - identifier: string, - ownerAddress: string, - ): Promise { - await this.cacheEventsPublisher.publish( - new ChangedEvent({ - id: identifier, - type: CacheEventTypeEnum.AssetRefresh, - address: ownerAddress, - }), - ); + private async validateNftScamInfo(nft: Asset) { + const nftScamRelatedInfo = await this.getNftScamInfo(nft); + + if ( + nftScamRelatedInfo.nftFromDb?.version === + elasticDictionary.scamInfo.manualVersionValue + ) { + return; + } + + if (!nft.scamInfo) { + await this.addScamInfo(nftScamRelatedInfo); + } else if (nft.scamInfo) { + await this.updateScamInfo(nftScamRelatedInfo); + } + await this.triggerCacheInvalidation(nft?.identifier, nft?.ownerAddress); } - private async validateOrUpdateScamInfoDataForNoScamNft( - scamEngineVersion: string, - nftFromApi: Nft, - nftFromElastic: any, - nftFromDb: NftScamInfoModel, + private async getNftScamInfo(nftFromApi: Asset): Promise { + const [nftFromElastic, nftFromDb, mxApiAbout] = await Promise.all([ + this.nftScamElasticService.getNftWithScamInfoFromElastic( + nftFromApi.identifier, + ), + this.documentDbService.getNftScamInfo(nftFromApi.identifier), + this.mxApiService.getMxApiAbout(), + ]); + + return new NftScamRelatedData({ + mxApiAbout, + nftFromElastic, + nftFromDb, + nftFromApi, + }); + } + + private async addScamInfo( + nftScamRelatedData: NftScamRelatedData, ): Promise { const clearScamInfoInElastic = - nftFromElastic?.[elasticDictionary.scamInfo.typeKey]; + nftScamRelatedData.nftFromElastic?.[elasticDictionary.scamInfo.typeKey]; const updateScamInfoInDb = - !nftFromDb || nftFromDb.type || nftFromDb.version !== scamEngineVersion; + !nftScamRelatedData.nftFromDb || + nftScamRelatedData.nftFromDb.type || + nftScamRelatedData.nftFromDb.version !== + nftScamRelatedData.mxApiAbout.scamEngineVersion; let updatePromises = []; if (updateScamInfoInDb) { updatePromises.push( this.documentDbService.saveOrUpdateNftScamInfo( - nftFromApi.identifier, - scamEngineVersion, + nftScamRelatedData.nftFromApi.identifier, + nftScamRelatedData.mxApiAbout.scamEngineVersion, ), ); } if (clearScamInfoInElastic) { updatePromises.push( this.nftScamElasticService.setBulkNftScamInfoInElastic( - [nftFromApi], + [nftScamRelatedData.nftFromApi], true, ), ); @@ -223,43 +236,58 @@ export class NftScamService { await Promise.all(updatePromises); } - private async validateOrUpdateScamInfoDataForScamNft( - scamEngineVersion: string, - nftFromApi: Nft, - nftFromElastic: any, - nftFromDb: NftScamInfoModel, + private async updateScamInfo( + nftScamRelatedData: NftScamRelatedData, ): Promise { - const isElasticScamInfoDifferent = - ScamInfo.areApiAndElasticScamInfoDifferent(nftFromApi, nftFromElastic); - const isDbScamInfoDifferent = ScamInfo.areApiAndDbScamInfoDifferent( - nftFromApi, - nftFromDb, - scamEngineVersion, - ); - let updatePromises = []; - if (isDbScamInfoDifferent) { + if ( + ScamInfo.areApiAndDbScamInfoDifferent( + nftScamRelatedData.nftFromApi, + nftScamRelatedData.nftFromDb, + nftScamRelatedData.mxApiAbout.scamEngineVersion, + ) + ) { updatePromises.push( this.documentDbService.saveOrUpdateNftScamInfo( - nftFromApi.identifier, - scamEngineVersion, + nftScamRelatedData.nftFromApi.identifier, + nftScamRelatedData.mxApiAbout.scamEngineVersion, new ScamInfo({ - type: ScamInfoTypeEnum[nftFromApi.scamInfo.type], - info: nftFromApi.scamInfo.info, + type: ScamInfoTypeEnum[nftScamRelatedData.nftFromApi.scamInfo.type], + info: nftScamRelatedData.nftFromApi.scamInfo.info, }), ), ); } - if (isElasticScamInfoDifferent) { + if ( + ScamInfo.areApiAndElasticScamInfoDifferent( + nftScamRelatedData.nftFromApi, + nftScamRelatedData.nftFromElastic, + ) + ) { updatePromises.push( - this.nftScamElasticService.setBulkNftScamInfoInElastic([nftFromApi]), + this.nftScamElasticService.setBulkNftScamInfoInElastic([ + nftScamRelatedData.nftFromApi, + ]), ); } await Promise.all(updatePromises); } + private async updateBulkScamInfo( + scamEngineVersion: string, + nftFromApi: Asset[], + ): Promise { + await Promise.all([ + this.documentDbService.saveOrUpdateBulkNftScamInfo( + nftFromApi, + scamEngineVersion, + ), + this.nftScamElasticService.setBulkNftScamInfoInElastic(nftFromApi), + ]); + } + private async validateOrUpdateNftsScamInfoBatch( nftsFromElastic: any, scamEngineVersion: string, @@ -268,154 +296,145 @@ export class NftScamService { return; } - const [ - nftsNoScamOutdatedInElastic, - nftsScamOutdatedInElastic, - nftsOutdatedOrMissingFromDb, - nftsToMigrateFromDbToElastic, - ] = await this.filterOutdatedNfts(nftsFromElastic, scamEngineVersion); - - const elasticUpdates = this.nftScamElasticService - .buildNftScamInfoBulkUpdate(nftsScamOutdatedInElastic) - .concat( - this.nftScamElasticService.buildNftScamInfoBulkUpdate( - nftsNoScamOutdatedInElastic, - true, - ), - ) - .concat( - this.nftScamElasticService.buildNftScamInfoDbToElasticMigrationBulkUpdate( - nftsToMigrateFromDbToElastic, - nftsFromElastic, - ), + const [nftsToMigrateFromDbToElastic, nftsMissingFromDb] = + await this.getMissingNftsFromDbOrOutdatedInElastic( + nftsFromElastic, + scamEngineVersion, + ); + + const apiNfts = await this.mxApiService.getNftsByIdentifiers( + nftsMissingFromDb?.map((x) => x.identifier), + ); + const mappedNfts = apiNfts?.map((x) => Asset.fromNft(x)); + if (!mappedNfts) return; + await this.pluginsService.computeScamInfo(mappedNfts); + + const elasticUpdates = + this.nftScamElasticService.buildNftScamInfoDbToElasticMigrationBulkUpdate( + nftsToMigrateFromDbToElastic, + nftsFromElastic, ); await Promise.all([ this.nftScamElasticService.updateBulkNftScamInfoInElastic(elasticUpdates), - this.documentDbService.saveOrUpdateBulkNftScamInfo( - nftsOutdatedOrMissingFromDb, - scamEngineVersion, - ), + this.updateBulkScamInfo(scamEngineVersion, mappedNfts), ]); } - private async filterOutdatedNfts( + private async markNftsScamInfoBatch( nftsFromElastic: any, scamEngineVersion: string, - ): Promise<[Nft[], Nft[], Nft[], NftScamInfoModel[]]> { - const [nftsOutdatedOrMissingFromDb, nftsToMigrateFromDbToElastic]: [ - Nft[], - NftScamInfoModel[], - ] = await this.getMissingNftsFromDbOrOutdatedInElastic( + scamInfo: ScamInfo, + ): Promise { + if (!nftsFromElastic || nftsFromElastic.length === 0) { + return; + } + + const nftsMissingFromDb = await this.getOutdatedNfts( nftsFromElastic, scamEngineVersion, + scamInfo, ); - if ( - !nftsOutdatedOrMissingFromDb || - nftsOutdatedOrMissingFromDb.length === 0 - ) { - return [[], [], [], nftsToMigrateFromDbToElastic]; - } - - const [nftsNoScamOutdatedInElastic, nftsScamOutdatedInElastic]: [ - Nft[], - Nft[], - ] = await this.filterOutdatedNftsInElastic( - nftsOutdatedOrMissingFromDb, - nftsFromElastic, + const apiNfts = await this.mxApiService.getNftsByIdentifiers( + nftsMissingFromDb?.map((x) => x.identifier), ); + if (!apiNfts) return; + let mappedNfts: Asset[] = []; + if (scamInfo.type === ScamInfoTypeEnum.none) { + mappedNfts = apiNfts?.map( + (x) => new Asset({ ...Asset.fromNft(x), scamInfo }), + ); + } else { + mappedNfts = apiNfts?.map((x) => Asset.fromNft(x)); + await this.pluginsService.computeScamInfo(mappedNfts); + } - return [ - nftsNoScamOutdatedInElastic, - nftsScamOutdatedInElastic, - nftsOutdatedOrMissingFromDb, - nftsToMigrateFromDbToElastic, - ]; + await this.updateBulkScamInfo(scamEngineVersion, mappedNfts); } private async getMissingNftsFromDbOrOutdatedInElastic( nftsFromElastic: any, scamEngineVersion: string, - ): Promise<[Nft[], NftScamInfoModel[]]> { - let nftsOutdatedOrMissingFromDb: Nft[] = []; + ): Promise<[NftScamInfoModel[], NftScamInfoModel[]]> { + if (!nftsFromElastic || nftsFromElastic.length === 0) { + return [[], []]; + } let nftsToMigrateFromDbToElastic: NftScamInfoModel[] = []; + let nftsMissingFromDb: NftScamInfoModel[] = []; - const identifiers = nftsFromElastic.map((nft) => nft.identifier); + const identifiers = nftsFromElastic.map( + (nft: { identifier: any }) => nft.identifier, + ); const nftsFromDb: NftScamInfoModel[] = await this.documentDbService.getBulkNftScamInfo(identifiers); - if (!nftsFromElastic || nftsFromElastic.length === 0) { - return [[], []]; - } - - let missingOrOutdatedNftsInDb: string[] = []; - for (let i = 0; i < nftsFromElastic?.length; i++) { const nftFromElastic = nftsFromElastic[i]; const nftFromDb = nftsFromDb?.find( (nft) => nft.identifier === nftFromElastic.identifier, ); - const isNftMissingFromDb = - !nftFromDb || nftFromDb.version !== scamEngineVersion; - - if (isNftMissingFromDb) { - missingOrOutdatedNftsInDb.push(nftFromElastic.identifier); + if (!nftFromDb) { + nftsMissingFromDb.push(nftFromElastic); continue; } - if ( + nftFromDb.version !== scamEngineVersion || ScamInfo.areElasticAndDbScamInfoDifferent(nftFromElastic, nftFromDb) ) { nftsToMigrateFromDbToElastic.push(nftFromDb); } } - if (missingOrOutdatedNftsInDb.length === 0) { - return [[], nftsToMigrateFromDbToElastic]; + return [nftsToMigrateFromDbToElastic, nftsMissingFromDb]; + } + + private async getOutdatedNfts( + nftsFromElastic: any, + scamEngineVersion: string, + scamInfo?: ScamInfo, + ): Promise { + if (!nftsFromElastic || nftsFromElastic.length === 0) { + return []; } + let nftsMissingFromDb: NftScamInfoModel[] = []; - nftsOutdatedOrMissingFromDb = await this.mxApiService.getBulkNftScamInfo( - identifiers, - true, + const identifiers = nftsFromElastic.map( + (nft: { identifier: any }) => nft.identifier, ); - return [nftsOutdatedOrMissingFromDb, nftsToMigrateFromDbToElastic]; - } + const nftsFromDb: NftScamInfoModel[] = + await this.documentDbService.getBulkNftScamInfo(identifiers); - private async filterOutdatedNftsInElastic( - nftsOutdatedOrMissingFromDb: Nft[], - nftsFromElastic: any, - ): Promise<[Nft[], Nft[]]> { - let nftsNoScamOutdatedInElastic: Nft[] = []; - let nftsScamOutdatedInElastic: Nft[] = []; - for (let i = 0; i < nftsOutdatedOrMissingFromDb?.length; i++) { - const nftFromApi = nftsOutdatedOrMissingFromDb[i]; - let nftFromElastic = nftsFromElastic.find( - (nft) => nft.identifier === nftFromApi.identifier, + for (let i = 0; i < nftsFromElastic?.length; i++) { + const nftFromDb = nftsFromDb?.find( + (nft) => nft.identifier === nftsFromElastic[i].identifier, ); - if (!nftFromApi.scamInfo) { - const updateScamInfoInElastic = - nftFromElastic?.[elasticDictionary.scamInfo.typeKey]; - - if (updateScamInfoInElastic) { - nftsNoScamOutdatedInElastic.push(nftFromApi); - } - } else if (nftFromApi.scamInfo) { - const isElasticScamInfoDifferent = - ScamInfo.areApiAndElasticScamInfoDifferent( - nftFromApi, - nftFromElastic, - ); - - if (isElasticScamInfoDifferent) { - nftsScamOutdatedInElastic.push(nftFromApi); - } + if ( + !nftFromDb || + nftFromDb.version !== scamEngineVersion || + nftFromDb.type !== scamInfo?.type + ) { + nftsMissingFromDb.push(nftsFromElastic[i]); } } - return [nftsNoScamOutdatedInElastic, nftsScamOutdatedInElastic]; + + return nftsMissingFromDb; + } + + private async triggerCacheInvalidation( + identifier: string, + ownerAddress: string, + ): Promise { + await this.cacheEventsPublisher.publish( + new ChangedEvent({ + id: identifier, + type: CacheEventTypeEnum.ScamUpdate, + address: ownerAddress, + }), + ); } } diff --git a/src/modules/scam/scam.module.ts b/src/modules/scam/scam.module.ts index 23299a2e2..ad45a40e0 100644 --- a/src/modules/scam/scam.module.ts +++ b/src/modules/scam/scam.module.ts @@ -1,6 +1,7 @@ import { Logger, Module } from '@nestjs/common'; import { CommonModule } from 'src/common.module'; import { DocumentDbModule } from 'src/document-db/document-db.module'; +import { PluginModule } from 'src/plugins/plugin.module'; import { AssetByIdentifierService } from '../assets'; import { AuthModule } from '../auth/auth.module'; import { CacheEventsPublisherModule } from '../rabbitmq/cache-invalidation/cache-invalidation-publisher/change-events-publisher.module'; @@ -16,6 +17,7 @@ import { NftScamService } from './nft-scam.service'; DocumentDbModule, AuthModule, CacheEventsPublisherModule, + PluginModule, ], providers: [ Logger, diff --git a/src/common/services/mx-communication/models/Token.model.ts b/src/modules/usdPrice/Token.model.ts similarity index 76% rename from src/common/services/mx-communication/models/Token.model.ts rename to src/modules/usdPrice/Token.model.ts index 860779906..7e2ba7667 100644 --- a/src/common/services/mx-communication/models/Token.model.ts +++ b/src/modules/usdPrice/Token.model.ts @@ -1,4 +1,8 @@ import { Field, Int, ObjectType } from '@nestjs/graphql'; +import { + ApiToken, + DexToken, +} from '../../common/services/mx-communication/models/api-token.model'; @ObjectType() export class Token { @@ -19,7 +23,7 @@ export class Token { Object.assign(this, init); } - static fromMxApiDexToken(token: any): Token { + static fromMxApiDexToken(token: DexToken): Token { return new Token({ identifier: token.id, symbol: token.symbol, @@ -28,12 +32,13 @@ export class Token { }); } - static fromMxApiToken(token: any): Token { + static fromMxApiToken(token: ApiToken): Token { return new Token({ identifier: token.identifier, symbol: token.ticker, name: token.name, decimals: token.decimals, + priceUsd: token.price, }); } } diff --git a/src/modules/usdPrice/usd-price.resolver.ts b/src/modules/usdPrice/usd-price.resolver.ts index 05c8d5bbe..c5082e41b 100644 --- a/src/modules/usdPrice/usd-price.resolver.ts +++ b/src/modules/usdPrice/usd-price.resolver.ts @@ -2,7 +2,7 @@ import { Resolver, ResolveField, Parent } from '@nestjs/graphql'; import { BaseResolver } from '../common/base.resolver'; import { Price } from '../assets/models'; import { UsdPriceService } from './usd-price.service'; -import { Token } from 'src/common/services/mx-communication/models/Token.model'; +import { Token } from './Token.model'; @Resolver(() => Price) export class UsdPriceResolver extends BaseResolver(Price) { diff --git a/src/modules/usdPrice/usd-price.service.ts b/src/modules/usdPrice/usd-price.service.ts index facd4c219..7a3df8184 100644 --- a/src/modules/usdPrice/usd-price.service.ts +++ b/src/modules/usdPrice/usd-price.service.ts @@ -1,44 +1,31 @@ import { Injectable } from '@nestjs/common'; -import { MxApiService, MxToolsService } from 'src/common'; -import { Token } from 'src/common/services/mx-communication/models/Token.model'; +import { MxApiService } from 'src/common'; import { CacheInfo } from 'src/common/services/caching/entities/cache.info'; import { mxConfig } from 'src/config'; import { computeUsdAmount } from 'src/utils/helpers'; -import { generateCacheKeyFromParams } from 'src/utils/generate-cache-key'; +import { CachingService } from '@multiversx/sdk-nestjs'; +import { Token } from './Token.model'; +import { MxDataApiService } from 'src/common/services/mx-communication/mx-data.service'; import { DateUtils } from 'src/utils/date-utils'; -import { CachingService, Constants } from '@multiversx/sdk-nestjs'; @Injectable() export class UsdPriceService { constructor( - private cacheService: CachingService, + private readonly cacheService: CachingService, private readonly mxApiService: MxApiService, - private readonly mxToolsService: MxToolsService, + private readonly mxDataApi: MxDataApiService, ) {} - async getUsdAmountDenom( - token: string, - amount: string, - timestamp?: number, - ): Promise { + async getUsdAmountDenom(token: string, amount: string): Promise { if (amount === '0') { return amount; } - if (token === mxConfig.egld || token === mxConfig.wegld) { - return computeUsdAmount( - await this.getEgldPrice(timestamp), - amount, - mxConfig.decimals, - ); - } - - const tokenPriceUsd = await this.getEsdtPriceUsd(token, timestamp); - if (!tokenPriceUsd) { + const tokenData = await this.getToken(token); + if (!tokenData.priceUsd) { return; } - const tokenData = await this.getToken(token); - return computeUsdAmount(tokenPriceUsd, amount, tokenData.decimals); + return computeUsdAmount(tokenData.priceUsd, amount, tokenData.decimals); } public async getAllCachedTokens(): Promise { @@ -49,14 +36,25 @@ export class UsdPriceService { ); } - public async getToken(tokenId: string): Promise { - if (tokenId === mxConfig.egld) { + public async getTokenPriceFromDate( + token: string, + timestamp: number, + ): Promise { + return await this.cacheService.getOrSetCache( + `${CacheInfo.TokenHistoricalPrice.key}_${token}_${timestamp}`, + async () => await this.getTokenHistoricalPrice(token, timestamp), + CacheInfo.TokenHistoricalPrice.ttl, + ); + } + + public async getToken(tokenId: string): Promise { + if (tokenId === mxConfig.egld || tokenId === mxConfig.wegld) { return new Token({ identifier: mxConfig.egld, symbol: mxConfig.egld, name: mxConfig.egld, decimals: mxConfig.decimals, - priceUsd: await this.getEgldPrice(), + priceUsd: await this.getCurrentEgldPrice(), }); } @@ -75,56 +73,23 @@ export class UsdPriceService { async getTokenPriceUsd(token: string): Promise { if (token === mxConfig.egld || token === mxConfig.wegld) { - return await this.getEgldPrice(); + return await this.getCurrentEgldPrice(); } return await this.getEsdtPriceUsd(token); } - private async getEsdtPriceUsd( - tokenId: string, - timestamp?: number, - ): Promise { - if (!timestamp || DateUtils.isTimestampToday(timestamp)) { - const dexTokens = await this.getCachedDexTokens(); - const token = dexTokens.find((token) => token.identifier === tokenId); - return token?.priceUsd; - } - - return await this.getTokenHistoricalPriceByEgld(tokenId, timestamp); - } - - private async getTokenHistoricalPriceByEgld( - token: string, - timestamp: number, - ): Promise { - const isoDateOnly = DateUtils.timestampToIsoStringWithoutTime(timestamp); - const egldPriceUsd = await this.getEgldHistoricalPrice(timestamp); - const cacheKey = this.getTokenHistoricalPriceCacheKey(token, isoDateOnly); - return await this.cacheService.getOrSetCache( - cacheKey, - async () => - await this.mxToolsService.getTokenHistoricalPriceByEgld( - token, - isoDateOnly, - egldPriceUsd, - ), - DateUtils.isTimestampToday(timestamp) - ? Constants.oneDay() - : CacheInfo.TokenHistoricalPrice.ttl, - ); + private async getEsdtPriceUsd(tokenId: string): Promise { + const dexTokens = await this.getCachedDexTokens(); + const token = dexTokens.find((token) => token.identifier === tokenId); + return token?.priceUsd; } private async setAllCachedTokens(): Promise { - let [apiTokens, dexTokens, egldPriceUSD] = await Promise.all([ + let [apiTokens, egldPriceUSD] = await Promise.all([ this.getCachedApiTokens(), - this.getCachedDexTokens(), - this.getEgldPrice(), + this.getCurrentEgldPrice(), ]); - dexTokens.map((dexToken) => { - apiTokens.find( - (apiToken) => apiToken.identifier === dexToken.identifier, - ).priceUsd = dexToken.priceUsd; - }); + const egldToken: Token = new Token({ identifier: mxConfig.egld, symbol: mxConfig.egld, @@ -135,6 +100,29 @@ export class UsdPriceService { return apiTokens.concat([egldToken]); } + private async getTokenHistoricalPrice( + tokenId: string, + timestamp: number, + ): Promise { + let [cexTokens, xExchangeTokens] = await Promise.all([ + this.getCexTokens(), + this.getXexchangeTokens(), + ]); + + if (cexTokens.includes(tokenId)) { + { + return await this.mxDataApi.getCexPrice( + DateUtils.timestampToIsoStringWithoutTime(timestamp), + ); + } + } else if (xExchangeTokens.includes(tokenId)) { + return await this.mxDataApi.getXechangeTokenPrice( + tokenId, + DateUtils.timestampToIsoStringWithoutTime(timestamp), + ); + } + } + private async getCachedDexTokens(): Promise { return await this.cacheService.getOrSetCache( CacheInfo.AllDexTokens.key, @@ -151,13 +139,6 @@ export class UsdPriceService { ); } - private async getEgldPrice(timestamp?: number): Promise { - if (!timestamp || DateUtils.isTimestampToday(timestamp)) { - return await this.getCurrentEgldPrice(); - } - return await this.getEgldHistoricalPrice(timestamp); - } - private async getCurrentEgldPrice(): Promise { return await this.cacheService.getOrSetCache( CacheInfo.EgldToken.key, @@ -166,25 +147,19 @@ export class UsdPriceService { ); } - private async getEgldHistoricalPrice(timestamp?: number): Promise { - const isoDateOnly = DateUtils.timestampToIsoStringWithoutTime(timestamp); - const cacheKey = this.getTokenHistoricalPriceCacheKey( - mxConfig.wegld, - isoDateOnly, - ); + private async getCexTokens(): Promise { return await this.cacheService.getOrSetCache( - cacheKey, - async () => await this.mxToolsService.getEgldHistoricalPrice(isoDateOnly), - DateUtils.isTimestampToday(timestamp) - ? Constants.oneDay() - : CacheInfo.TokenHistoricalPrice.ttl, + CacheInfo.CexTokens.key, + async () => await this.mxDataApi.getCexTokens(), + CacheInfo.CexTokens.ttl, ); } - private getTokenHistoricalPriceCacheKey( - token: string, - isoDateOnly: string, - ): string { - return generateCacheKeyFromParams(token, isoDateOnly); + private async getXexchangeTokens(): Promise { + return await this.cacheService.getOrSetCache( + CacheInfo.xExchangeTokens.key, + async () => await this.mxDataApi.getXexchangeTokens(), + CacheInfo.xExchangeTokens.ttl, + ); } } diff --git a/src/modules/usdPrice/usd-token-price.resolver.ts b/src/modules/usdPrice/usd-token-price.resolver.ts index 3c5de53e7..c81caaa77 100644 --- a/src/modules/usdPrice/usd-token-price.resolver.ts +++ b/src/modules/usdPrice/usd-token-price.resolver.ts @@ -1,7 +1,7 @@ import { Resolver, ResolveField, Parent } from '@nestjs/graphql'; +import { Token } from './Token.model'; import { BaseResolver } from '../common/base.resolver'; import { UsdPriceService } from './usd-price.service'; -import { Token } from 'src/common/services/mx-communication/models/Token.model'; @Resolver(() => Token) export class UsdTokenPriceResolver extends BaseResolver(Token) { diff --git a/src/plugins.template/plugins.module.ts.template b/src/plugins.template/plugins.module.ts.template new file mode 100644 index 000000000..c414b34bb --- /dev/null +++ b/src/plugins.template/plugins.module.ts.template @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { PluginService } from 'src/common/pluggins/plugin.service'; + +@Module({ + providers: [PluginService], + exports: [PluginService], +}) +export class PluginModule {} diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 9e5090906..3d598e1f1 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -20,3 +20,4 @@ export const DEADRARE_KEY = 'deadrare'; export const ELRONDNFTSWAP_KEY = 'elrondnftswap'; export const ENEFTOR_KEY = 'eneftor'; export const FRAMEIT_KEY = 'frameit'; +export const ICI_KEY = 'ici'; diff --git a/src/utils/date-utils.ts b/src/utils/date-utils.ts index 471837bf5..e283b8898 100644 --- a/src/utils/date-utils.ts +++ b/src/utils/date-utils.ts @@ -35,6 +35,10 @@ export class DateUtils { return new Date(timestamp * 1000).toJSON().slice(0, 10); } + static getUtcDateFromTimestamp(timestamp: number): Date { + return new Date(new Date(timestamp * 1000).toUTCString()); + } + static timestampToIsoStringWithoutTime(timestamp: number): string { let date = new Date(timestamp * 1000); date.setUTCHours(0, 0, 0, 0); diff --git a/src/utils/dynamic.module.utils.ts b/src/utils/dynamic.module.utils.ts index 7030370a1..4c363236c 100644 --- a/src/utils/dynamic.module.utils.ts +++ b/src/utils/dynamic.module.utils.ts @@ -1,6 +1,11 @@ import { + ApiModule, + ApiModuleOptions, + ApiService, CachingModule, CachingModuleOptions, + ElasticModule, + ElasticModuleOptions, RedisCacheModule, RedisCacheModuleOptions, } from '@multiversx/sdk-nestjs'; @@ -10,9 +15,35 @@ import { ClientProxyFactory, Transport, } from '@nestjs/microservices'; +import { ApiConfigModule } from 'src/modules/common/api-config/api.config.module'; import { ApiConfigService } from 'src/modules/common/api-config/api.config.service'; export class DynamicModuleUtils { + static getElasticModule(): DynamicModule { + return ElasticModule.forRootAsync({ + useFactory: (apiConfigService: ApiConfigService) => + new ElasticModuleOptions({ + url: apiConfigService.getElasticUrl(), + customValuePrefix: 'nft', + }), + inject: [ApiConfigService, ApiService], + }); + } + + static getApiModule(): DynamicModule { + return ApiModule.forRootAsync({ + imports: [ApiConfigModule], + useFactory: (apiConfigService: ApiConfigService) => + new ApiModuleOptions({ + axiosTimeout: apiConfigService.getAxiosTimeout(), + rateLimiterSecret: apiConfigService.getRateLimiterSecret(), + serverTimeout: apiConfigService.getServerTimeout(), + useKeepAliveAgent: apiConfigService.getUseKeepAliveAgentFlag(), + }), + inject: [ApiConfigService], + }); + } + static getCachingModule(): DynamicModule { return CachingModule.forRootAsync({ useFactory: (apiConfigService: ApiConfigService) =>