From 166701a3a4a32ae91a64dcbea143b5eee2c7dc14 Mon Sep 17 00:00:00 2001 From: Boldizsar Mezei Date: Thu, 26 Oct 2023 21:24:03 +0200 Subject: [PATCH 1/2] Auction Fixes Fixes Fixes Fixes Fixes --- .github/workflows/action_deploy-prod.yml | 4 +- .github/workflows/action_deploy-wen.yml | 4 +- .../workflows/functions_emulated-tests.yml | 87 ++- .../functions_online-emulated-tests.yml | 49 +- ...ions_tangle-online-unit-tests_emulator.yml | 293 +++++----- .../workflows/functions_tangle-unit-tests.yml | 305 ++++++----- packages/api/package.json | 13 +- packages/database/package.json | 12 +- packages/functions/package.json | 4 +- .../scripts/db.upgrade.ts | 2 +- .../scripts/dbUpgrades/1.0.0/auction.roll.ts | 105 ++++ .../scripts/dbUpgrades/1.0.0/soonProjects.ts | 3 +- .../auction/AuctionBidRequestSchema.ts | 10 + .../auction/AuctionCreateRequestSchema.ts | 74 +++ .../src/controls/auction/auction.control.ts | 25 + .../auction/auction.create.control.ts | 23 + .../award/AwardCreateRequestSchema.ts | 4 +- .../controls/nft/NftDepositRequestSchema.ts | 2 +- .../src/controls/nft/nft.bid.control.ts | 9 +- .../src/controls/nft/nft.set.for.sale.ts | 15 +- packages/functions/src/cron/auction.cron.ts | 27 + packages/functions/src/cron/nft.cron.ts | 22 - packages/functions/src/runtime/common.ts | 2 +- packages/functions/src/runtime/cron/index.ts | 7 +- .../src/runtime/firebase/auction/index.ts | 6 + packages/functions/src/runtime/https/index.ts | 16 + .../src/services/notification/notification.ts | 39 +- .../payment/auction/auction-bid.service.ts | 189 +++++++ .../auction/auction.finalize.service.ts | 101 ++++ .../src/services/payment/nft/common.ts | 220 +------- .../services/payment/nft/nft-bid.service.ts | 89 --- .../payment/nft/nft-purchase.service.ts | 63 ++- .../services/payment/payment-processing.ts | 5 +- .../tangle-service/TangleRequestService.ts | 9 +- .../auction/AuctionBidTangleRequestSchema.ts | 12 + .../AuctionCreateTangleRequestSchema.ts | 13 + .../auction/NftBidTangleRequestSchema.ts | 16 + .../auction/auction.bid.order.ts | 164 ++++++ .../auction/auction.bid.service.ts | 88 +++ .../auction/auction.create.service.ts | 69 +++ .../tangle-service/nft/nft-bid.service.ts | 159 ------ .../nft/nft-set-for-sale.service.ts | 64 ++- .../services/payment/transaction-service.ts | 3 + .../src/triggers/collection.trigger.ts | 59 +- packages/functions/src/utils/config.utils.ts | 2 + .../auction-tangle/auction.bit.tangle.spec.ts | 127 +++++ .../collection-minting_4.spec.ts | 160 ------ .../collection-minting_4_a.spec.ts | 116 ++++ .../collection-minting_4_b.spec.ts | 122 +++++ .../collection-minting_4_c.spec.ts | 31 ++ .../minted-nft-trading_1.spec.ts | 181 +++--- .../minted-nft-trading_1_b.spec.ts | 166 +++--- .../functions/test-tangle/nft-bid/Helper.ts | 6 +- .../test-tangle/nft-bid/nft-bid.otr_1.spec.ts | 8 +- .../test-tangle/nft-bid/nft-bid.otr_2.spec.ts | 8 +- .../test-tangle/nft-bid/nft-bid.otr_3.spec.ts | 8 +- .../test-tangle/nft-bid/nft-bid.otr_4.spec.ts | 14 +- .../nft-set-for-sale_3.spec.ts | 4 +- .../deposit-withraw-nft_4.spec.ts | 1 - .../functions/test/controls/auction/Helper.ts | 47 ++ .../test/controls/auction/auction.bid.spec.ts | 91 ++++ .../test/controls/nft-bidding.spec.ts | 513 ------------------ .../functions/test/controls/nft/Helper.ts | 122 +++++ .../controls/nft/nft.bidding.extends.spec.ts | 154 ++++++ .../controls/nft/nft.bidding.finalize.spec.ts | 110 ++++ .../test/controls/nft/nft.bidding.spec.ts | 207 +++++++ .../controls/nft/nft.set.for.sale.spec.ts | 89 +++ .../test/dbroll/nft.auction.roll.spec.ts | 176 ++++++ .../src/api/post/AuctionBidRequest.ts | 14 + .../src/api/post/AuctionCreateRequest.ts | 42 ++ .../src/api/post/AwardCreateRequest.ts | 4 +- .../src/api/post/NftDepositRequest.ts | 2 +- packages/interfaces/src/api/post/index.ts | 2 + .../src/api/tangle/AuctionBidTangleRequest.ts | 18 + .../api/tangle/AuctionCreateTangleRequest.ts | 46 ++ .../api/tangle/AwardCreateTangleRequest.ts | 4 +- .../src/api/tangle/NftBidTangleRequest.ts | 2 +- packages/interfaces/src/api/tangle/common.ts | 3 + packages/interfaces/src/api/tangle/index.ts | 3 + packages/interfaces/src/errors.ts | 4 +- packages/interfaces/src/functions/index.ts | 4 +- packages/interfaces/src/models/auction.ts | 37 ++ packages/interfaces/src/models/base.ts | 1 + packages/interfaces/src/models/index.ts | 1 + packages/interfaces/src/models/nft.ts | 8 +- .../interfaces/src/models/notification.ts | 30 +- .../src/models/transaction/common.ts | 1 + .../src/models/transaction/payload.ts | 2 + 88 files changed, 3373 insertions(+), 1803 deletions(-) rename packages/{database => functions}/scripts/db.upgrade.ts (96%) create mode 100644 packages/functions/scripts/dbUpgrades/1.0.0/auction.roll.ts rename packages/{database => functions}/scripts/dbUpgrades/1.0.0/soonProjects.ts (95%) create mode 100644 packages/functions/src/controls/auction/AuctionBidRequestSchema.ts create mode 100644 packages/functions/src/controls/auction/AuctionCreateRequestSchema.ts create mode 100644 packages/functions/src/controls/auction/auction.control.ts create mode 100644 packages/functions/src/controls/auction/auction.create.control.ts create mode 100644 packages/functions/src/cron/auction.cron.ts create mode 100644 packages/functions/src/runtime/firebase/auction/index.ts create mode 100644 packages/functions/src/services/payment/auction/auction-bid.service.ts create mode 100644 packages/functions/src/services/payment/auction/auction.finalize.service.ts delete mode 100644 packages/functions/src/services/payment/nft/nft-bid.service.ts create mode 100644 packages/functions/src/services/payment/tangle-service/auction/AuctionBidTangleRequestSchema.ts create mode 100644 packages/functions/src/services/payment/tangle-service/auction/AuctionCreateTangleRequestSchema.ts create mode 100644 packages/functions/src/services/payment/tangle-service/auction/NftBidTangleRequestSchema.ts create mode 100644 packages/functions/src/services/payment/tangle-service/auction/auction.bid.order.ts create mode 100644 packages/functions/src/services/payment/tangle-service/auction/auction.bid.service.ts create mode 100644 packages/functions/src/services/payment/tangle-service/auction/auction.create.service.ts delete mode 100644 packages/functions/src/services/payment/tangle-service/nft/nft-bid.service.ts create mode 100644 packages/functions/test-tangle/auction-tangle/auction.bit.tangle.spec.ts delete mode 100644 packages/functions/test-tangle/collection-minting/collection-minting_4.spec.ts create mode 100644 packages/functions/test-tangle/collection-minting/collection-minting_4_a.spec.ts create mode 100644 packages/functions/test-tangle/collection-minting/collection-minting_4_b.spec.ts create mode 100644 packages/functions/test-tangle/collection-minting/collection-minting_4_c.spec.ts create mode 100644 packages/functions/test/controls/auction/Helper.ts create mode 100644 packages/functions/test/controls/auction/auction.bid.spec.ts delete mode 100644 packages/functions/test/controls/nft-bidding.spec.ts create mode 100644 packages/functions/test/controls/nft/Helper.ts create mode 100644 packages/functions/test/controls/nft/nft.bidding.extends.spec.ts create mode 100644 packages/functions/test/controls/nft/nft.bidding.finalize.spec.ts create mode 100644 packages/functions/test/controls/nft/nft.bidding.spec.ts create mode 100644 packages/functions/test/controls/nft/nft.set.for.sale.spec.ts create mode 100644 packages/functions/test/dbroll/nft.auction.roll.spec.ts create mode 100644 packages/interfaces/src/api/post/AuctionBidRequest.ts create mode 100644 packages/interfaces/src/api/post/AuctionCreateRequest.ts create mode 100644 packages/interfaces/src/api/tangle/AuctionBidTangleRequest.ts create mode 100644 packages/interfaces/src/api/tangle/AuctionCreateTangleRequest.ts create mode 100644 packages/interfaces/src/models/auction.ts diff --git a/.github/workflows/action_deploy-prod.yml b/.github/workflows/action_deploy-prod.yml index 398f224852..57e222cb53 100644 --- a/.github/workflows/action_deploy-prod.yml +++ b/.github/workflows/action_deploy-prod.yml @@ -90,7 +90,7 @@ jobs: needs: [deploy_functions] defaults: run: - working-directory: packages/database + working-directory: packages/functions steps: - uses: actions/checkout@v3 with: @@ -101,7 +101,7 @@ jobs: - name: Install dependencies run: | npm i -g ts-node - cd ../../ && npm run build:database + cd ../../ && npm run build:functions - name: Set env vars run: echo "$FUNCTIONS_ENV_VARS" >> .env env: diff --git a/.github/workflows/action_deploy-wen.yml b/.github/workflows/action_deploy-wen.yml index ffca6628a3..dbe2348daa 100644 --- a/.github/workflows/action_deploy-wen.yml +++ b/.github/workflows/action_deploy-wen.yml @@ -70,7 +70,7 @@ jobs: needs: [deploy_functions] defaults: run: - working-directory: packages/database + working-directory: packages/functions steps: - uses: actions/checkout@v3 with: @@ -81,7 +81,7 @@ jobs: - name: Install dependencies run: | npm i -g ts-node - cd ../../ && npm run build:database + cd ../../ && npm run build:functions - name: Set env vars run: echo "$FUNCTIONS_ENV_VARS" >> .env env: diff --git a/.github/workflows/functions_emulated-tests.yml b/.github/workflows/functions_emulated-tests.yml index 24d0fc5ab4..b15d8338eb 100644 --- a/.github/workflows/functions_emulated-tests.yml +++ b/.github/workflows/functions_emulated-tests.yml @@ -54,11 +54,11 @@ jobs: firebase emulators:exec " npm run test:ci -- --forceExit --findRelatedTests ./test/auth.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/controls/address.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/auction/auction.bid.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/controls/collection.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/controls/member.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/nft-bidding.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/controls/nft.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/order.spec.ts + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/nft/nft.bidding.extends.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -93,13 +93,13 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/nft/nft.bidding.finalize.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/nft/nft.bidding.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/nft/nft.set.for.sale.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/order.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/controls/project/project.create.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/controls/project/project.deactivate.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/proposal.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/space.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/stake.reward.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/stamp.control.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token-distribution-auto-trigger.spec.ts + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/proposal.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -134,13 +134,13 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/space.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/stake.reward.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/stamp.control.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token-distribution-auto-trigger.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token-distribution.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token-trade.buy.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token-trade.sell.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token-trade.trigger.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token.expired.sale.cron.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token.order.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.airdrop.claim.spec.ts + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token-trade.sell.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -175,13 +175,13 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token-trade.trigger.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token.expired.sale.cron.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token.order.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.airdrop.claim.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.airdrop.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.cancel.pub.sale.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.create.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.order.and.claim.air.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.rank.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.set.to.sale.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.update.spec.ts + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.create.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -216,13 +216,13 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.order.and.claim.air.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.rank.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.set.to.sale.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.update.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/controls/token/token.vote.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/controls/workflow-online.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/controls/workflow.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/cron/floor-price.cron.only.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/cron/nft-stake.cron.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/cron/proposal.cron.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/db.roll.spec.ts + npm run test:ci -- --forceExit --findRelatedTests ./test/controls/workflow.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -257,9 +257,13 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test:ci -- --forceExit --findRelatedTests ./test/cron/floor-price.cron.only.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/cron/nft-stake.cron.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/cron/proposal.cron.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/db.roll.spec.ts && + npm run test:ci -- --forceExit --findRelatedTests ./test/dbroll/nft.auction.roll.spec.ts && npm run test:ci -- --forceExit --findRelatedTests ./test/stake/delete.stake.reward.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/stake/stake.reward.cron.spec.ts && - npm run test:ci -- --forceExit --findRelatedTests ./test/storage/resize.img.spec.ts + npm run test:ci -- --forceExit --findRelatedTests ./test/stake/stake.reward.cron.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -268,3 +272,38 @@ jobs: name: firestore-data-test-chunk_5 path: ./packages/functions/firestore-data/ retention-days: 1 + chunk_6: + needs: npm-install + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - uses: actions/cache@v3 + with: + path: | + node_modules + packages/functions/node_modules + packages/interfaces/node_modules + key: ${{ runner.os }}-modules-${{ hashFiles('**/package.json') }} + - name: Init + run: | + npm run build:functions + npm install -g firebase-tools + - name: Test + working-directory: packages/functions + run: | + export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" + npm run milestone-sync & + firebase emulators:exec " + npm run test:ci -- --forceExit --findRelatedTests ./test/storage/resize.img.spec.ts + " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data + - name: Archive firestore data + uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: firestore-data-test-chunk_6 + path: ./packages/functions/firestore-data/ + retention-days: 1 diff --git a/.github/workflows/functions_online-emulated-tests.yml b/.github/workflows/functions_online-emulated-tests.yml index 0a6bd7ecf7..28a00fc3ff 100644 --- a/.github/workflows/functions_online-emulated-tests.yml +++ b/.github/workflows/functions_online-emulated-tests.yml @@ -56,11 +56,11 @@ jobs: firebase emulators:exec " npm run test-online:ci -- --forceExit --findRelatedTests ./test/auth.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/address.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/auction/auction.bid.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/collection.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/member.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/nft-bidding.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/nft.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/order.spec.ts + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/nft/nft.bidding.extends.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -95,13 +95,13 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/nft/nft.bidding.finalize.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/nft/nft.bidding.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/nft/nft.set.for.sale.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/order.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/project/project.create.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/project/project.deactivate.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/proposal.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/space.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/stake.reward.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/stamp.control.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token-distribution-auto-trigger.spec.ts + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/proposal.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -136,13 +136,13 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/space.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/stake.reward.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/stamp.control.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token-distribution-auto-trigger.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token-distribution.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token-trade.buy.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token-trade.sell.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token-trade.trigger.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token.expired.sale.cron.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token.order.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.airdrop.claim.spec.ts + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token-trade.sell.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -177,13 +177,13 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token-trade.trigger.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token.expired.sale.cron.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token.order.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.airdrop.claim.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.airdrop.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.cancel.pub.sale.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.create.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.order.and.claim.air.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.rank.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.set.to.sale.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.update.spec.ts + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.create.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -218,13 +218,13 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.order.and.claim.air.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.rank.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.set.to.sale.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.update.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/token/token.vote.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/workflow-online.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/workflow.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/cron/nft-stake.cron.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/cron/proposal.cron.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/db.roll.spec.ts && - npm run test-online:ci -- --forceExit --findRelatedTests ./test/stake/delete.stake.reward.spec.ts + npm run test-online:ci -- --forceExit --findRelatedTests ./test/controls/workflow.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -259,6 +259,11 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-online:ci -- --forceExit --findRelatedTests ./test/cron/nft-stake.cron.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/cron/proposal.cron.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/db.roll.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/dbroll/nft.auction.roll.spec.ts && + npm run test-online:ci -- --forceExit --findRelatedTests ./test/stake/delete.stake.reward.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/stake/stake.reward.cron.spec.ts && npm run test-online:ci -- --forceExit --findRelatedTests ./test/storage/resize.img.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data diff --git a/.github/workflows/functions_tangle-online-unit-tests_emulator.yml b/.github/workflows/functions_tangle-online-unit-tests_emulator.yml index 553c342b96..cc08a736e5 100644 --- a/.github/workflows/functions_tangle-online-unit-tests_emulator.yml +++ b/.github/workflows/functions_tangle-online-unit-tests_emulator.yml @@ -55,8 +55,8 @@ jobs: npm run milestone-sync & firebase emulators:exec " npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/address.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_1.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_2.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/auction-tangle/auction.bit.tangle.spec.ts && + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_1.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -91,9 +91,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_2.spec.ts && npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_3.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_4.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_5.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_4.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -128,9 +128,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_5.spec.ts && npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_6.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_7.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_1.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_7.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -165,9 +165,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_1.spec.ts && npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_10.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_2.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_3.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_2.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -202,9 +202,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_3.spec.ts && npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_4.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_5.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_6.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_5.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -239,9 +239,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_6.spec.ts && npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_7.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_8.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_9.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_8.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -276,9 +276,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_9.spec.ts && npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_1.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_10.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_a.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_10.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -313,9 +313,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_a.spec.ts && npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_b.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_c.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_d.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_c.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -350,9 +350,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_d.spec.ts && npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_12.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_13.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_2.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_13.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -387,9 +387,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_2.spec.ts && npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_3.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_4.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_5.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_4.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -424,9 +424,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_5.spec.ts && npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_6.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_7.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_8.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_7.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -461,9 +461,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_8.spec.ts && npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_9.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_1.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_10.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_1.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -498,9 +498,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_10.spec.ts && npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_11.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_12.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_2.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_12.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -535,9 +535,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_4.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_5.spec.ts && - npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_6.spec.ts + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_2.spec.ts && + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_4_a.spec.ts && + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_4_b.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -547,6 +547,43 @@ jobs: path: ./packages/functions/firestore-data/ retention-days: 1 chunk_14: + needs: npm-install + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - uses: actions/cache@v3 + with: + path: | + node_modules + packages/functions/node_modules + packages/interfaces/node_modules + key: ${{ runner.os }}-modules-${{ hashFiles('**/package.json') }} + - name: Init + run: | + npm run build:functions + npm install -g firebase-tools + - name: Test + working-directory: packages/functions + run: | + export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" + npm run milestone-sync & + firebase emulators:exec " + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_4_c.spec.ts && + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_5.spec.ts && + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_6.spec.ts + " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data + - name: Archive firestore data + uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: firestore-data-test-tangle-online-chunk_14 + path: ./packages/functions/firestore-data/ + retention-days: 1 + chunk_15: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -580,10 +617,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_14 + name: firestore-data-test-tangle-online-chunk_15 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_15: + chunk_16: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -617,10 +654,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_15 + name: firestore-data-test-tangle-online-chunk_16 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_16: + chunk_17: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -654,10 +691,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_16 + name: firestore-data-test-tangle-online-chunk_17 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_17: + chunk_18: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -691,10 +728,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_17 + name: firestore-data-test-tangle-online-chunk_18 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_18: + chunk_19: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -728,10 +765,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_18 + name: firestore-data-test-tangle-online-chunk_19 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_19: + chunk_20: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -765,10 +802,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_19 + name: firestore-data-test-tangle-online-chunk_20 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_20: + chunk_21: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -802,10 +839,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_20 + name: firestore-data-test-tangle-online-chunk_21 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_21: + chunk_22: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -839,10 +876,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_21 + name: firestore-data-test-tangle-online-chunk_22 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_22: + chunk_23: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -876,10 +913,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_22 + name: firestore-data-test-tangle-online-chunk_23 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_23: + chunk_24: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -913,10 +950,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_23 + name: firestore-data-test-tangle-online-chunk_24 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_24: + chunk_25: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -950,10 +987,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_24 + name: firestore-data-test-tangle-online-chunk_25 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_25: + chunk_26: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -987,10 +1024,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_25 + name: firestore-data-test-tangle-online-chunk_26 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_26: + chunk_27: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1024,10 +1061,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_26 + name: firestore-data-test-tangle-online-chunk_27 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_27: + chunk_28: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1061,10 +1098,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_27 + name: firestore-data-test-tangle-online-chunk_28 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_28: + chunk_29: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1098,10 +1135,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_28 + name: firestore-data-test-tangle-online-chunk_29 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_29: + chunk_30: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1135,10 +1172,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_29 + name: firestore-data-test-tangle-online-chunk_30 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_30: + chunk_31: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1172,10 +1209,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_30 + name: firestore-data-test-tangle-online-chunk_31 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_31: + chunk_32: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1209,10 +1246,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_31 + name: firestore-data-test-tangle-online-chunk_32 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_32: + chunk_33: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1246,10 +1283,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_32 + name: firestore-data-test-tangle-online-chunk_33 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_33: + chunk_34: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1283,10 +1320,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_33 + name: firestore-data-test-tangle-online-chunk_34 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_34: + chunk_35: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1320,10 +1357,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_34 + name: firestore-data-test-tangle-online-chunk_35 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_35: + chunk_36: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1357,10 +1394,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_35 + name: firestore-data-test-tangle-online-chunk_36 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_36: + chunk_37: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1394,10 +1431,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_36 + name: firestore-data-test-tangle-online-chunk_37 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_37: + chunk_38: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1431,10 +1468,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_37 + name: firestore-data-test-tangle-online-chunk_38 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_38: + chunk_39: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1468,10 +1505,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_38 + name: firestore-data-test-tangle-online-chunk_39 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_39: + chunk_40: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1505,10 +1542,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_39 + name: firestore-data-test-tangle-online-chunk_40 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_40: + chunk_41: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1542,10 +1579,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_40 + name: firestore-data-test-tangle-online-chunk_41 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_41: + chunk_42: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1579,10 +1616,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_41 + name: firestore-data-test-tangle-online-chunk_42 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_42: + chunk_43: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1616,10 +1653,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_42 + name: firestore-data-test-tangle-online-chunk_43 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_43: + chunk_44: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1653,10 +1690,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_43 + name: firestore-data-test-tangle-online-chunk_44 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_44: + chunk_45: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1690,10 +1727,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_44 + name: firestore-data-test-tangle-online-chunk_45 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_45: + chunk_46: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1727,10 +1764,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_45 + name: firestore-data-test-tangle-online-chunk_46 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_46: + chunk_47: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1764,10 +1801,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_46 + name: firestore-data-test-tangle-online-chunk_47 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_47: + chunk_48: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1801,10 +1838,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_47 + name: firestore-data-test-tangle-online-chunk_48 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_48: + chunk_49: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1838,10 +1875,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_48 + name: firestore-data-test-tangle-online-chunk_49 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_49: + chunk_50: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1875,10 +1912,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_49 + name: firestore-data-test-tangle-online-chunk_50 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_50: + chunk_51: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1912,10 +1949,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_50 + name: firestore-data-test-tangle-online-chunk_51 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_51: + chunk_52: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1949,10 +1986,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_51 + name: firestore-data-test-tangle-online-chunk_52 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_52: + chunk_53: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1986,10 +2023,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_52 + name: firestore-data-test-tangle-online-chunk_53 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_53: + chunk_54: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2023,10 +2060,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_53 + name: firestore-data-test-tangle-online-chunk_54 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_54: + chunk_55: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2060,10 +2097,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_54 + name: firestore-data-test-tangle-online-chunk_55 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_55: + chunk_56: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2097,10 +2134,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_55 + name: firestore-data-test-tangle-online-chunk_56 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_56: + chunk_57: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2134,10 +2171,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_56 + name: firestore-data-test-tangle-online-chunk_57 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_57: + chunk_58: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2171,10 +2208,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_57 + name: firestore-data-test-tangle-online-chunk_58 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_58: + chunk_59: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2208,10 +2245,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_58 + name: firestore-data-test-tangle-online-chunk_59 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_59: + chunk_60: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2245,10 +2282,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_59 + name: firestore-data-test-tangle-online-chunk_60 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_60: + chunk_61: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2282,10 +2319,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_60 + name: firestore-data-test-tangle-online-chunk_61 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_61: + chunk_62: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2319,10 +2356,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_61 + name: firestore-data-test-tangle-online-chunk_62 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_62: + chunk_63: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2356,10 +2393,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_62 + name: firestore-data-test-tangle-online-chunk_63 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_63: + chunk_64: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2391,6 +2428,6 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_63 + name: firestore-data-test-tangle-online-chunk_64 path: ./packages/functions/firestore-data/ retention-days: 1 diff --git a/.github/workflows/functions_tangle-unit-tests.yml b/.github/workflows/functions_tangle-unit-tests.yml index 2a81abf409..b1608c8fd3 100644 --- a/.github/workflows/functions_tangle-unit-tests.yml +++ b/.github/workflows/functions_tangle-unit-tests.yml @@ -53,8 +53,8 @@ jobs: npm run milestone-sync & firebase emulators:exec " npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/address.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_1.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_2.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/auction-tangle/auction.bit.tangle.spec.ts && + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_1.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -89,9 +89,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_2.spec.ts && npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_3.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_4.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_5.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_4.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -126,9 +126,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_5.spec.ts && npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_6.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_7.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_1.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award-tangle/award-tangle_7.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -163,9 +163,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_1.spec.ts && npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_10.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_2.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_3.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_2.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -200,9 +200,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_3.spec.ts && npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_4.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_5.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_6.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_5.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -237,9 +237,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_6.spec.ts && npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_7.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_8.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_9.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_8.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -274,9 +274,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/award/award_9.spec.ts && npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_1.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_10.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_a.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_10.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -311,9 +311,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_a.spec.ts && npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_b.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_c.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_d.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_c.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -348,9 +348,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_11_d.spec.ts && npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_12.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_13.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_2.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_13.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -385,9 +385,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_2.spec.ts && npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_3.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_4.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_5.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_4.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -422,9 +422,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_5.spec.ts && npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_6.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_7.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_8.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_7.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -459,9 +459,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_8.spec.ts && npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/base-token-trading/base-token-trading_9.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_1.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_10.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_1.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -496,9 +496,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_10.spec.ts && npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_11.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_12.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_2.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_12.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -533,9 +533,9 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" npm run milestone-sync & firebase emulators:exec " - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_4.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_5.spec.ts && - npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_6.spec.ts + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_2.spec.ts && + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_4_a.spec.ts && + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_4_b.spec.ts " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data - name: Archive firestore data uses: actions/upload-artifact@v3 @@ -545,6 +545,43 @@ jobs: path: ./packages/functions/firestore-data/ retention-days: 1 chunk_14: + needs: npm-install + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - uses: actions/cache@v3 + with: + path: | + node_modules + packages/functions/node_modules + packages/interfaces/node_modules + key: ${{ runner.os }}-modules-${{ hashFiles('**/package.json') }} + - name: Init + run: | + npm run build:functions + npm install -g firebase-tools + - name: Test + working-directory: packages/functions + run: | + export GOOGLE_APPLICATION_CREDENTIALS="./test-service-account-key.json" + npm run milestone-sync & + firebase emulators:exec " + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_4_c.spec.ts && + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_5.spec.ts && + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/collection-minting/collection-minting_6.spec.ts + " --project dev --only functions,firestore,storage,ui,auth --export-on-exit=./firestore-data + - name: Archive firestore data + uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: firestore-data-test-tangle-chunk_14 + path: ./packages/functions/firestore-data/ + retention-days: 1 + chunk_15: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -578,10 +615,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_14 + name: firestore-data-test-tangle-chunk_15 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_15: + chunk_16: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -615,10 +652,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_15 + name: firestore-data-test-tangle-chunk_16 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_16: + chunk_17: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -652,10 +689,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_16 + name: firestore-data-test-tangle-chunk_17 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_17: + chunk_18: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -689,10 +726,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_17 + name: firestore-data-test-tangle-chunk_18 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_18: + chunk_19: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -726,10 +763,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_18 + name: firestore-data-test-tangle-chunk_19 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_19: + chunk_20: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -763,10 +800,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_19 + name: firestore-data-test-tangle-chunk_20 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_20: + chunk_21: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -800,10 +837,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_20 + name: firestore-data-test-tangle-chunk_21 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_21: + chunk_22: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -837,10 +874,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_21 + name: firestore-data-test-tangle-chunk_22 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_22: + chunk_23: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -874,10 +911,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_22 + name: firestore-data-test-tangle-chunk_23 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_23: + chunk_24: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -911,10 +948,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_23 + name: firestore-data-test-tangle-chunk_24 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_24: + chunk_25: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -948,10 +985,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_24 + name: firestore-data-test-tangle-chunk_25 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_25: + chunk_26: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -985,10 +1022,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_25 + name: firestore-data-test-tangle-chunk_26 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_26: + chunk_27: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1022,10 +1059,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_26 + name: firestore-data-test-tangle-chunk_27 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_27: + chunk_28: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1059,10 +1096,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_27 + name: firestore-data-test-tangle-chunk_28 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_28: + chunk_29: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1096,10 +1133,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_28 + name: firestore-data-test-tangle-chunk_29 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_29: + chunk_30: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1133,10 +1170,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_29 + name: firestore-data-test-tangle-chunk_30 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_30: + chunk_31: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1170,10 +1207,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_30 + name: firestore-data-test-tangle-chunk_31 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_31: + chunk_32: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1207,10 +1244,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_31 + name: firestore-data-test-tangle-chunk_32 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_32: + chunk_33: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1244,10 +1281,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_32 + name: firestore-data-test-tangle-chunk_33 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_33: + chunk_34: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1281,10 +1318,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_33 + name: firestore-data-test-tangle-chunk_34 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_34: + chunk_35: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1318,10 +1355,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_34 + name: firestore-data-test-tangle-chunk_35 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_35: + chunk_36: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1355,10 +1392,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_35 + name: firestore-data-test-tangle-chunk_36 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_36: + chunk_37: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1392,10 +1429,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_36 + name: firestore-data-test-tangle-chunk_37 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_37: + chunk_38: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1429,10 +1466,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_37 + name: firestore-data-test-tangle-chunk_38 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_38: + chunk_39: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1466,10 +1503,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_38 + name: firestore-data-test-tangle-chunk_39 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_39: + chunk_40: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1503,10 +1540,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_39 + name: firestore-data-test-tangle-chunk_40 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_40: + chunk_41: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1540,10 +1577,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_40 + name: firestore-data-test-tangle-chunk_41 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_41: + chunk_42: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1577,10 +1614,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_41 + name: firestore-data-test-tangle-chunk_42 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_42: + chunk_43: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1614,10 +1651,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_42 + name: firestore-data-test-tangle-chunk_43 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_43: + chunk_44: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1651,10 +1688,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_43 + name: firestore-data-test-tangle-chunk_44 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_44: + chunk_45: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1688,10 +1725,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_44 + name: firestore-data-test-tangle-chunk_45 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_45: + chunk_46: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1725,10 +1762,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_45 + name: firestore-data-test-tangle-chunk_46 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_46: + chunk_47: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1762,10 +1799,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_46 + name: firestore-data-test-tangle-chunk_47 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_47: + chunk_48: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1799,10 +1836,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_47 + name: firestore-data-test-tangle-chunk_48 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_48: + chunk_49: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1836,10 +1873,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_48 + name: firestore-data-test-tangle-chunk_49 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_49: + chunk_50: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1873,10 +1910,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_49 + name: firestore-data-test-tangle-chunk_50 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_50: + chunk_51: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1910,10 +1947,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_50 + name: firestore-data-test-tangle-chunk_51 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_51: + chunk_52: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1947,10 +1984,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_51 + name: firestore-data-test-tangle-chunk_52 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_52: + chunk_53: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1984,10 +2021,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_52 + name: firestore-data-test-tangle-chunk_53 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_53: + chunk_54: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2021,10 +2058,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_53 + name: firestore-data-test-tangle-chunk_54 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_54: + chunk_55: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2058,10 +2095,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_54 + name: firestore-data-test-tangle-chunk_55 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_55: + chunk_56: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2095,10 +2132,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_55 + name: firestore-data-test-tangle-chunk_56 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_56: + chunk_57: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2132,10 +2169,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_56 + name: firestore-data-test-tangle-chunk_57 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_57: + chunk_58: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2169,10 +2206,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_57 + name: firestore-data-test-tangle-chunk_58 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_58: + chunk_59: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2206,10 +2243,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_58 + name: firestore-data-test-tangle-chunk_59 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_59: + chunk_60: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2243,10 +2280,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_59 + name: firestore-data-test-tangle-chunk_60 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_60: + chunk_61: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2280,10 +2317,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_60 + name: firestore-data-test-tangle-chunk_61 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_61: + chunk_62: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2317,10 +2354,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_61 + name: firestore-data-test-tangle-chunk_62 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_62: + chunk_63: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2354,10 +2391,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_62 + name: firestore-data-test-tangle-chunk_63 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_63: + chunk_64: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2389,10 +2426,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_63 + name: firestore-data-test-tangle-chunk_64 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_64: + chunk_65: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2424,10 +2461,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_64 + name: firestore-data-test-tangle-chunk_65 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_65: + chunk_66: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2459,10 +2496,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_65 + name: firestore-data-test-tangle-chunk_66 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_66: + chunk_67: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2494,6 +2531,6 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_66 + name: firestore-data-test-tangle-chunk_67 path: ./packages/functions/firestore-data/ retention-days: 1 diff --git a/packages/api/package.json b/packages/api/package.json index 699082fa45..9bd8f6b126 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -20,18 +20,21 @@ "@build-5/interfaces": "*", "@iota/sdk": "1.1.1", "cors": "^2.8.5", - "dayjs": "^1.11.9", + "dayjs": "1.11.10", "express": "^4.18.2", + "firebase-admin": "^11.11.0", "joi": "17.11.0", + "jsonwebtoken": "9.0.2", "lodash": "^4.17.21", "rxjs": "^7.8.1", "ws": "^8.13.0" }, "devDependencies": { - "@types/cors": "^2.8.14", - "@types/express": "^4.17.17", - "@types/lodash": "^4.14.197", - "@types/ws": "^8.5.5", + "@types/cors": "2.8.14", + "@types/express": "4.17.17", + "@types/lodash": "4.14.200", + "@types/ws": "8.5.5", + "dotenv": "16.3.1", "typescript": "4.9.5" } } diff --git a/packages/database/package.json b/packages/database/package.json index a8fbfff8e6..52b4bd6719 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -16,14 +16,14 @@ }, "dependencies": { "@build-5/interfaces": "*", - "dayjs": "^1.11.9", - "firebase-admin": "^11.10.1", - "jsonwebtoken": "^9.0.2", - "lodash": "^4.17.21" + "dayjs": "1.11.10", + "firebase-admin": "11.11.0", + "jsonwebtoken": "9.0.2", + "lodash": "4.17.21" }, "devDependencies": { - "@types/lodash": "^4.14.197", - "dotenv": "^16.3.1", + "@types/lodash": "4.14.197", + "dotenv": "16.3.1", "glob": "8.0.3", "typescript": "4.9.5" } diff --git a/packages/functions/package.json b/packages/functions/package.json index 4cc860e794..bac6a356d7 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -63,8 +63,8 @@ "typescript": "4.9.5" }, "dependencies": { - "@build-5/interfaces": "*", "@build-5/database": "*", + "@build-5/interfaces": "*", "@ffmpeg-installer/ffmpeg": "1.1.0", "@ffprobe-installer/ffprobe": "2.0.0", "@iota/sdk": "1.1.1", @@ -79,7 +79,7 @@ "crypto-js": "4.1.1", "dayjs": "1.11.7", "ethers": "6.2.3", - "firebase-admin": "11.9.0", + "firebase-admin": "11.11.0", "firebase-functions": "4.4.1", "glob": "8.0.3", "interfaces": "^0.0.3", diff --git a/packages/database/scripts/db.upgrade.ts b/packages/functions/scripts/db.upgrade.ts similarity index 96% rename from packages/database/scripts/db.upgrade.ts rename to packages/functions/scripts/db.upgrade.ts index b2c77f6b15..bd2fff49c6 100644 --- a/packages/database/scripts/db.upgrade.ts +++ b/packages/functions/scripts/db.upgrade.ts @@ -5,7 +5,7 @@ import admin from 'firebase-admin'; import { getFirestore } from 'firebase-admin/firestore'; import fs from 'fs'; import { glob } from 'glob'; -import { FirebaseApp } from '../src/app/app'; +import { FirebaseApp } from '@build-5/database'; import serviceAccount from './serviceAccountKey.json'; dotenv.config({ path: '../.env' }); diff --git a/packages/functions/scripts/dbUpgrades/1.0.0/auction.roll.ts b/packages/functions/scripts/dbUpgrades/1.0.0/auction.roll.ts new file mode 100644 index 0000000000..7773e0cec3 --- /dev/null +++ b/packages/functions/scripts/dbUpgrades/1.0.0/auction.roll.ts @@ -0,0 +1,105 @@ +import { FirebaseApp, Firestore, build5Db } from '@build-5/database'; +import { + Auction, + AuctionType, + COL, + DEFAULT_NETWORK, + EXTEND_AUCTION_WITHIN, + Nft, + SOON_PROJECT_ID, + Transaction, +} from '@build-5/interfaces'; +import { randomBytes } from 'crypto'; +import dayjs from 'dayjs'; +import { Wallet } from 'ethers'; +import { get, head } from 'lodash'; + +export const nftAuctionRoll = async (app: FirebaseApp) => { + const db = new Firestore(app); + + let lastDocId = ''; + + do { + const lastDoc = lastDocId ? await db.doc(`${COL.NFT}/${lastDocId}`).getSnapshot() : undefined; + + const nfts = await db + .collection(COL.NFT) + .where('auctionTo', '>=', dayjs().toDate()) + .startAfter(lastDoc) + .limit(500) + .get(); + + const promises = nfts.map((n) => + build5Db().runTransaction(async (transaction) => { + const nftDocRef = build5Db().doc(`${COL.NFT}/${n.uid}`); + const nft = await transaction.get(nftDocRef); + + if (nft.auction || !nft.auctionTo || dayjs(nft.auctionTo.toDate()).isBefore(dayjs())) { + return; + } + + const auction = await getAuctionData(nft); + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auction.uid}`); + transaction.create(auctionDocRef, auction); + + transaction.update(nftDocRef, { auction: auction.uid }); + }), + ); + + await Promise.all(promises); + } while (lastDocId); +}; + +const getAuctionData = async (nft: Nft) => { + const auction: Auction = { + uid: getRandomEthAddress(), + createdBy: nft.owner, + project: nft.owner || SOON_PROJECT_ID, + projects: nft.projects || { [SOON_PROJECT_ID]: true }, + auctionFrom: nft.auctionFrom!, + auctionTo: nft.auctionTo!, + auctionFloorPrice: nft.auctionFloorPrice || 0, + auctionLength: nft.auctionLength || 0, + + bids: [], + maxBids: 1, + type: AuctionType.NFT, + network: nft.mintingData?.network || DEFAULT_NETWORK, + nftId: nft.uid, + + active: true, + topUpBased: false, + }; + + if (nft.auctionHighestBidder) { + auction.auctionHighestBidder = nft.auctionHighestBidder; + auction.auctionHighestBid = nft.auctionHighestBid || 0; + + const paymentDocRef = build5Db().doc( + `${COL.TRANSACTION}/${get(nft, 'auctionHighestTransaction', '')}`, + ); + const payment = await paymentDocRef.get(); + auction.bids.push({ + bidder: nft.auctionHighestBidder, + amount: nft.auctionHighestBid || 0, + order: head(payment.payload.sourceTransaction) || '', + }); + } + + if (nft.extendedAuctionLength) { + return { + ...auction, + extendedAuctionTo: nft.extendedAuctionTo, + extendedAuctionLength: nft.extendedAuctionLength || 0, + extendAuctionWithin: nft.extendAuctionWithin || EXTEND_AUCTION_WITHIN, + }; + } + return auction; +}; + +const getRandomEthAddress = () => { + const wallet = new Wallet('0x' + randomBytes(32).toString('hex')); + return wallet.address.toLowerCase(); +}; + +export const roll = nftAuctionRoll; diff --git a/packages/database/scripts/dbUpgrades/1.0.0/soonProjects.ts b/packages/functions/scripts/dbUpgrades/1.0.0/soonProjects.ts similarity index 95% rename from packages/database/scripts/dbUpgrades/1.0.0/soonProjects.ts rename to packages/functions/scripts/dbUpgrades/1.0.0/soonProjects.ts index caf21ece6b..1db1a3fec6 100644 --- a/packages/database/scripts/dbUpgrades/1.0.0/soonProjects.ts +++ b/packages/functions/scripts/dbUpgrades/1.0.0/soonProjects.ts @@ -1,3 +1,4 @@ +import { FirebaseApp, Firestore } from '@build-5/database'; import { COL, MIN_IOTA_AMOUNT, @@ -7,8 +8,6 @@ import { } from '@build-5/interfaces'; import dayjs from 'dayjs'; import jwt from 'jsonwebtoken'; -import { FirebaseApp } from '../../../src/app/app'; -import { Firestore } from '../../../src/firestore/firestore'; import serviceAccount from '../../serviceAccountKey.json'; const ADMIN_ID = '0x551fd2c7c7bf356bac194587dab2fcd46420054b'; diff --git a/packages/functions/src/controls/auction/AuctionBidRequestSchema.ts b/packages/functions/src/controls/auction/AuctionBidRequestSchema.ts new file mode 100644 index 0000000000..2d311025bd --- /dev/null +++ b/packages/functions/src/controls/auction/AuctionBidRequestSchema.ts @@ -0,0 +1,10 @@ +import { AuctionBidRequest } from '@build-5/interfaces'; +import { CommonJoi, toJoiObject } from '../../services/joi/common'; + +export const auctionBidSchema = toJoiObject({ + auction: CommonJoi.uid().description('Build5 id of the auction.'), +}) + .description('Request object to create a bid order.') + .meta({ + className: 'AuctionBidRequest', + }); diff --git a/packages/functions/src/controls/auction/AuctionCreateRequestSchema.ts b/packages/functions/src/controls/auction/AuctionCreateRequestSchema.ts new file mode 100644 index 0000000000..3ed5f814e1 --- /dev/null +++ b/packages/functions/src/controls/auction/AuctionCreateRequestSchema.ts @@ -0,0 +1,74 @@ +import { + AuctionCreateRequest, + EXTEND_AUCTION_WITHIN, + MAX_IOTA_AMOUNT, + MIN_IOTA_AMOUNT, + TRANSACTION_AUTO_EXPIRY_MS, + TRANSACTION_MAX_EXPIRY_MS, +} from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import Joi from 'joi'; +import { toJoiObject } from '../../services/joi/common'; +import { AVAILABLE_NETWORKS } from '../common'; + +const minAvailableFrom = 10; +const minBids = 1; +const maxBids = 10; + +export const auctionCreateSchema = { + auctionFrom: Joi.date() + .greater(dayjs().subtract(minAvailableFrom, 'minutes').toDate()) + .required() + .description( + `Starting date of the auction. Can not be sooner then ${minAvailableFrom} minutes.`, + ), + auctionFloorPrice: Joi.number() + .min(MIN_IOTA_AMOUNT) + .max(MAX_IOTA_AMOUNT) + .required() + .description( + `Floor price of the auction. Minimum ${MIN_IOTA_AMOUNT}, maximum ${MAX_IOTA_AMOUNT}`, + ), + auctionLength: Joi.number() + .min(TRANSACTION_AUTO_EXPIRY_MS) + .max(TRANSACTION_MAX_EXPIRY_MS) + .required() + .description( + `Millisecond value of the auction length. Minimum ${TRANSACTION_AUTO_EXPIRY_MS}, maximum ${TRANSACTION_MAX_EXPIRY_MS}`, + ), + extendedAuctionLength: Joi.number() + .min(TRANSACTION_AUTO_EXPIRY_MS) + .max(TRANSACTION_MAX_EXPIRY_MS) + .greater(Joi.ref('auctionLength')) + .description( + 'If set, auction will automatically extended by this length if a bid comes in within {@link extendAuctionWithin} before the end of the auction.', + ), + extendAuctionWithin: Joi.number() + .min(TRANSACTION_AUTO_EXPIRY_MS) + .max(TRANSACTION_MAX_EXPIRY_MS) + .description( + 'Auction will be extended if a bid happens this many milliseconds before auction ends. ' + + `Default value is ${EXTEND_AUCTION_WITHIN} minutes`, + ), + maxBids: Joi.number() + .integer() + .min(minBids) + .max(maxBids) + .required() + .description( + `Specifies the maximum number of active bids. Minimum ${minBids}, maximum ${maxBids}`, + ), + network: Joi.string() + .valid(...AVAILABLE_NETWORKS) + .description('Network on which this auction accepts bids.') + .required(), + topUpBased: Joi.boolean().description( + 'If set to true, consequent bids from the same user will be treated as topups', + ), +}; + +export const auctionCreateSchemaObject = toJoiObject(auctionCreateSchema) + .description('Request object to create an auction.') + .meta({ + className: 'AuctionCreateRequest', + }); diff --git a/packages/functions/src/controls/auction/auction.control.ts b/packages/functions/src/controls/auction/auction.control.ts new file mode 100644 index 0000000000..ef523e6992 --- /dev/null +++ b/packages/functions/src/controls/auction/auction.control.ts @@ -0,0 +1,25 @@ +import { build5Db } from '@build-5/database'; +import { AuctionBidRequest, COL, Transaction, WenError } from '@build-5/interfaces'; +import { createBidOrder } from '../../services/payment/tangle-service/auction/auction.bid.order'; +import { invalidArgument } from '../../utils/error.utils'; +import { Context } from '../common'; + +export const auctionBidControl = async ({ + ip, + owner, + params, + project, +}: Context): Promise => { + const memberDocRef = build5Db().doc(`${COL.MEMBER}/${owner}`); + const member = await memberDocRef.get(); + if (!member) { + throw invalidArgument(WenError.member_does_not_exists); + } + + const bidTransaction = await createBidOrder(project, owner, params.auction, ip); + + const transactionDocRef = build5Db().doc(`${COL.TRANSACTION}/${bidTransaction.uid}`); + await transactionDocRef.create(bidTransaction); + + return (await transactionDocRef.get())!; +}; diff --git a/packages/functions/src/controls/auction/auction.create.control.ts b/packages/functions/src/controls/auction/auction.create.control.ts new file mode 100644 index 0000000000..20c4a21597 --- /dev/null +++ b/packages/functions/src/controls/auction/auction.create.control.ts @@ -0,0 +1,23 @@ +import { build5Db } from '@build-5/database'; +import { Auction, AuctionCreateRequest, COL, Member, WenError } from '@build-5/interfaces'; +import { getAuctionData } from '../../services/payment/tangle-service/auction/auction.create.service'; +import { invalidArgument } from '../../utils/error.utils'; +import { Context } from '../common'; + +export const auctionCreateControl = async ({ + owner, + project, + params, +}: Context) => { + const memberDocRef = build5Db().doc(`${COL.MEMBER}/${owner}`); + const member = await memberDocRef.get(); + if (!member) { + throw invalidArgument(WenError.member_does_not_exists); + } + + const auction = getAuctionData(project, owner, params); + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auction.uid}`); + await auctionDocRef.create(auction); + + return await auctionDocRef.get(); +}; diff --git a/packages/functions/src/controls/award/AwardCreateRequestSchema.ts b/packages/functions/src/controls/award/AwardCreateRequestSchema.ts index 69858cba9a..a00f1fe6d3 100644 --- a/packages/functions/src/controls/award/AwardCreateRequestSchema.ts +++ b/packages/functions/src/controls/award/AwardCreateRequestSchema.ts @@ -29,7 +29,7 @@ export const awardBageSchema = { .min(0) .integer() .required() - .description('The time for wich the reward nft will be locked.'), + .description('The time for which the reward nft will be locked.'), }; export const awardBageSchemaObject = toJoiObject(awardBageSchema) @@ -47,7 +47,7 @@ export const awardCreateSchema = { network: Joi.string() .equal(...AVAILABLE_NETWORKS) .required() - .description('Network on wich the award will be minted and issued'), + .description('Network on which the award will be minted and issued'), }; export const awardCreateSchemaObject = toJoiObject(awardCreateSchema) diff --git a/packages/functions/src/controls/nft/NftDepositRequestSchema.ts b/packages/functions/src/controls/nft/NftDepositRequestSchema.ts index 54cf007418..975215312d 100644 --- a/packages/functions/src/controls/nft/NftDepositRequestSchema.ts +++ b/packages/functions/src/controls/nft/NftDepositRequestSchema.ts @@ -9,7 +9,7 @@ export const depositNftSchema = toJoiObject({ network: Joi.string() .equal(...availaibleNetworks) .required() - .description('Network on wich the nft was minted.'), + .description('Network on which the nft was minted.'), }) .description('Request object to create an NFT deposit order') .meta({ diff --git a/packages/functions/src/controls/nft/nft.bid.control.ts b/packages/functions/src/controls/nft/nft.bid.control.ts index 2eb67afd87..63484ab7ac 100644 --- a/packages/functions/src/controls/nft/nft.bid.control.ts +++ b/packages/functions/src/controls/nft/nft.bid.control.ts @@ -1,6 +1,6 @@ import { build5Db } from '@build-5/database'; -import { COL, NftBidRequest, Transaction, WenError } from '@build-5/interfaces'; -import { createNftBidOrder } from '../../services/payment/tangle-service/nft/nft-bid.service'; +import { COL, Nft, NftBidRequest, Transaction, WenError } from '@build-5/interfaces'; +import { createBidOrder } from '../../services/payment/tangle-service/auction/auction.bid.order'; import { invalidArgument } from '../../utils/error.utils'; import { Context } from '../common'; @@ -16,7 +16,10 @@ export const nftBidControl = async ({ throw invalidArgument(WenError.member_does_not_exists); } - const bidTransaction = await createNftBidOrder(project, params.nft, owner, ip || ''); + const nftDocRef = build5Db().doc(`${COL.NFT}/${params.nft}`); + const nft = await nftDocRef.get(); + + const bidTransaction = await createBidOrder(project, owner, nft.auction || '', ip); const transactionDocRef = build5Db().doc(`${COL.TRANSACTION}/${bidTransaction.uid}`); await transactionDocRef.create(bidTransaction); diff --git a/packages/functions/src/controls/nft/nft.set.for.sale.ts b/packages/functions/src/controls/nft/nft.set.for.sale.ts index 2732bd662c..8a6f43114c 100644 --- a/packages/functions/src/controls/nft/nft.set.for.sale.ts +++ b/packages/functions/src/controls/nft/nft.set.for.sale.ts @@ -7,6 +7,7 @@ import { Context } from '../common'; export const setForSaleNftControl = async ({ owner, params, + project, }: Context): Promise => { const memberDocRef = build5Db().doc(`${COL.MEMBER}/${owner}`); const member = await memberDocRef.get(); @@ -14,9 +15,19 @@ export const setForSaleNftControl = async ({ throw invalidArgument(WenError.member_does_not_exists); } - const updateData = await getNftSetForSaleParams(params, member); + const { nft, auction } = await getNftSetForSaleParams(member, project, params); + + const batch = build5Db().batch(); + const nftDocRef = build5Db().doc(`${COL.NFT}/${params.nft}`); - await nftDocRef.update(updateData); + batch.update(nftDocRef, nft); + + if (auction) { + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auction.uid}`); + batch.create(auctionDocRef, auction); + } + + await batch.commit(); return (await nftDocRef.get())!; }; diff --git a/packages/functions/src/cron/auction.cron.ts b/packages/functions/src/cron/auction.cron.ts new file mode 100644 index 0000000000..7a7f80528a --- /dev/null +++ b/packages/functions/src/cron/auction.cron.ts @@ -0,0 +1,27 @@ +import { build5Db } from '@build-5/database'; +import { Auction, AuctionType, COL } from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { AuctionFinalizeService } from '../services/payment/auction/auction.finalize.service'; +import { TransactionService } from '../services/payment/transaction-service'; + +export const finalizeAuctions = async () => { + const snap = await build5Db() + .collection(COL.AUCTION) + .where('auctionTo', '<=', dayjs().toDate()) + .where('active', '==', true) + .get(); + const promises = snap.map(async (a) => { + if (a.type === AuctionType.NFT) { + await finalizeNftAuction(a.uid); + } + }); + await Promise.all(promises); +}; + +const finalizeNftAuction = (auction: string) => + build5Db().runTransaction(async (transaction) => { + const tranService = new TransactionService(transaction); + const service = new AuctionFinalizeService(tranService); + await service.markAsFinalized(auction); + tranService.submit(); + }); diff --git a/packages/functions/src/cron/nft.cron.ts b/packages/functions/src/cron/nft.cron.ts index ae4b99edd6..5b105c05ff 100644 --- a/packages/functions/src/cron/nft.cron.ts +++ b/packages/functions/src/cron/nft.cron.ts @@ -1,28 +1,6 @@ import { build5Db } from '@build-5/database'; import { COL, Nft } from '@build-5/interfaces'; import dayjs from 'dayjs'; -import { NftBidService } from '../services/payment/nft/nft-bid.service'; -import { TransactionService } from '../services/payment/transaction-service'; - -const finalizeNftAuction = (nftId: string) => - build5Db().runTransaction(async (transaction) => { - const nftDocRef = build5Db().collection(COL.NFT).doc(nftId); - const nft = (await transaction.get(nftDocRef))!; - - const tranService = new TransactionService(transaction); - const service = new NftBidService(tranService); - await service.markNftAsFinalized(nft); - tranService.submit(); - }); - -export const finalizeAllNftAuctions = async () => { - const snap = await build5Db() - .collection(COL.NFT) - .where('auctionTo', '<=', dayjs().toDate()) - .get(); - const promises = snap.map((d) => finalizeNftAuction(d.uid)); - await Promise.all(promises); -}; export const hidePlaceholderAfterSoldOutCron = async () => { const snap = await build5Db() diff --git a/packages/functions/src/runtime/common.ts b/packages/functions/src/runtime/common.ts index cc5e0cfece..e3743065fe 100644 --- a/packages/functions/src/runtime/common.ts +++ b/packages/functions/src/runtime/common.ts @@ -23,7 +23,7 @@ export enum WEN_SCHEDULED { retryWallet = 'retrywallet', processExpiredAwards = 'processexpiredawards', voidExpiredOrders = 'voidexpiredorders', - finalizeAuctionNft = 'finalizeauctionnft', + finalizeAuctions = 'finalizeauctions', hidePlaceholderAfterSoldOut = 'hideplaceholderaftersoldout', tokenCoolDownOver = 'tokencooldownover', cancelExpiredSale = 'cancelexpiredsale', diff --git a/packages/functions/src/runtime/cron/index.ts b/packages/functions/src/runtime/cron/index.ts index 499d554392..a6f2bd6247 100644 --- a/packages/functions/src/runtime/cron/index.ts +++ b/packages/functions/src/runtime/cron/index.ts @@ -1,8 +1,9 @@ +import { finalizeAuctions } from '../../cron/auction.cron'; import { processExpiredAwards } from '../../cron/award.cron'; import { getLatestBitfinexPricesCron } from '../../cron/bitfinex.cron'; import { updateFloorPriceOnCollections } from '../../cron/collection.floor.price.cron'; import { uploadMediaToWeb3 } from '../../cron/media.cron'; -import { finalizeAllNftAuctions, hidePlaceholderAfterSoldOutCron } from '../../cron/nft.cron'; +import { hidePlaceholderAfterSoldOutCron } from '../../cron/nft.cron'; import { voidExpiredOrdersCron } from '../../cron/orders.cron'; import { markExpiredProposalCompleted } from '../../cron/proposal.cron'; import { removeExpiredStakesFromSpace } from '../../cron/stake.cron'; @@ -29,9 +30,9 @@ exports[WEN_SCHEDULED.voidExpiredOrders] = onSchedule({ handler: voidExpiredOrdersCron, }); -exports[WEN_SCHEDULED.finalizeAuctionNft] = onSchedule({ +exports[WEN_SCHEDULED.finalizeAuctions] = onSchedule({ schedule: 'every 1 minutes', - handler: finalizeAllNftAuctions, + handler: finalizeAuctions, }); exports[WEN_SCHEDULED.hidePlaceholderAfterSoldOut] = onSchedule({ diff --git a/packages/functions/src/runtime/firebase/auction/index.ts b/packages/functions/src/runtime/firebase/auction/index.ts new file mode 100644 index 0000000000..22975354e3 --- /dev/null +++ b/packages/functions/src/runtime/firebase/auction/index.ts @@ -0,0 +1,6 @@ +import { WEN_FUNC } from '@build-5/interfaces'; +import { https } from '../../..'; + +export const auctionCreate = https[WEN_FUNC.createauction]; + +export const bidAuction = https[WEN_FUNC.bidAuction]; diff --git a/packages/functions/src/runtime/https/index.ts b/packages/functions/src/runtime/https/index.ts index 275f880db1..a0b78dc8df 100644 --- a/packages/functions/src/runtime/https/index.ts +++ b/packages/functions/src/runtime/https/index.ts @@ -2,6 +2,10 @@ import { NftCreateRequest, ProposalType, WEN_FUNC } from '@build-5/interfaces'; import Joi from 'joi'; import { validateAddressSchemaObject } from '../../controls/address/AddressValidationRequestSchema'; import { validateAddressControl } from '../../controls/address/address.control'; +import { auctionBidSchema } from '../../controls/auction/AuctionBidRequestSchema'; +import { auctionCreateSchemaObject } from '../../controls/auction/AuctionCreateRequestSchema'; +import { auctionBidControl } from '../../controls/auction/auction.control'; +import { auctionCreateControl } from '../../controls/auction/auction.create.control'; import { customTokenSchema } from '../../controls/auth/CutomTokenRequestSchema'; import { generateCustomTokenControl } from '../../controls/auth/auth.control'; import { approveAwardParticipantSchemaObject } from '../../controls/award/AwardApproveParticipantRequestSchema'; @@ -522,3 +526,15 @@ exports[WEN_FUNC.deactivateProject] = onRequest({ schema: toJoiObject({}), handler: deactivateProjectControl, }); + +exports[WEN_FUNC.bidAuction] = onRequest({ + name: WEN_FUNC.bidAuction, + schema: auctionBidSchema, + handler: auctionBidControl, +}); + +exports[WEN_FUNC.createauction] = onRequest({ + name: WEN_FUNC.createauction, + schema: auctionCreateSchemaObject, + handler: auctionCreateControl, +}); diff --git a/packages/functions/src/services/notification/notification.ts b/packages/functions/src/services/notification/notification.ts index 345f550c6c..a02b92236e 100644 --- a/packages/functions/src/services/notification/notification.ts +++ b/packages/functions/src/services/notification/notification.ts @@ -1,62 +1,49 @@ -import { Member, Nft, Notification, NotificationType, Transaction } from '@build-5/interfaces'; +import { Member, Notification, NotificationType } from '@build-5/interfaces'; import { serverTime } from '../../utils/dateTime.utils'; import { getRandomEthAddress } from '../../utils/wallet.utils'; export class NotificationService { - public static prepareBid(member: Member, nft: Nft, tran: Transaction): Notification { + public static prepareBid(member: Member, amount: number, auction: string): Notification { return { uid: getRandomEthAddress(), type: NotificationType.NEW_BID, - member: nft.owner, + member: member.uid, params: { - amount: tran.payload.amount, + amount: amount, member: { name: member.name || member.uid, }, - nft: { - uid: nft.uid, - name: nft.name || nft.uid, - }, + auction, }, createdOn: serverTime(), }; } - public static prepareWinBid(member: Member, nft: Nft, tran: Transaction): Notification { + public static prepareWinBid(member: Member, amount: number, auction: string): Notification { return { uid: getRandomEthAddress(), type: NotificationType.WIN_BID, member: member.uid, params: { - amount: tran.payload.amount, + amount: amount, member: { name: member.name || member.uid, }, - nft: { - uid: nft.uid, - name: nft.name || nft.uid, - }, + auction, }, createdOn: serverTime(), }; } - public static prepareLostBid(member: Member, nft: Nft, tran: Transaction): Notification { - return { + public static prepareLostBid(member: Member, amount: number, auction: string): Notification { + const notification = { uid: getRandomEthAddress(), type: NotificationType.LOST_BID, member: member.uid, - params: { - amount: tran.payload.amount, - member: { - name: member.name || member.uid, - }, - nft: { - uid: nft.uid, - name: nft.name || nft.uid, - }, - }, + params: { amount, member: { name: member.name || member.uid }, auction }, createdOn: serverTime(), }; + + return notification; } } diff --git a/packages/functions/src/services/payment/auction/auction-bid.service.ts b/packages/functions/src/services/payment/auction/auction-bid.service.ts new file mode 100644 index 0000000000..97b5814bdd --- /dev/null +++ b/packages/functions/src/services/payment/auction/auction-bid.service.ts @@ -0,0 +1,189 @@ +import { build5Db } from '@build-5/database'; +import { + Auction, + AuctionBid, + AuctionType, + COL, + Member, + Transaction, + TransactionPayloadType, + TransactionType, +} from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { head, last, set } from 'lodash'; +import { NotificationService } from '../../notification/notification'; +import { HandlerParams } from '../base'; +import { TransactionService } from '../transaction-service'; + +export class AuctionBidService { + constructor(readonly transactionService: TransactionService) {} + + public handleRequest = async ({ + order, + match, + tran, + tranEntry, + build5Tran, + owner, + }: HandlerParams) => { + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${order.payload.auction!}`); + const auction = await this.transactionService.get(auctionDocRef); + + if (!auction.active) { + await this.transactionService.processAsInvalid(tran, order, tranEntry, build5Tran); + return; + } + + this.transactionService.markAsReconciled(order, match.msgId); + const payment = await this.transactionService.createPayment(order, match); + await this.addNewBid(owner, auction, order, payment); + }; + + private addNewBid = async ( + owner: string, + auction: Auction, + order: Transaction, + payment: Transaction, + ): Promise => { + if (paidAmountIsBelowFloor(payment, auction) || newPaymentTooLow(payment, auction)) { + await this.creditAsInvalidPayment(payment); + return; + } + + const { bids, invalidBid } = placeBid(auction, order.uid, owner, payment.payload.amount!); + + const auctionUpdateData = this.getAuctionUpdateData(auction, bids); + + if (invalidBid) { + await this.creditInvalidPayments(auction, invalidBid); + } + + if (auctionUpdateData.auctionHighestBid !== auction.auctionHighestBid) { + await this.onAuctionHighestBidChange(order, auctionUpdateData); + } + + if (auction.type === AuctionType.NFT) { + this.updateNft(auctionUpdateData); + } + }; + + private creditInvalidPayments = async (auction: Auction, invalidBid: AuctionBid) => { + const payments = await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.PAYMENT) + .where('member', '==', invalidBid.bidder) + .where('payload.invalidPayment', '==', false) + .where('payload.auction', '==', auction.uid) + .get(); + for (const payment of payments) { + await this.creditAsInvalidPayment(payment); + } + + const invalidBidderDocRef = build5Db().doc(`${COL.MEMBER}/${invalidBid.bidder}`); + const invalidBidder = await this.transactionService.get(invalidBidderDocRef); + + const notification = NotificationService.prepareLostBid( + invalidBidder, + invalidBid.amount, + auction.uid, + ); + const notificationDocRef = build5Db().doc(`${COL.NOTIFICATION}/${notification.uid}`); + this.transactionService.push({ ref: notificationDocRef, data: notification, action: 'set' }); + }; + + private creditAsInvalidPayment = async (payment: Transaction) => { + const paymentDocRef = build5Db().doc(`${COL.TRANSACTION}/${payment.uid}`); + this.transactionService.push({ + ref: paymentDocRef, + data: { 'payload.invalidPayment': true }, + action: 'update', + }); + const paymentPayload = payment.payload; + await this.transactionService.createCredit(TransactionPayloadType.INVALID_PAYMENT, payment, { + msgId: paymentPayload.chainReference!, + to: { + address: paymentPayload.targetAddress!, + amount: paymentPayload.amount!, + }, + from: paymentPayload.sourceAddress!, + }); + return; + }; + + private onAuctionHighestBidChange = async (order: Transaction, auction: Auction) => { + const memberDocRef = build5Db().doc(`${COL.MEMBER}/${order.member!}`); + const member = await this.transactionService.get(memberDocRef); + const bidNotification = NotificationService.prepareBid( + member, + auction.auctionHighestBid!, + auction.uid, + ); + const notificationDocRef = build5Db().doc(`${COL.NOTIFICATION}/${bidNotification.uid}`); + this.transactionService.push({ ref: notificationDocRef, data: bidNotification, action: 'set' }); + }; + + private getAuctionUpdateData = (auction: Auction, bids: AuctionBid[]) => { + const auctionUpdateData = { + ...auction, + bids, + auctionHighestBidder: head(bids)?.bidder || '', + auctionHighestBid: head(bids)?.amount || 0, + }; + const auctionTTL = dayjs(auction.auctionTo!.toDate()).diff(dayjs()); + if ( + auction.auctionLength < (auction.extendedAuctionLength || 0) && + auctionTTL < (auction.extendAuctionWithin || 0) + ) { + set(auctionUpdateData, 'auctionTo', auction.extendedAuctionTo || null); + set(auctionUpdateData, 'auctionLength', auction.extendedAuctionLength || null); + } + + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auction.uid}`); + this.transactionService.push({ ref: auctionDocRef, data: auctionUpdateData, action: 'update' }); + return auctionUpdateData as Auction; + }; + + private updateNft = (auction: Auction) => { + const nftUpdateData = { + auctionTo: auction.auctionTo, + auctionLength: auction.auctionLength, + auctionHighestBid: auction.auctionHighestBid, + auctionHighestBidder: auction.auctionHighestBidder, + }; + this.transactionService.push({ + ref: build5Db().doc(`${COL.NFT}/${auction.nftId}`), + data: nftUpdateData, + action: 'update', + }); + }; +} + +const placeBid = (auction: Auction, order: string, bidder: string, amount: number) => { + const bids = [...auction.bids]; + const currentBid = bids.find((b) => b.bidder === bidder); + + if (currentBid) { + if (auction.topUpBased) { + currentBid.amount += amount; + bids.sort((a, b) => b.amount - a.amount); + } else { + currentBid.amount = Math.max(currentBid.amount, amount); + bids.sort((a, b) => b.amount - a.amount); + bids.push({ bidder, amount: Math.min(currentBid.amount, amount), order }); + } + } else { + bids.push({ bidder, amount, order }); + bids.sort((a, b) => b.amount - a.amount); + } + + return { + bids: bids.slice(0, auction.maxBids), + invalidBid: head(bids.slice(auction.maxBids)), + }; +}; + +const paidAmountIsBelowFloor = (payment: Transaction, auction: Auction) => + payment.payload.amount! < auction.auctionFloorPrice; + +const newPaymentTooLow = (payment: Transaction, auction: Auction) => + !auction.topUpBased && (last(auction.bids)?.amount || 0) > payment.payload.amount!; diff --git a/packages/functions/src/services/payment/auction/auction.finalize.service.ts b/packages/functions/src/services/payment/auction/auction.finalize.service.ts new file mode 100644 index 0000000000..f5aba724e7 --- /dev/null +++ b/packages/functions/src/services/payment/auction/auction.finalize.service.ts @@ -0,0 +1,101 @@ +import { build5Db } from '@build-5/database'; +import { + Auction, + AuctionType, + COL, + Member, + Nft, + NftStatus, + Transaction, + TransactionType, + WenError, +} from '@build-5/interfaces'; +import { invalidArgument } from '../../../utils/error.utils'; +import { NotificationService } from '../../notification/notification'; +import { BaseNftService } from '../nft/common'; +import { TransactionService } from '../transaction-service'; + +export class AuctionFinalizeService { + constructor(readonly transactionService: TransactionService) {} + + public markAsFinalized = async (auctionId: string) => { + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auctionId}`); + const auction = await this.transactionService.get(auctionDocRef); + if (!auction.active) { + throw invalidArgument(WenError.auction_not_active); + } + + this.transactionService.push({ ref: auctionDocRef, data: { active: false }, action: 'update' }); + + const payments = await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.PAYMENT) + .where('payload.invalidPayment', '==', false) + .where('payload.auction', '==', auction.uid) + .get(); + for (const payment of payments) { + const orderDocRef = build5Db().doc( + `${COL.TRANSACTION}/${payment.payload.sourceTransaction![0]}`, + ); + const order = await orderDocRef.get(); + this.transactionService.createBillPayment(order, payment); + } + + switch (auction.type) { + case AuctionType.NFT: + await this.finalizeNftAuction(auction); + } + }; + + private finalizeNftAuction = async (auction: Auction) => { + const nftDocRef = build5Db().doc(`${COL.NFT}/${auction.nftId}`); + const nft = await this.transactionService.get(nftDocRef); + + if (!auction.auctionHighestBidder) { + this.transactionService.push({ + ref: nftDocRef, + data: { + auctionFrom: null, + auctionTo: null, + extendedAuctionTo: null, + auctionFloorPrice: null, + auctionLength: null, + extendedAuctionLength: null, + auctionHighestBid: null, + auctionHighestBidder: null, + auction: null, + }, + action: 'update', + }); + } + + const orderDocRef = build5Db().doc(`${COL.TRANSACTION}/${auction.bids[0].order}`); + const order = await orderDocRef.get(); + + const nftService = new BaseNftService(this.transactionService); + await nftService.setNftOwner(order, auction.auctionHighestBid!); + + const memberDocRef = build5Db().collection(COL.MEMBER).doc(order!.member!); + const member = await memberDocRef.get(); + + const notification = NotificationService.prepareWinBid( + member, + auction.auctionHighestBid!, + auction.uid, + ); + const notificationDocRef = build5Db().doc(`${COL.NOTIFICATION}/${notification.uid}`); + this.transactionService.push({ + ref: notificationDocRef, + data: notification, + action: 'set', + }); + + nftService.setTradingStats(nft); + + const tanglePuchase = order.payload.tanglePuchase; + const disableWithdraw = order.payload.disableWithdraw; + if (!disableWithdraw && tanglePuchase && nft.status === NftStatus.MINTED) { + await nftService.withdrawNft(order, nft); + } + }; +} diff --git a/packages/functions/src/services/payment/nft/common.ts b/packages/functions/src/services/payment/nft/common.ts index 87014edbc8..ae1544172c 100644 --- a/packages/functions/src/services/payment/nft/common.ts +++ b/packages/functions/src/services/payment/nft/common.ts @@ -1,24 +1,15 @@ import { build5Db } from '@build-5/database'; -import { - COL, - Collection, - Member, - Nft, - NftAccess, - Transaction, - TransactionPayloadType, -} from '@build-5/interfaces'; -import dayjs from 'dayjs'; -import { last, set } from 'lodash'; +import { COL, Collection, Member, Nft, NftAccess, Transaction } from '@build-5/interfaces'; import { getAddress } from '../../../utils/address.utils'; import { getProject } from '../../../utils/common.utils'; -import { dateToTimestamp, serverTime } from '../../../utils/dateTime.utils'; -import { NotificationService } from '../../notification/notification'; -import { BaseService } from '../base'; +import { serverTime } from '../../../utils/dateTime.utils'; import { createNftWithdrawOrder } from '../tangle-service/nft/nft-purchase.service'; +import { TransactionService } from '../transaction-service'; -export abstract class BaseNftService extends BaseService { - protected setTradingStats = (nft: Nft) => { +export class BaseNftService { + constructor(private readonly transactionService: TransactionService) {} + + public setTradingStats = (nft: Nft) => { const data = { lastTradedOn: serverTime(), totalTrades: build5Db().inc(1) }; const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${nft.collection}`); this.transactionService.push({ ref: collectionDocRef, data, action: 'update' }); @@ -27,7 +18,7 @@ export abstract class BaseNftService extends BaseService { this.transactionService.push({ ref: nftDocRef, data, action: 'update' }); }; - protected withdrawNft = async (order: Transaction, nft: Nft) => { + public withdrawNft = async (order: Transaction, nft: Nft) => { const membderDocRef = build5Db().doc(`${COL.MEMBER}/${order.member}`); const member = await membderDocRef.get(); const { order: withdrawOrder, nftUpdateData } = createNftWithdrawOrder( @@ -48,14 +39,14 @@ export abstract class BaseNftService extends BaseService { }); }; - protected async setNftOwner(order: Transaction, payment: Transaction): Promise { - const nftDocRef = build5Db().collection(COL.NFT).doc(payment.payload.nft!); + public setNftOwner = async (order: Transaction, amount: number) => { + const nftDocRef = build5Db().collection(COL.NFT).doc(order.payload.nft!); const nft = await this.transactionService.get(nftDocRef); const nftUpdateData = { - owner: payment.member, + owner: order.member, isOwned: true, - price: nft.saleAccess === NftAccess.MEMBERS ? nft.price : payment.payload.amount, + price: nft.saleAccess === NftAccess.MEMBERS ? nft.price : amount, sold: true, locked: false, lockedBy: null, @@ -71,9 +62,9 @@ export abstract class BaseNftService extends BaseService { extendedAuctionLength: null, auctionHighestBid: null, auctionHighestBidder: null, - auctionHighestTransaction: null, saleAccess: null, saleAccessMembers: [], + auction: null, }; this.transactionService.push({ ref: nftDocRef, @@ -81,51 +72,8 @@ export abstract class BaseNftService extends BaseService { action: 'update', }); - if ( - nft.auctionHighestTransaction && - order.payload.type === TransactionPayloadType.NFT_PURCHASE - ) { - const highestTranDocRef = build5Db().doc( - `${COL.TRANSACTION}/${nft.auctionHighestTransaction}`, - ); - const highestPay = (await highestTranDocRef.get())!; - this.transactionService.push({ - ref: highestTranDocRef, - data: { invalidPayment: true }, - action: 'update', - }); - - const sameOwner = highestPay.member === order.member; - const credit = await this.transactionService.createCredit( - TransactionPayloadType.NONE, - highestPay, - { - msgId: highestPay.payload.chainReference || '', - to: { - address: highestPay.payload.targetAddress!, - amount: highestPay.payload.amount!, - }, - from: highestPay.payload.sourceAddress!, - }, - serverTime(), - sameOwner, - ); - - if (!sameOwner) { - const orderId = Array.isArray(highestPay.payload.sourceTransaction) - ? last(highestPay.payload.sourceTransaction)! - : highestPay.payload.sourceTransaction!; - const orderDocRef = build5Db().doc(`${COL.TRANSACTION}/${orderId}`); - this.transactionService.push({ - ref: orderDocRef, - data: { linkedTransactions: build5Db().arrayUnion(credit?.uid) }, - action: 'update', - }); - } - } - if (order.payload.beneficiary === 'space') { - const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${payment.payload.collection}`); + const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${order.payload.collection}`); this.transactionService.push({ ref: collectionDocRef, data: { sold: build5Db().inc(1) }, @@ -149,143 +97,5 @@ export abstract class BaseNftService extends BaseService { }); } } - } - - protected async addNewBid(transaction: Transaction, payment: Transaction): Promise { - const nftDocRef = build5Db().collection(COL.NFT).doc(transaction.payload.nft!); - const paymentDocRef = build5Db().doc(`${COL.TRANSACTION}/${payment.uid}`); - const nft = await this.transactionService.get(nftDocRef); - let newValidPayment = false; - let previousHighestPay: Transaction | undefined; - const paymentPayload = payment.payload; - if (nft?.auctionHighestTransaction) { - const previousHighestPayRef = build5Db().doc( - `${COL.TRANSACTION}/${nft?.auctionHighestTransaction}`, - ); - previousHighestPay = (await this.transactionService.get(previousHighestPayRef))!; - - if ( - previousHighestPay!.payload.amount! < paymentPayload.amount! && - paymentPayload.amount! >= (nft?.auctionFloorPrice || 0) - ) { - newValidPayment = true; - } - } else { - if (paymentPayload.amount! >= (nft?.auctionFloorPrice || 0)) { - newValidPayment = true; - } - } - - // We need to credit the old payment. - if (newValidPayment && previousHighestPay) { - const refPrevPayment = build5Db().doc(`${COL.TRANSACTION}/${previousHighestPay.uid}`); - previousHighestPay.payload.invalidPayment = true; - this.transactionService.push({ - ref: refPrevPayment, - data: previousHighestPay, - action: 'update', - }); - - // Mark as invalid and create credit. - const sameOwner = previousHighestPay.member === transaction.member; - const credit = await this.transactionService.createCredit( - TransactionPayloadType.DATA_NO_LONGER_VALID, - previousHighestPay, - { - msgId: previousHighestPay.payload.chainReference!, - to: { - address: previousHighestPay.payload.targetAddress!, - amount: previousHighestPay.payload.amount!, - }, - from: previousHighestPay.payload.sourceAddress!, - }, - dateToTimestamp(dayjs(payment.createdOn?.toDate()).subtract(1, 's')), - sameOwner, - ); - - // We have to set link on the past order. - if (!sameOwner) { - const sourcTran: string = Array.isArray(previousHighestPay.payload.sourceTransaction) - ? last(previousHighestPay.payload.sourceTransaction)! - : previousHighestPay.payload.sourceTransaction!; - const refHighTranOrderDocRef = build5Db().doc(`${COL.TRANSACTION}/${sourcTran}`); - const refHighTranOrder = await this.transactionService.get( - refHighTranOrderDocRef, - ); - if (refHighTranOrder) { - this.transactionService.push({ - ref: refHighTranOrderDocRef, - data: { - linkedTransactions: [ - ...(refHighTranOrder?.linkedTransactions || []), - ...[credit?.uid], - ], - }, - action: 'update', - }); - - // Notify them. - const refMember = build5Db().collection(COL.MEMBER).doc(refHighTranOrder?.member!); - const sfDocMember = await this.transactionService.get(refMember); - const bidNotification = NotificationService.prepareLostBid( - sfDocMember!, - nft!, - previousHighestPay, - ); - const refNotification = build5Db().collection(COL.NOTIFICATION).doc(bidNotification.uid); - this.transactionService.push({ - ref: refNotification, - data: bidNotification, - action: 'set', - }); - } - } - } - - // Update NFT with highest bid. - if (newValidPayment) { - const nftUpdateData = { - auctionHighestBid: payment.payload.amount, - auctionHighestBidder: payment.member, - auctionHighestTransaction: payment.uid, - }; - const auctionTTL = dayjs(nft?.auctionTo!.toDate()).diff(dayjs()); - if ( - (nft?.auctionLength || 0) < (nft?.extendedAuctionLength || 0) && - auctionTTL < (nft?.extendAuctionWithin || 0) - ) { - set(nftUpdateData, 'auctionTo', nft?.extendedAuctionTo || null); - set(nftUpdateData, 'auctionLength', nft?.extendedAuctionLength || null); - } - this.transactionService.push({ - ref: nftDocRef, - data: nftUpdateData, - action: 'update', - }); - - const refMember = build5Db().collection(COL.MEMBER).doc(transaction.member!); - const sfDocMember = await this.transactionService.get(refMember); - const bidNotification = NotificationService.prepareBid(sfDocMember!, nft!, payment); - const refNotification = build5Db().collection(COL.NOTIFICATION).doc(bidNotification.uid); - this.transactionService.push({ - ref: refNotification, - data: bidNotification, - action: 'set', - }); - } else { - // Invalidate payment. - paymentPayload.invalidPayment = true; - this.transactionService.push({ ref: paymentDocRef, data: payment, action: 'update' }); - - // No valid payment so we credit anyways. - await this.transactionService.createCredit(TransactionPayloadType.INVALID_PAYMENT, payment, { - msgId: paymentPayload.chainReference!, - to: { - address: paymentPayload.targetAddress!, - amount: paymentPayload.amount!, - }, - from: paymentPayload.sourceAddress!, - }); - } - } + }; } diff --git a/packages/functions/src/services/payment/nft/nft-bid.service.ts b/packages/functions/src/services/payment/nft/nft-bid.service.ts deleted file mode 100644 index ff41f01638..0000000000 --- a/packages/functions/src/services/payment/nft/nft-bid.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { build5Db } from '@build-5/database'; -import { COL, Member, Nft, NftStatus, Transaction } from '@build-5/interfaces'; -import { last } from 'lodash'; -import { NotificationService } from '../../notification/notification'; -import { HandlerParams } from '../base'; -import { BaseNftService } from './common'; - -export class NftBidService extends BaseNftService { - public handleRequest = async ({ order, match, tran, tranEntry, build5Tran }: HandlerParams) => { - const nftDocRef = build5Db().collection(COL.NFT).doc(order.payload.nft!); - const nft = await this.transactionService.get(nftDocRef); - if (nft?.auctionFrom) { - const payment = await this.transactionService.createPayment(order, match); - await this.addNewBid(order, payment); - } else { - await this.transactionService.processAsInvalid(tran, order, tranEntry, build5Tran); - } - }; - - public async markNftAsFinalized({ uid, auctionFrom }: Nft): Promise { - if (!auctionFrom) { - throw new Error('NFT auctionFrom is no longer defined'); - } - - const nftDocRef = build5Db().doc(`${COL.NFT}/${uid}`); - const nft = await this.transactionService.get(nftDocRef); - if (nft.auctionHighestTransaction) { - const highestPayDocRef = build5Db().doc( - `${COL.TRANSACTION}/${nft.auctionHighestTransaction}`, - ); - const highestPay = (await highestPayDocRef.get())!; - - const orderId = Array.isArray(highestPay.payload.sourceTransaction) - ? last(highestPay.payload.sourceTransaction)! - : highestPay.payload.sourceTransaction!; - const orderDocRef = build5Db().doc(`${COL.TRANSACTION}/${orderId}`); - const order = await orderDocRef.get(); - if (!order) { - throw new Error('Unable to find ORDER linked to PAYMENT'); - } - - this.transactionService.markAsReconciled(order, highestPay.payload.chainReference!); - this.transactionService.createBillPayment(order, highestPay); - await this.setNftOwner(order, highestPay); - - const memberDocRef = build5Db().collection(COL.MEMBER).doc(order.member!); - const member = await memberDocRef.get(); - - const notification = NotificationService.prepareWinBid(member, nft, highestPay); - const notificationDocRef = build5Db().doc(`${COL.NOTIFICATION}/${notification.uid}`); - this.transactionService.push({ - ref: notificationDocRef, - data: notification, - action: 'set', - }); - this.transactionService.push({ - ref: orderDocRef, - data: { - linkedTransactions: build5Db().arrayUnion(...this.transactionService.linkedTransactions), - }, - action: 'update', - }); - - this.setTradingStats(nft); - - const tanglePuchase = order.payload.tanglePuchase; - const disableWithdraw = order.payload.disableWithdraw; - if (!disableWithdraw && tanglePuchase && nft.status === NftStatus.MINTED) { - await this.withdrawNft(order, nft); - } - } else { - this.transactionService.push({ - ref: nftDocRef, - data: { - auctionFrom: null, - auctionTo: null, - extendedAuctionTo: null, - auctionFloorPrice: null, - auctionLength: null, - extendedAuctionLength: null, - auctionHighestBid: null, - auctionHighestBidder: null, - auctionHighestTransaction: null, - }, - action: 'update', - }); - } - } -} diff --git a/packages/functions/src/services/payment/nft/nft-purchase.service.ts b/packages/functions/src/services/payment/nft/nft-purchase.service.ts index cd58d25be7..1e181df776 100644 --- a/packages/functions/src/services/payment/nft/nft-purchase.service.ts +++ b/packages/functions/src/services/payment/nft/nft-purchase.service.ts @@ -1,9 +1,17 @@ import { build5Db } from '@build-5/database'; -import { COL, Nft, NftStatus, Transaction, TransactionPayloadType } from '@build-5/interfaces'; -import { HandlerParams } from '../base'; +import { + Auction, + COL, + Nft, + NftStatus, + Transaction, + TransactionPayloadType, + TransactionType, +} from '@build-5/interfaces'; +import { BaseService, HandlerParams } from '../base'; import { BaseNftService } from './common'; -export class NftPurchaseService extends BaseNftService { +export class NftPurchaseService extends BaseService { public handleRequest = async ({ order, match, tran, tranEntry, build5Tran }: HandlerParams) => { const nftDocRef = build5Db().doc(`${COL.NFT}/${order.payload.nft}`); const nft = await this.transactionService.get(nftDocRef); @@ -13,17 +21,54 @@ export class NftPurchaseService extends BaseNftService { return; } + const nftService = new BaseNftService(this.transactionService); + const payment = await this.transactionService.createPayment(order, match); this.transactionService.createBillPayment(order, payment); - await this.setNftOwner(order, payment); + await nftService.setNftOwner(order, payment.payload.amount!); + + if (nft.auction) { + await this.creditBids(nft.auction); + } + this.transactionService.markAsReconciled(order, match.msgId); - this.setTradingStats(nft); + nftService.setTradingStats(nft); const tanglePuchase = order.payload.tanglePuchase; const disableWithdraw = order.payload.disableWithdraw; if (!disableWithdraw && tanglePuchase && nft.status === NftStatus.MINTED) { - await this.withdrawNft(order, nft); + await nftService.withdrawNft(order, nft); + } + }; + + private creditBids = async (auctionId: string) => { + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auctionId}`); + const auction = await this.transaction.get(auctionDocRef); + this.transactionService.push({ + ref: auctionDocRef, + data: { active: false }, + action: 'update', + }); + + for (const bid of auction.bids) { + const payments = await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.PAYMENT) + .where('member', '==', bid.bidder) + .where('payload.invalidPayment', '==', false) + .where('payload.auction', '==', auctionId) + .get(); + for (const payment of payments) { + await this.transactionService.createCredit(TransactionPayloadType.NONE, payment, { + msgId: payment.payload.chainReference || '', + to: { + address: payment.payload.targetAddress!, + amount: payment.payload.amount!, + }, + from: payment.payload.sourceAddress!, + }); + } } }; @@ -43,7 +88,11 @@ export class NftPurchaseService extends BaseNftService { data: { locked: false, lockedBy: null }, action: 'update', }); - } else if (transaction.payload.type === TransactionPayloadType.NFT_BID) { + } else if ( + [TransactionPayloadType.AUCTION_BID, TransactionPayloadType.NFT_BID].includes( + transaction.payload.type!, + ) + ) { const payments = await build5Db() .collection(COL.TRANSACTION) .where('payload.invalidPayment', '==', false) diff --git a/packages/functions/src/services/payment/payment-processing.ts b/packages/functions/src/services/payment/payment-processing.ts index 210306fccd..baad332034 100644 --- a/packages/functions/src/services/payment/payment-processing.ts +++ b/packages/functions/src/services/payment/payment-processing.ts @@ -16,12 +16,12 @@ import { dateToTimestamp } from '../../utils/dateTime.utils'; import { invalidArgument } from '../../utils/error.utils'; import { MemberAddressService } from './address/address-member.service'; import { SpaceAddressService } from './address/address.space.service'; +import { AuctionBidService } from './auction/auction-bid.service'; import { AwardFundService } from './award/award-service'; import { HandlerParams } from './base'; import { CreditService } from './credit-service'; import { MetadataNftService } from './metadataNft-service'; import { CollectionMintingService } from './nft/collection-minting.service'; -import { NftBidService } from './nft/nft-bid.service'; import { NftDepositService } from './nft/nft-deposit.service'; import { NftPurchaseService } from './nft/nft-purchase.service'; import { NftStakeService } from './nft/nft-stake.service'; @@ -143,7 +143,8 @@ export class ProcessingService { case TransactionPayloadType.NFT_PURCHASE: return new NftPurchaseService(tranService); case TransactionPayloadType.NFT_BID: - return new NftBidService(tranService); + case TransactionPayloadType.AUCTION_BID: + return new AuctionBidService(tranService); case TransactionPayloadType.SPACE_ADDRESS_VALIDATION: return new SpaceAddressService(tranService); case TransactionPayloadType.MEMBER_ADDRESS_VALIDATION: diff --git a/packages/functions/src/services/payment/tangle-service/TangleRequestService.ts b/packages/functions/src/services/payment/tangle-service/TangleRequestService.ts index 7493b588bb..9721e81a53 100644 --- a/packages/functions/src/services/payment/tangle-service/TangleRequestService.ts +++ b/packages/functions/src/services/payment/tangle-service/TangleRequestService.ts @@ -14,11 +14,12 @@ import { invalidArgument } from '../../../utils/error.utils'; import { getRandomNonce } from '../../../utils/wallet.utils'; import { BaseService, HandlerParams } from '../base'; import { TangleAddressValidationService } from './address/address-validation.service'; +import { TangleAuctionBidService, TangleNftAuctionBidService } from './auction/auction.bid.service'; +import { TangleAuctionCreateService } from './auction/auction.create.service'; import { AwardApproveParticipantService } from './award/award.approve.participant.service'; import { AwardCreateService } from './award/award.create.service'; import { AwardFundService } from './award/award.fund.service'; import { MintMetadataNftService } from './metadataNft/mint-metadata-nft.service'; -import { TangleNftBidService } from './nft/nft-bid.service'; import { NftDepositService } from './nft/nft-deposit.service'; import { TangleNftPurchaseService } from './nft/nft-purchase.service'; import { TangleNftSetForSaleService } from './nft/nft-set-for-sale.service'; @@ -95,7 +96,7 @@ export class TangleRequestService extends BaseService { case TangleRequestType.NFT_SET_FOR_SALE: return new TangleNftSetForSaleService(this.transactionService); case TangleRequestType.NFT_BID: - return new TangleNftBidService(this.transactionService); + return new TangleNftAuctionBidService(this.transactionService); case TangleRequestType.CLAIM_MINTED_AIRDROPS: return new TangleTokenClaimService(this.transactionService); case TangleRequestType.AWARD_CREATE: @@ -132,6 +133,10 @@ export class TangleRequestService extends BaseService { return new MintMetadataNftService(this.transactionService); case TangleRequestType.STAMP: return new StampTangleService(this.transactionService); + case TangleRequestType.CREATE_AUCTION: + return new TangleAuctionCreateService(this.transactionService); + case TangleRequestType.BID_AUCTION: + return new TangleAuctionBidService(this.transactionService); default: throw invalidArgument(WenError.invalid_tangle_request_type); } diff --git a/packages/functions/src/services/payment/tangle-service/auction/AuctionBidTangleRequestSchema.ts b/packages/functions/src/services/payment/tangle-service/auction/AuctionBidTangleRequestSchema.ts new file mode 100644 index 0000000000..4f62d433a5 --- /dev/null +++ b/packages/functions/src/services/payment/tangle-service/auction/AuctionBidTangleRequestSchema.ts @@ -0,0 +1,12 @@ +import { AuctionBidTangleRequest, TangleRequestType } from '@build-5/interfaces'; +import { CommonJoi, toJoiObject } from '../../../joi/common'; +import { baseTangleSchema } from '../common'; + +export const auctionBidTangleSchema = toJoiObject({ + ...baseTangleSchema(TangleRequestType.BID_AUCTION), + auction: CommonJoi.uid().description('Build5 id of the auction to bid on.'), +}) + .description('Tangle request object to create an auction bid') + .meta({ + className: 'AuctionBidTangleRequest', + }); diff --git a/packages/functions/src/services/payment/tangle-service/auction/AuctionCreateTangleRequestSchema.ts b/packages/functions/src/services/payment/tangle-service/auction/AuctionCreateTangleRequestSchema.ts new file mode 100644 index 0000000000..31e3257783 --- /dev/null +++ b/packages/functions/src/services/payment/tangle-service/auction/AuctionCreateTangleRequestSchema.ts @@ -0,0 +1,13 @@ +import { AuctionCreateTangleRequest, TangleRequestType } from '@build-5/interfaces'; +import { auctionCreateSchema } from '../../../../controls/auction/AuctionCreateRequestSchema'; +import { toJoiObject } from '../../../joi/common'; +import { baseTangleSchema } from '../common'; + +export const auctionCreateTangleSchema = toJoiObject({ + ...baseTangleSchema(TangleRequestType.CREATE_AUCTION), + ...auctionCreateSchema, +}) + .description('Request object to create an auction.') + .meta({ + className: 'AuctionCreateTangleRequest', + }); diff --git a/packages/functions/src/services/payment/tangle-service/auction/NftBidTangleRequestSchema.ts b/packages/functions/src/services/payment/tangle-service/auction/NftBidTangleRequestSchema.ts new file mode 100644 index 0000000000..a5ea769043 --- /dev/null +++ b/packages/functions/src/services/payment/tangle-service/auction/NftBidTangleRequestSchema.ts @@ -0,0 +1,16 @@ +import { NftBidTangleRequest, TangleRequestType } from '@build-5/interfaces'; +import Joi from 'joi'; +import { CommonJoi, toJoiObject } from '../../../joi/common'; +import { baseTangleSchema } from '../common'; + +export const nftBidSchema = toJoiObject({ + ...baseTangleSchema(TangleRequestType.NFT_BID), + nft: CommonJoi.uid().description('Build5 id of the nft to bid on.'), + disableWithdraw: Joi.boolean().description( + "If set to true, NFT will not be sent to the buyer's validated address upon purchase.", + ), +}) + .description('Tangle request object to create an NFT bid') + .meta({ + className: 'NftBidTangleRequest', + }); diff --git a/packages/functions/src/services/payment/tangle-service/auction/auction.bid.order.ts b/packages/functions/src/services/payment/tangle-service/auction/auction.bid.order.ts new file mode 100644 index 0000000000..6550d9b861 --- /dev/null +++ b/packages/functions/src/services/payment/tangle-service/auction/auction.bid.order.ts @@ -0,0 +1,164 @@ +import { build5Db } from '@build-5/database'; +import { + Auction, + AuctionType, + COL, + Collection, + CollectionStatus, + Entity, + MIN_AMOUNT_TO_TRANSFER, + Member, + Nft, + NftAccess, + Space, + Transaction, + TransactionPayloadType, + TransactionType, + TransactionValidationType, + WenError, +} from '@build-5/interfaces'; +import { assertMemberHasValidAddress, getAddress } from '../../../../utils/address.utils'; +import { generateRandomAmount, getProjects, getRestrictions } from '../../../../utils/common.utils'; +import { isProdEnv } from '../../../../utils/config.utils'; +import { dateToTimestamp } from '../../../../utils/dateTime.utils'; +import { invalidArgument } from '../../../../utils/error.utils'; +import { assertIpNotBlocked } from '../../../../utils/ip.utils'; +import { getSpace } from '../../../../utils/space.utils'; +import { getRandomEthAddress } from '../../../../utils/wallet.utils'; +import { WalletService } from '../../../wallet/wallet.service'; + +export const createBidOrder = async ( + project: string, + owner: string, + auctionId: string, + ip = '', +): Promise => { + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auctionId}`); + const auction = await auctionDocRef.get(); + if (!auction) { + throw invalidArgument(WenError.auction_does_not_exist); + } + if (!auction.active) { + throw invalidArgument(WenError.auction_not_active); + } + + const validationResponse = await assertAuctionData(owner, ip, auction); + + const network = auction?.network; + + const wallet = await WalletService.newWallet(network); + const targetAddress = await wallet.getNewIotaAddressDetails(); + + const order = { + project, + projects: getProjects([], project), + type: TransactionType.ORDER, + uid: getRandomEthAddress(), + member: owner, + space: '', + network, + payload: { + type: + auction.type === AuctionType.NFT + ? TransactionPayloadType.NFT_BID + : TransactionPayloadType.AUCTION_BID, + amount: generateRandomAmount(), + targetAddress: targetAddress.bech32, + expiresOn: dateToTimestamp((auction.extendedAuctionTo || auction.auctionTo).toDate()), + reconciled: false, + validationType: TransactionValidationType.ADDRESS, + void: false, + chainReference: null, + auction: auction.uid, + }, + linkedTransactions: [], + }; + + if (auction.type === AuctionType.NFT) { + const nft = validationResponse as Nft; + + const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${nft.collection}`); + const collection = (await collectionDocRef.get())!; + + const spaceDocRef = build5Db().doc(`${COL.SPACE}/${collection.space}`); + const space = await spaceDocRef.get(); + + const prevOwnerDocRef = build5Db().doc(`${COL.MEMBER}/${nft.owner}`); + const prevOwner = await prevOwnerDocRef.get(); + assertMemberHasValidAddress(prevOwner, network); + + const royaltySpace = await getSpace(collection.royaltiesSpace); + + const auctionFloorPrice = auction.auctionFloorPrice || MIN_AMOUNT_TO_TRANSFER; + const finalPrice = Number(Math.max(auctionFloorPrice, MIN_AMOUNT_TO_TRANSFER).toPrecision(2)); + + return { + ...order, + space: collection.space, + payload: { + ...order.payload, + amount: finalPrice, + beneficiary: nft.owner ? Entity.MEMBER : Entity.SPACE, + beneficiaryUid: nft.owner || collection.space, + beneficiaryAddress: getAddress(nft.owner ? prevOwner : space, network), + royaltiesFee: collection.royaltiesFee, + royaltiesSpace: collection.royaltiesSpace, + royaltiesSpaceAddress: getAddress(royaltySpace, network), + expiresOn: nft.auctionTo!, + nft: nft.uid, + collection: collection.uid, + restrictions: getRestrictions(collection, nft), + }, + }; + } + + return order; +}; + +const assertAuctionData = async (owner: string, ip: string, auction: Auction) => { + if (!auction.active) { + throw invalidArgument(WenError.auction_not_active); + } + switch (auction.type) { + case AuctionType.NFT: + return await assertNftAuction(owner, ip, auction); + } + return; +}; + +const assertNftAuction = async (owner: string, ip: string, auction: Auction) => { + const nftDocRef = build5Db().doc(`${COL.NFT}/${auction.nftId}`); + const nft = await nftDocRef.get(); + if (!nft) { + throw invalidArgument(WenError.nft_does_not_exists); + } + + if (isProdEnv()) { + await assertIpNotBlocked(ip, nft.uid, 'nft'); + } + + const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${nft.collection}`); + const collection = (await collectionDocRef.get())!; + + if (!collection.approved) { + throw invalidArgument(WenError.collection_must_be_approved); + } + + if (![CollectionStatus.PRE_MINTED, CollectionStatus.MINTED].includes(collection.status!)) { + throw invalidArgument(WenError.invalid_collection_status); + } + + if (nft.saleAccess === NftAccess.MEMBERS && !(nft.saleAccessMembers || []).includes(owner)) { + throw invalidArgument(WenError.you_are_not_allowed_member_to_purchase_this_nft); + } + + if (nft.placeholderNft) { + throw invalidArgument(WenError.nft_placeholder_cant_be_purchased); + } + + if (nft.owner === owner) { + throw invalidArgument(WenError.you_cant_buy_your_nft); + } + + return nft; +}; diff --git a/packages/functions/src/services/payment/tangle-service/auction/auction.bid.service.ts b/packages/functions/src/services/payment/tangle-service/auction/auction.bid.service.ts new file mode 100644 index 0000000000..fe79ec9168 --- /dev/null +++ b/packages/functions/src/services/payment/tangle-service/auction/auction.bid.service.ts @@ -0,0 +1,88 @@ +import { build5Db } from '@build-5/database'; +import { COL, Nft, TransactionPayloadType, WenError } from '@build-5/interfaces'; +import { invalidArgument } from '../../../../utils/error.utils'; +import { assertValidationAsync } from '../../../../utils/schema.utils'; +import { HandlerParams } from '../../base'; +import { TransactionService } from '../../transaction-service'; +import { auctionBidTangleSchema } from './AuctionBidTangleRequestSchema'; +import { nftBidSchema } from './NftBidTangleRequestSchema'; +import { createBidOrder } from './auction.bid.order'; + +export class TangleNftAuctionBidService { + constructor(readonly transactionService: TransactionService) {} + + public handleRequest = async ({ + request, + project, + owner, + order: tangleOrder, + tran, + tranEntry, + }: HandlerParams) => { + const params = await assertValidationAsync(nftBidSchema, request); + + const nftDocRef = build5Db().doc(`${COL.NFT}/${params.nft}`); + const nft = await nftDocRef.get(); + + const order = await createBidOrder(project, owner, nft?.auction || ''); + order.payload.tanglePuchase = true; + order.payload.disableWithdraw = params.disableWithdraw || false; + + if (tangleOrder.network !== order.network) { + throw invalidArgument(WenError.invalid_network); + } + + this.transactionService.push({ + ref: build5Db().doc(`${COL.TRANSACTION}/${order.uid}`), + data: order, + action: 'set', + merge: true, + }); + + this.transactionService.createUnlockTransaction( + order, + tran, + tranEntry, + TransactionPayloadType.TANGLE_TRANSFER, + tranEntry.outputId, + ); + return; + }; +} + +export class TangleAuctionBidService { + constructor(readonly transactionService: TransactionService) {} + + public handleRequest = async ({ + request, + project, + owner, + order: tangleOrder, + tran, + tranEntry, + }: HandlerParams) => { + const params = await assertValidationAsync(auctionBidTangleSchema, request); + + const order = await createBidOrder(project, owner, params.auction); + + if (tangleOrder.network !== order.network) { + throw invalidArgument(WenError.invalid_network); + } + + this.transactionService.push({ + ref: build5Db().doc(`${COL.TRANSACTION}/${order.uid}`), + data: order, + action: 'set', + merge: true, + }); + + this.transactionService.createUnlockTransaction( + order, + tran, + tranEntry, + TransactionPayloadType.TANGLE_TRANSFER, + tranEntry.outputId, + ); + return; + }; +} diff --git a/packages/functions/src/services/payment/tangle-service/auction/auction.create.service.ts b/packages/functions/src/services/payment/tangle-service/auction/auction.create.service.ts new file mode 100644 index 0000000000..6873a36597 --- /dev/null +++ b/packages/functions/src/services/payment/tangle-service/auction/auction.create.service.ts @@ -0,0 +1,69 @@ +import { build5Db } from '@build-5/database'; +import { + Auction, + AuctionCreateRequest, + AuctionCreateTangleRequest, + AuctionType, + COL, + Network, +} from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { getProjects } from '../../../../utils/common.utils'; +import { dateToTimestamp } from '../../../../utils/dateTime.utils'; +import { assertValidationAsync } from '../../../../utils/schema.utils'; +import { getRandomEthAddress } from '../../../../utils/wallet.utils'; +import { HandlerParams } from '../../base'; +import { TransactionService } from '../../transaction-service'; +import { auctionCreateTangleSchema } from './AuctionCreateTangleRequestSchema'; + +export class TangleAuctionCreateService { + constructor(readonly transactionService: TransactionService) {} + + public handleRequest = async ({ request, project, owner }: HandlerParams) => { + const params = await assertValidationAsync(auctionCreateTangleSchema, request); + + const auction = getAuctionData(project, owner, params); + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auction.uid}`); + + this.transactionService.push({ ref: auctionDocRef, data: auction, action: 'set' }); + + return { auction: auction.uid }; + }; +} + +export const getAuctionData = ( + project: string, + owner: string, + params: AuctionCreateRequest | AuctionCreateTangleRequest, +) => { + const auction: Auction = { + uid: getRandomEthAddress(), + project, + projects: getProjects([], project), + createdBy: owner, + auctionFrom: dateToTimestamp(params.auctionFrom), + auctionTo: dateToTimestamp(dayjs(params.auctionFrom).add(params.auctionLength)), + auctionLength: params.auctionLength, + + auctionFloorPrice: params.auctionFloorPrice || 0, + + maxBids: params.maxBids, + + type: AuctionType.OPEN, + network: params.network as Network, + + active: true, + topUpBased: params.topUpBased || false, + + bids: [], + }; + + if (params.extendedAuctionLength && params.extendAuctionWithin) { + auction.extendedAuctionLength = params.extendedAuctionLength; + auction.extendAuctionWithin = params.extendAuctionWithin; + auction.extendedAuctionTo = dateToTimestamp( + dayjs(params.auctionFrom).add(params.extendedAuctionLength), + ); + } + return auction; +}; diff --git a/packages/functions/src/services/payment/tangle-service/nft/nft-bid.service.ts b/packages/functions/src/services/payment/tangle-service/nft/nft-bid.service.ts deleted file mode 100644 index 09204e3136..0000000000 --- a/packages/functions/src/services/payment/tangle-service/nft/nft-bid.service.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { build5Db } from '@build-5/database'; -import { - COL, - Collection, - CollectionStatus, - Entity, - MIN_AMOUNT_TO_TRANSFER, - Member, - Network, - Nft, - NftAccess, - Space, - Transaction, - TransactionPayloadType, - TransactionType, - TransactionValidationType, - WenError, -} from '@build-5/interfaces'; -import { assertMemberHasValidAddress, getAddress } from '../../../../utils/address.utils'; -import { getProjects, getRestrictions } from '../../../../utils/common.utils'; -import { isProdEnv } from '../../../../utils/config.utils'; -import { invalidArgument } from '../../../../utils/error.utils'; -import { assertIpNotBlocked } from '../../../../utils/ip.utils'; -import { assertValidationAsync } from '../../../../utils/schema.utils'; -import { getSpace } from '../../../../utils/space.utils'; -import { getRandomEthAddress } from '../../../../utils/wallet.utils'; -import { WalletService } from '../../../wallet/wallet.service'; -import { HandlerParams } from '../../base'; -import { TransactionService } from '../../transaction-service'; -import { nftBidSchema } from './NftBidTangleRequestSchema'; - -export class TangleNftBidService { - constructor(readonly transactionService: TransactionService) {} - - public handleRequest = async ({ - request, - project, - owner, - order: tangleOrder, - tran, - tranEntry, - }: HandlerParams) => { - const params = await assertValidationAsync(nftBidSchema, request); - - const order = await createNftBidOrder(project, params.nft, owner, ''); - order.payload.tanglePuchase = true; - order.payload.disableWithdraw = params.disableWithdraw || false; - - if (tangleOrder.network !== order.network) { - throw invalidArgument(WenError.invalid_network); - } - - this.transactionService.push({ - ref: build5Db().doc(`${COL.TRANSACTION}/${order.uid}`), - data: order, - action: 'set', - }); - - this.transactionService.createUnlockTransaction( - order, - tran, - tranEntry, - TransactionPayloadType.TANGLE_TRANSFER, - tranEntry.outputId, - ); - return; - }; -} - -export const createNftBidOrder = async ( - project: string, - nftId: string, - owner: string, - ip = '', -): Promise => { - const nftDocRef = build5Db().doc(`${COL.NFT}/${nftId}`); - const nft = await nftDocRef.get(); - if (!nft) { - throw invalidArgument(WenError.nft_does_not_exists); - } - - if (isProdEnv()) { - await assertIpNotBlocked(ip, nftId, 'nft'); - } - - const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${nft.collection}`); - const collection = (await collectionDocRef.get())!; - - const spaceDocRef = build5Db().doc(`${COL.SPACE}/${collection.space}`); - const space = await spaceDocRef.get(); - - if (!collection.approved) { - throw invalidArgument(WenError.collection_must_be_approved); - } - - if (![CollectionStatus.PRE_MINTED, CollectionStatus.MINTED].includes(collection.status!)) { - throw invalidArgument(WenError.invalid_collection_status); - } - - if (nft.saleAccess === NftAccess.MEMBERS && !(nft.saleAccessMembers || []).includes(owner)) { - throw invalidArgument(WenError.you_are_not_allowed_member_to_purchase_this_nft); - } - - if (!nft.auctionFrom) { - throw invalidArgument(WenError.nft_not_available_for_sale); - } - - if (nft.placeholderNft) { - throw invalidArgument(WenError.nft_placeholder_cant_be_purchased); - } - - if (nft.owner === owner) { - throw invalidArgument(WenError.you_cant_buy_your_nft); - } - - const isProd = isProdEnv(); - const network = nft.mintingData?.network || (isProd ? Network.IOTA : Network.ATOI); - - const prevOwnerDocRef = build5Db().doc(`${COL.MEMBER}/${nft.owner}`); - const prevOwner = await prevOwnerDocRef.get(); - assertMemberHasValidAddress(prevOwner, network); - - const newWallet = await WalletService.newWallet(network); - const targetAddress = await newWallet.getNewIotaAddressDetails(); - const royaltySpace = await getSpace(collection.royaltiesSpace); - - const auctionFloorPrice = nft.auctionFloorPrice || MIN_AMOUNT_TO_TRANSFER; - const finalPrice = Number(Math.max(auctionFloorPrice, MIN_AMOUNT_TO_TRANSFER).toPrecision(2)); - - return { - project, - projects: getProjects([], project), - type: TransactionType.ORDER, - uid: getRandomEthAddress(), - member: owner, - space: collection.space, - network, - payload: { - type: TransactionPayloadType.NFT_BID, - amount: finalPrice, - targetAddress: targetAddress.bech32, - beneficiary: nft.owner ? Entity.MEMBER : Entity.SPACE, - beneficiaryUid: nft.owner || collection.space, - beneficiaryAddress: getAddress(nft.owner ? prevOwner : space, network), - royaltiesFee: collection.royaltiesFee, - royaltiesSpace: collection.royaltiesSpace, - royaltiesSpaceAddress: getAddress(royaltySpace, network), - expiresOn: nft.auctionTo!, - reconciled: false, - validationType: TransactionValidationType.ADDRESS, - void: false, - chainReference: null, - nft: nft.uid, - collection: collection.uid, - restrictions: getRestrictions(collection, nft), - }, - linkedTransactions: [], - }; -}; diff --git a/packages/functions/src/services/payment/tangle-service/nft/nft-set-for-sale.service.ts b/packages/functions/src/services/payment/tangle-service/nft/nft-set-for-sale.service.ts index 2a930eb22c..1a48547793 100644 --- a/packages/functions/src/services/payment/tangle-service/nft/nft-set-for-sale.service.ts +++ b/packages/functions/src/services/payment/tangle-service/nft/nft-set-for-sale.service.ts @@ -1,5 +1,7 @@ import { build5Db } from '@build-5/database'; import { + Auction, + AuctionType, COL, Collection, CollectionStatus, @@ -13,27 +15,39 @@ import { } from '@build-5/interfaces'; import dayjs from 'dayjs'; import { assertMemberHasValidAddress } from '../../../../utils/address.utils'; +import { getProjects } from '../../../../utils/common.utils'; +import { getDefaultNetwork } from '../../../../utils/config.utils'; import { dateToTimestamp } from '../../../../utils/dateTime.utils'; import { invalidArgument } from '../../../../utils/error.utils'; import { assertValidationAsync } from '../../../../utils/schema.utils'; +import { getRandomEthAddress } from '../../../../utils/wallet.utils'; import { BaseService, HandlerParams } from '../../base'; import { setNftForSaleTangleSchema } from './NftSetForSaleTangleRequestSchema'; export class TangleNftSetForSaleService extends BaseService { - public handleRequest = async ({ owner, request }: HandlerParams) => { + public handleRequest = async ({ owner, request, project }: HandlerParams) => { const params = await assertValidationAsync(setNftForSaleTangleSchema, request); const memberDocRef = build5Db().doc(`${COL.MEMBER}/${owner}`); const member = await memberDocRef.get(); - const updateData = await getNftSetForSaleParams(params, member!); + const { nft, auction } = await getNftSetForSaleParams(member!, project, params); const nftDocRef = build5Db().doc(`${COL.NFT}/${params.nft}`); - this.transactionService.push({ ref: nftDocRef, data: updateData, action: 'update' }); + this.transactionService.push({ ref: nftDocRef, data: nft, action: 'update' }); + + if (auction) { + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auction.uid}`); + this.transactionService.push({ ref: auctionDocRef, data: auction, action: 'set' }); + } return { status: 'success' }; }; } -export const getNftSetForSaleParams = async (params: NftSetForSaleRequest, owner: Member) => { +export const getNftSetForSaleParams = async ( + owner: Member, + project: string, + params: NftSetForSaleRequest, +) => { const nftDocRef = build5Db().doc(`${COL.NFT}/${params.nft}`); const nft = await nftDocRef.get(); if (!nft) { @@ -41,7 +55,7 @@ export const getNftSetForSaleParams = async (params: NftSetForSaleRequest, owner } if (nft.auctionFrom && dayjs(nft.auctionFrom.toDate()).isBefore(dayjs())) { - throw invalidArgument(WenError.nft_auction_already_in_progress); + throw invalidArgument(WenError.auction_already_in_progress); } if (nft.setAsAvatar) { @@ -76,7 +90,8 @@ export const getNftSetForSaleParams = async (params: NftSetForSaleRequest, owner throw invalidArgument(WenError.invalid_collection_status); } - return getNftUpdateData(params); + const auction = getAuctionData(project, owner.uid, params, nft); + return { nft: { ...getNftUpdateData(params), auction: auction?.uid || '' }, auction }; }; const getNftUpdateData = (params: NftSetForSaleRequest) => { @@ -94,7 +109,6 @@ const getNftUpdateData = (params: NftSetForSaleRequest) => { update.auctionLength = params.auctionLength; update.auctionHighestBid = 0; update.auctionHighestBidder = null; - update.auctionHighestTransaction = null; if (params.extendedAuctionLength) { update.extendedAuctionTo = dayjs(params.auctionFrom) .add(params.extendedAuctionLength) @@ -111,7 +125,6 @@ const getNftUpdateData = (params: NftSetForSaleRequest) => { update.extendedAuctionLength = null; update.auctionHighestBid = null; update.auctionHighestBidder = null; - update.auctionHighestTransaction = null; } if (params.availableFrom) { @@ -123,3 +136,38 @@ const getNftUpdateData = (params: NftSetForSaleRequest) => { } return update; }; + +const getAuctionData = (project: string, owner: string, params: NftSetForSaleRequest, nft: Nft) => { + if (!params.auctionFrom) { + return; + } + const auction: Auction = { + uid: getRandomEthAddress(), + createdBy: owner, + project, + projects: getProjects([], project), + auctionFrom: dateToTimestamp(params.auctionFrom), + auctionTo: dateToTimestamp(dayjs(params.auctionFrom).add(params.auctionLength || 0)), + auctionFloorPrice: params.auctionFloorPrice || 0, + auctionLength: params.auctionLength!, + + bids: [], + maxBids: 1, + type: AuctionType.NFT, + network: nft.mintingData?.network || getDefaultNetwork(), + nftId: nft.uid, + + active: true, + }; + + if (params.extendedAuctionLength) { + return { + ...auction, + extendedAuctionTo: dayjs(params.auctionFrom).add(params.extendedAuctionLength).toDate(), + extendedAuctionLength: params.extendedAuctionLength || 0, + extendAuctionWithin: params.extendAuctionWithin || EXTEND_AUCTION_WITHIN, + }; + } + + return auction; +}; diff --git a/packages/functions/src/services/payment/transaction-service.ts b/packages/functions/src/services/payment/transaction-service.ts index f04a306a08..32e379d2ca 100644 --- a/packages/functions/src/services/payment/transaction-service.ts +++ b/packages/functions/src/services/payment/transaction-service.ts @@ -100,6 +100,9 @@ export class TransactionService { if (order.payload.stamp) { data.payload.stamp = order.payload.stamp; } + if (order.payload.auction) { + data.payload.auction = order.payload.auction; + } if (order.payload.token) { const tokenDocRef = build5Db().doc(`${COL.TOKEN}/${order.payload.token}`); diff --git a/packages/functions/src/triggers/collection.trigger.ts b/packages/functions/src/triggers/collection.trigger.ts index d9572fb010..cb6fc1ef8a 100644 --- a/packages/functions/src/triggers/collection.trigger.ts +++ b/packages/functions/src/triggers/collection.trigger.ts @@ -281,39 +281,44 @@ const setNftForMinting = async (nftId: string, collection: Collection): Promise< extendedAuctionLength: null, auctionHighestBid: null, auctionHighestBidder: null, - auctionHighestTransaction: null, + auction: null, mediaStatus: nft.mediaStatus === MediaStatus.PREPARE_IPFS ? MediaStatus.ERROR : nft.mediaStatus || MediaStatus.PREPARE_IPFS, }; - if (nft.auctionHighestTransaction) { - const highestTransaction = ( - await build5Db().doc(`${COL.TRANSACTION}/${nft.auctionHighestTransaction}`).get() - ); - const member = ( - await build5Db().doc(`${COL.MEMBER}/${nft.auctionHighestBidder}`).get() - ); - const credit: Transaction = { - project: getProject(highestTransaction), - projects: getProjects([highestTransaction]), - type: TransactionType.CREDIT, - uid: getRandomEthAddress(), - space: highestTransaction.space, - member: highestTransaction.member, - network: highestTransaction.network || DEFAULT_NETWORK, - payload: { - amount: highestTransaction.payload.amount, - sourceAddress: highestTransaction.payload.targetAddress, - targetAddress: getAddress(member, highestTransaction.network || DEFAULT_NETWORK), - sourceTransaction: [highestTransaction.uid], - nft: nft.uid, - collection: nft.collection, - }, - }; - const creditDocRef = build5Db().doc(`${COL.TRANSACTION}/${credit.uid}`); - transaction.create(creditDocRef, credit); + if (nft.auction) { + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${nft.auction}`); + transaction.update(auctionDocRef, { active: false }); + + const payments = await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.PAYMENT) + .where('payload.invalidPayment', '==', false) + .where('payload.auction', '==', nft.auction) + .get(); + for (const payment of payments) { + const credit: Transaction = { + project: getProject(payment), + projects: getProjects([payment]), + type: TransactionType.CREDIT, + uid: getRandomEthAddress(), + space: payment.space, + member: payment.member, + network: payment.network || DEFAULT_NETWORK, + payload: { + amount: payment.payload.amount, + sourceAddress: payment.payload.targetAddress, + targetAddress: payment.payload.sourceAddress, + sourceTransaction: [payment.uid], + nft: nft.uid, + collection: nft.collection, + }, + }; + const creditDocRef = build5Db().doc(`${COL.TRANSACTION}/${credit.uid}`); + transaction.create(creditDocRef, credit); + } } if (nft.locked) { diff --git a/packages/functions/src/utils/config.utils.ts b/packages/functions/src/utils/config.utils.ts index 1400dfd21f..01b4618508 100644 --- a/packages/functions/src/utils/config.utils.ts +++ b/packages/functions/src/utils/config.utils.ts @@ -70,3 +70,5 @@ export const xpTokenUid = () => process.env.XPTOKEN_UID!; export const xpTokenGuardianId = () => process.env.XPTOKEN_GUARDIANID!; export const getStampRoyaltyAddress = (network: Network) => STAMP_ROYALTY_ADDRESS[network]; + +export const getDefaultNetwork = () => (isProdEnv() ? Network.IOTA : Network.ATOI); diff --git a/packages/functions/test-tangle/auction-tangle/auction.bit.tangle.spec.ts b/packages/functions/test-tangle/auction-tangle/auction.bit.tangle.spec.ts new file mode 100644 index 0000000000..dd5a9eabb2 --- /dev/null +++ b/packages/functions/test-tangle/auction-tangle/auction.bit.tangle.spec.ts @@ -0,0 +1,127 @@ +import { IDocument, build5Db } from '@build-5/database'; +import { + Auction, + AuctionType, + COL, + MIN_IOTA_AMOUNT, + Member, + Network, + TangleRequestType, + Transaction, + TransactionType, +} from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { MnemonicService } from '../../src/services/wallet/mnemonic'; +import { Wallet } from '../../src/services/wallet/wallet'; +import { AddressDetails } from '../../src/services/wallet/wallet.service'; +import { getAddress } from '../../src/utils/address.utils'; +import * as wallet from '../../src/utils/wallet.utils'; +import { createMember, wait } from '../../test/controls/common'; +import { getWallet } from '../../test/set-up'; +import { getTangleOrder } from '../common'; +import { requestFundsFromFaucet } from '../faucet'; + +let walletSpy: any; + +describe('Auction tangle test', () => { + let member: string; + let memberAddress: AddressDetails; + let w: Wallet; + let tangleOrder: Transaction; + let auctionDocRef: IDocument; + const now = dayjs(); + let auction: Auction; + + beforeAll(async () => { + walletSpy = jest.spyOn(wallet, 'decodeAuth'); + w = await getWallet(Network.RMS); + tangleOrder = await getTangleOrder(Network.RMS); + }); + + beforeEach(async () => { + member = await createMember(walletSpy); + + const memberDocRef = build5Db().doc(`${COL.MEMBER}/${member}`); + const memberData = await memberDocRef.get(); + const bech32 = getAddress(memberData, Network.RMS); + memberAddress = await w.getAddressDetails(bech32); + + await requestFundsFromFaucet(Network.RMS, memberAddress.bech32, 5 * MIN_IOTA_AMOUNT); + await w.send(memberAddress, tangleOrder.payload.targetAddress!, 5 * MIN_IOTA_AMOUNT, { + customMetadata: { + request: { + requestType: TangleRequestType.CREATE_AUCTION, + ...auctionRequest(now), + }, + }, + }); + await MnemonicService.store(bech32, memberAddress.mnemonic); + + const creaditQuery = build5Db() + .collection(COL.TRANSACTION) + .where('member', '==', member) + .where('type', '==', TransactionType.CREDIT_TANGLE_REQUEST); + await wait(async () => { + const credits = await creaditQuery.get(); + return credits.length === 1 && credits[0].payload.walletReference?.confirmed; + }); + + const credits = await creaditQuery.get(); + expect(credits[0].payload.response?.auction).toBeDefined(); + + auctionDocRef = build5Db().doc(`${COL.AUCTION}/${credits[0].payload.response?.auction}`); + auction = await auctionDocRef.get(); + }); + + it('Should bid on auction', async () => { + expect(dayjs(auction.auctionFrom.toDate()).isSame(now)).toBe(true); + expect(dayjs(auction.auctionTo.toDate()).isSame(now.add(60000 * 4))); + expect(auction.auctionLength).toBe(60000 * 4); + + expect(dayjs(auction.extendedAuctionTo?.toDate()).isSame(now.add(60000 * 4 + 6000))).toBe(true); + expect(auction.extendedAuctionLength).toBe(60000 * 4 + 6000); + expect(auction.extendAuctionWithin).toBe(60000 * 4); + + expect(auction.auctionFloorPrice).toBe(2 * MIN_IOTA_AMOUNT); + expect(auction.maxBids).toBe(2); + expect(auction.type).toBe(AuctionType.OPEN); + expect(auction.network).toBe(Network.RMS); + expect(auction.nftId).toBeUndefined(); + expect(auction.active).toBe(true); + expect(auction.topUpBased).toBe(true); + }); + + it('Should bid on auction', async () => { + const block = await w.send( + memberAddress, + tangleOrder.payload.targetAddress!, + 2 * MIN_IOTA_AMOUNT, + { + customMetadata: { + request: { + requestType: TangleRequestType.BID_AUCTION, + auction: auction.uid, + }, + }, + }, + ); + console.log(block); + await wait(async () => { + auction = await auctionDocRef.get(); + return auction.auctionHighestBidder === member; + }); + + expect(auction.auctionHighestBid).toBe(2 * MIN_IOTA_AMOUNT); + }); +}); + +const auctionRequest = (now: dayjs.Dayjs, auctionLength = 60000 * 4) => ({ + auctionFrom: now.toDate(), + auctionFloorPrice: 2 * MIN_IOTA_AMOUNT, + auctionLength, + extendedAuctionLength: auctionLength + 6000, + extendAuctionWithin: 60000 * 4, + maxBids: 2, + network: Network.RMS, + topUpBased: true, +}); diff --git a/packages/functions/test-tangle/collection-minting/collection-minting_4.spec.ts b/packages/functions/test-tangle/collection-minting/collection-minting_4.spec.ts deleted file mode 100644 index 4bd93f1af6..0000000000 --- a/packages/functions/test-tangle/collection-minting/collection-minting_4.spec.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { build5Db } from '@build-5/database'; -import { - COL, - Collection, - KEY_NAME_TANGLE, - Member, - MIN_IOTA_AMOUNT, - Network, - Nft, - NftStatus, - Space, - Transaction, - TransactionType, -} from '@build-5/interfaces'; -import { NftOutput } from '@iota/sdk'; -import { getAddress } from '../../src/utils/address.utils'; -import { EMPTY_NFT_ID } from '../../src/utils/collection-minting-utils/nft.utils'; -import { CollectionMintHelper, getNftMetadata } from './Helper'; - -describe('Collection minting', () => { - const helper = new CollectionMintHelper(); - - beforeAll(async () => { - await helper.beforeAll(); - }); - - beforeEach(async () => { - await helper.beforeEach(); - }); - - it.each([false, true])( - 'Should mint, cancel active sells, not mint placeholder', - async (limited: boolean) => { - await helper.createAndOrderNft(); - await helper.createAndOrderNft(true); - const nft = await helper.createAndOrderNft(true, true); - let placeholderNft = await helper.createAndOrderNft(true, false); - await build5Db().doc(`${COL.NFT}/${placeholderNft.uid}`).update({ placeholderNft: true }); - await build5Db() - .doc(`${COL.COLLECTION}/${helper.collection}`) - .update({ total: build5Db().inc(-1) }); - - if (limited) { - await build5Db() - .doc(`${COL.COLLECTION}/${helper.collection}`) - .update({ limitedEdition: limited }); - } - await helper.mintCollection(); - if (limited) { - await helper.lockCollectionConfirmed(); - const collection = ( - await build5Db().doc(`${COL.COLLECTION}/${helper.collection}`).get() - ); - const outputId = await helper.walletService!.client.nftOutputId( - collection.mintingData?.nftId!, - ); - const output = (await helper.walletService!.client.getOutput(outputId)).output; - expect((output.unlockConditions[0] as any).address.pubKeyHash).toBe(EMPTY_NFT_ID); - } - - const bidCredit = ( - await build5Db() - .collection(COL.TRANSACTION) - .where('payload.collection', '==', helper.collection) - .where('type', '==', TransactionType.CREDIT) - .get() - ).map((d) => d); - expect(bidCredit.length).toBe(1); - expect(bidCredit[0].payload.amount).toBe(2 * MIN_IOTA_AMOUNT); - const bidder = await build5Db().doc(`${COL.MEMBER}/${helper.member}`).get(); - const order = ( - await build5Db().doc(`${COL.TRANSACTION}/${nft.auctionHighestTransaction}`).get() - ); - expect(bidCredit[0].payload.targetAddress).toBe(getAddress(bidder, Network.ATOI)); - expect(bidCredit[0].payload.sourceAddress).toBe(order.payload.targetAddress); - - const nftsQuery = build5Db() - .collection(COL.NFT) - .where('collection', '==', helper.collection) - .where('placeholderNft', '==', false); - const nfts = (await nftsQuery.get()).map((d) => d); - const allCancelled = nfts.reduce( - (acc, act) => - acc && - act.auctionFrom === null && - act.auctionTo === null && - act.auctionFloorPrice === null && - act.auctionLength === null && - act.auctionHighestBid === null && - act.auctionHighestBidder === null && - act.auctionHighestTransaction === null && - (!act.sold || (act.availableFrom === null && act.availablePrice === null)), - true, - ); - expect(allCancelled).toBe(true); - - const collection = ( - await build5Db().doc(`${COL.COLLECTION}/${helper.collection}`).get() - ); - const royaltySpace = ( - await build5Db().doc(`${COL.SPACE}/${collection.royaltiesSpace}`).get() - ); - - const collectionOutput = await helper.nftWallet!.getNftOutputs( - collection.mintingData?.nftId, - undefined, - ); - expect(Object.keys(collectionOutput).length).toBe(1); - const collectionMetadata = getNftMetadata(Object.values(collectionOutput)[0]); - expect(collectionMetadata.standard).toBe('IRC27'); - expect(collectionMetadata.version).toBe('v1.0'); - expect(collectionMetadata.type).toBe('image/png'); - expect(collectionMetadata.uri).toBe(`ipfs://${collection.ipfsMedia}`); - expect(collectionMetadata.description).toBe(collection.description); - expect(collectionMetadata.issuerName).toBe(KEY_NAME_TANGLE); - expect(collectionMetadata.royalties[getAddress(royaltySpace, Network.RMS)]).toBe( - collection.royaltiesFee, - ); - expect(collectionMetadata.build5Id).toBe(collection.uid); - - for (const nft of nfts) { - const nftOutputs = await helper.nftWallet!.getNftOutputs(nft.mintingData?.nftId, undefined); - expect(Object.keys(nftOutputs).length).toBe(1); - const metadata = getNftMetadata(Object.values(nftOutputs)[0]); - expect(metadata.standard).toBe('IRC27'); - expect(metadata.version).toBe('v1.0'); - expect(metadata.type).toBe('image/png'); - expect(metadata.uri).toBe(`ipfs://${nft.ipfsMedia}`); - expect(metadata.name).toBe(nft.name); - expect(metadata.description).toBe(nft.description); - expect(metadata.issuerName).toBe(KEY_NAME_TANGLE); - expect(metadata.collectionId).toBe(collection.mintingData?.nftId); - expect(metadata.collectionName).toBe(collection.name); - expect(metadata.royalties[getAddress(royaltySpace, Network.RMS)]).toBe( - collection.royaltiesFee, - ); - expect(metadata.build5Id).toBe(nft.uid); - } - - placeholderNft = await build5Db().doc(`${COL.NFT}/${placeholderNft.uid}`).get(); - expect(placeholderNft.status).toBe(NftStatus.PRE_MINTED); - }, - ); - - it('Should unlock locked nft', async () => { - let lockedNft = await helper.createLockedNft(); - await helper.mintCollection(); - const lockedNftOrder = ( - await build5Db().doc(`${COL.TRANSACTION}/${lockedNft.lockedBy}`).get() - ); - expect(lockedNftOrder.payload.void).toBe(true); - - lockedNft = await build5Db().doc(`${COL.NFT}/${lockedNft.uid}`).get(); - expect(lockedNft.locked).toBe(false); - expect(lockedNft.lockedBy).toBe(null); - expect(lockedNft.mintingData).toBeDefined(); - expect(lockedNft.status).toBe(NftStatus.MINTED); - }); -}); diff --git a/packages/functions/test-tangle/collection-minting/collection-minting_4_a.spec.ts b/packages/functions/test-tangle/collection-minting/collection-minting_4_a.spec.ts new file mode 100644 index 0000000000..21d3472552 --- /dev/null +++ b/packages/functions/test-tangle/collection-minting/collection-minting_4_a.spec.ts @@ -0,0 +1,116 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { build5Db } from '@build-5/database'; +import { + COL, + Collection, + KEY_NAME_TANGLE, + MIN_IOTA_AMOUNT, + Network, + Nft, + NftStatus, + Space, + Transaction, + TransactionType, +} from '@build-5/interfaces'; +import { getAddress } from '../../src/utils/address.utils'; +import { CollectionMintHelper, getNftMetadata } from './Helper'; + +describe('Collection minting', () => { + const helper = new CollectionMintHelper(); + + beforeAll(async () => { + await helper.beforeAll(); + }); + + beforeEach(async () => { + await helper.beforeEach(); + }); + + it('Should mint, cancel active sells, not mint placeholder', async () => { + await helper.createAndOrderNft(); + await helper.createAndOrderNft(true); + await helper.createAndOrderNft(true, true); + let placeholderNft = await helper.createAndOrderNft(true, false); + await build5Db().doc(`${COL.NFT}/${placeholderNft.uid}`).update({ placeholderNft: true }); + await build5Db() + .doc(`${COL.COLLECTION}/${helper.collection}`) + .update({ total: build5Db().inc(-1) }); + + await helper.mintCollection(); + + const bidCredit = ( + await build5Db() + .collection(COL.TRANSACTION) + .where('payload.collection', '==', helper.collection) + .where('type', '==', TransactionType.CREDIT) + .get() + ).map((d) => d); + expect(bidCredit.length).toBe(1); + expect(bidCredit[0].payload.amount).toBe(2 * MIN_IOTA_AMOUNT); + + const nftsQuery = build5Db() + .collection(COL.NFT) + .where('collection', '==', helper.collection) + .where('placeholderNft', '==', false); + const nfts = (await nftsQuery.get()).map((d) => d); + const allCancelled = nfts.reduce( + (acc, act) => + acc && + act.auctionFrom === null && + act.auctionTo === null && + act.auctionFloorPrice === null && + act.auctionLength === null && + act.auctionHighestBid === null && + act.auctionHighestBidder === null && + (!act.sold || (act.availableFrom === null && act.availablePrice === null)), + true, + ); + expect(allCancelled).toBe(true); + + const collection = ( + await build5Db().doc(`${COL.COLLECTION}/${helper.collection}`).get() + ); + const royaltySpace = ( + await build5Db().doc(`${COL.SPACE}/${collection.royaltiesSpace}`).get() + ); + + const collectionOutput = await helper.nftWallet!.getNftOutputs( + collection.mintingData?.nftId, + undefined, + ); + expect(Object.keys(collectionOutput).length).toBe(1); + const collectionMetadata = getNftMetadata(Object.values(collectionOutput)[0]); + expect(collectionMetadata.standard).toBe('IRC27'); + expect(collectionMetadata.version).toBe('v1.0'); + expect(collectionMetadata.type).toBe('image/png'); + expect(collectionMetadata.uri).toBe(`ipfs://${collection.ipfsMedia}`); + expect(collectionMetadata.description).toBe(collection.description); + expect(collectionMetadata.issuerName).toBe(KEY_NAME_TANGLE); + expect(collectionMetadata.royalties[getAddress(royaltySpace, Network.RMS)]).toBe( + collection.royaltiesFee, + ); + expect(collectionMetadata.build5Id).toBe(collection.uid); + + for (const nft of nfts) { + const nftOutputs = await helper.nftWallet!.getNftOutputs(nft.mintingData?.nftId, undefined); + expect(Object.keys(nftOutputs).length).toBe(1); + const metadata = getNftMetadata(Object.values(nftOutputs)[0]); + expect(metadata.standard).toBe('IRC27'); + expect(metadata.version).toBe('v1.0'); + expect(metadata.type).toBe('image/png'); + expect(metadata.uri).toBe(`ipfs://${nft.ipfsMedia}`); + expect(metadata.name).toBe(nft.name); + expect(metadata.description).toBe(nft.description); + expect(metadata.issuerName).toBe(KEY_NAME_TANGLE); + expect(metadata.collectionId).toBe(collection.mintingData?.nftId); + expect(metadata.collectionName).toBe(collection.name); + expect(metadata.royalties[getAddress(royaltySpace, Network.RMS)]).toBe( + collection.royaltiesFee, + ); + expect(metadata.build5Id).toBe(nft.uid); + } + + placeholderNft = await build5Db().doc(`${COL.NFT}/${placeholderNft.uid}`).get(); + expect(placeholderNft.status).toBe(NftStatus.PRE_MINTED); + }); +}); diff --git a/packages/functions/test-tangle/collection-minting/collection-minting_4_b.spec.ts b/packages/functions/test-tangle/collection-minting/collection-minting_4_b.spec.ts new file mode 100644 index 0000000000..2dfd4436b9 --- /dev/null +++ b/packages/functions/test-tangle/collection-minting/collection-minting_4_b.spec.ts @@ -0,0 +1,122 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { build5Db } from '@build-5/database'; +import { + COL, + Collection, + KEY_NAME_TANGLE, + MIN_IOTA_AMOUNT, + Network, + Nft, + NftStatus, + Space, + Transaction, + TransactionType, +} from '@build-5/interfaces'; +import { NftOutput } from '@iota/sdk'; +import { getAddress } from '../../src/utils/address.utils'; +import { EMPTY_NFT_ID } from '../../src/utils/collection-minting-utils/nft.utils'; +import { CollectionMintHelper, getNftMetadata } from './Helper'; + +describe('Collection minting', () => { + const helper = new CollectionMintHelper(); + + beforeAll(async () => { + await helper.beforeAll(); + }); + + beforeEach(async () => { + await helper.beforeEach(); + }); + + it('Should mint, cancel active sells, not mint placeholder', async () => { + await helper.createAndOrderNft(); + await helper.createAndOrderNft(true); + await helper.createAndOrderNft(true, true); + let placeholderNft = await helper.createAndOrderNft(true, false); + await build5Db().doc(`${COL.NFT}/${placeholderNft.uid}`).update({ placeholderNft: true }); + + const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${helper.collection}`); + await collectionDocRef.update({ total: build5Db().inc(-1), limitedEdition: true }); + + await helper.mintCollection(); + await helper.lockCollectionConfirmed(); + + let collection = await collectionDocRef.get(); + const outputId = await helper.walletService!.client.nftOutputId(collection.mintingData?.nftId!); + const output = (await helper.walletService!.client.getOutput(outputId)).output; + expect((output.unlockConditions[0] as any).address.pubKeyHash).toBe(EMPTY_NFT_ID); + + const bidCredit = ( + await build5Db() + .collection(COL.TRANSACTION) + .where('payload.collection', '==', helper.collection) + .where('type', '==', TransactionType.CREDIT) + .get() + ).map((d) => d); + expect(bidCredit.length).toBe(1); + expect(bidCredit[0].payload.amount).toBe(2 * MIN_IOTA_AMOUNT); + + const nftsQuery = build5Db() + .collection(COL.NFT) + .where('collection', '==', helper.collection) + .where('placeholderNft', '==', false); + const nfts = (await nftsQuery.get()).map((d) => d); + const allCancelled = nfts.reduce( + (acc, act) => + acc && + act.auctionFrom === null && + act.auctionTo === null && + act.auctionFloorPrice === null && + act.auctionLength === null && + act.auctionHighestBid === null && + act.auctionHighestBidder === null && + (!act.sold || (act.availableFrom === null && act.availablePrice === null)), + true, + ); + expect(allCancelled).toBe(true); + + collection = await collectionDocRef.get(); + const royaltySpace = ( + await build5Db().doc(`${COL.SPACE}/${collection.royaltiesSpace}`).get() + ); + + const collectionOutput = await helper.nftWallet!.getNftOutputs( + collection.mintingData?.nftId, + undefined, + ); + expect(Object.keys(collectionOutput).length).toBe(1); + const collectionMetadata = getNftMetadata(Object.values(collectionOutput)[0]); + expect(collectionMetadata.standard).toBe('IRC27'); + expect(collectionMetadata.version).toBe('v1.0'); + expect(collectionMetadata.type).toBe('image/png'); + expect(collectionMetadata.uri).toBe(`ipfs://${collection.ipfsMedia}`); + expect(collectionMetadata.description).toBe(collection.description); + expect(collectionMetadata.issuerName).toBe(KEY_NAME_TANGLE); + expect(collectionMetadata.royalties[getAddress(royaltySpace, Network.RMS)]).toBe( + collection.royaltiesFee, + ); + expect(collectionMetadata.build5Id).toBe(collection.uid); + + for (const nft of nfts) { + const nftOutputs = await helper.nftWallet!.getNftOutputs(nft.mintingData?.nftId, undefined); + expect(Object.keys(nftOutputs).length).toBe(1); + const metadata = getNftMetadata(Object.values(nftOutputs)[0]); + expect(metadata.standard).toBe('IRC27'); + expect(metadata.version).toBe('v1.0'); + expect(metadata.type).toBe('image/png'); + expect(metadata.uri).toBe(`ipfs://${nft.ipfsMedia}`); + expect(metadata.name).toBe(nft.name); + expect(metadata.description).toBe(nft.description); + expect(metadata.issuerName).toBe(KEY_NAME_TANGLE); + expect(metadata.collectionId).toBe(collection.mintingData?.nftId); + expect(metadata.collectionName).toBe(collection.name); + expect(metadata.royalties[getAddress(royaltySpace, Network.RMS)]).toBe( + collection.royaltiesFee, + ); + expect(metadata.build5Id).toBe(nft.uid); + } + + placeholderNft = await build5Db().doc(`${COL.NFT}/${placeholderNft.uid}`).get(); + expect(placeholderNft.status).toBe(NftStatus.PRE_MINTED); + }); +}); diff --git a/packages/functions/test-tangle/collection-minting/collection-minting_4_c.spec.ts b/packages/functions/test-tangle/collection-minting/collection-minting_4_c.spec.ts new file mode 100644 index 0000000000..7acc45bb8f --- /dev/null +++ b/packages/functions/test-tangle/collection-minting/collection-minting_4_c.spec.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { build5Db } from '@build-5/database'; +import { COL, Nft, NftStatus, Transaction } from '@build-5/interfaces'; +import { CollectionMintHelper } from './Helper'; + +describe('Collection minting', () => { + const helper = new CollectionMintHelper(); + + beforeAll(async () => { + await helper.beforeAll(); + }); + + beforeEach(async () => { + await helper.beforeEach(); + }); + + it('Should unlock locked nft', async () => { + let lockedNft = await helper.createLockedNft(); + await helper.mintCollection(); + const lockedNftOrder = ( + await build5Db().doc(`${COL.TRANSACTION}/${lockedNft.lockedBy}`).get() + ); + expect(lockedNftOrder.payload.void).toBe(true); + + lockedNft = await build5Db().doc(`${COL.NFT}/${lockedNft.uid}`).get(); + expect(lockedNft.locked).toBe(false); + expect(lockedNft.lockedBy).toBe(null); + expect(lockedNft.mintingData).toBeDefined(); + expect(lockedNft.status).toBe(NftStatus.MINTED); + }); +}); diff --git a/packages/functions/test-tangle/minted-nft-trading/minted-nft-trading_1.spec.ts b/packages/functions/test-tangle/minted-nft-trading/minted-nft-trading_1.spec.ts index 7c07255739..8f398471d4 100644 --- a/packages/functions/test-tangle/minted-nft-trading/minted-nft-trading_1.spec.ts +++ b/packages/functions/test-tangle/minted-nft-trading/minted-nft-trading_1.spec.ts @@ -13,9 +13,8 @@ import { import { NftOutput } from '@iota/sdk'; import dayjs from 'dayjs'; import { isEmpty } from 'lodash'; -import { finalizeAllNftAuctions } from '../../src/cron/nft.cron'; -import { openBid } from '../../src/runtime/firebase/nft'; -import { withdrawNft } from '../../src/runtime/firebase/nft/index'; +import { finalizeAuctions } from '../../src/cron/auction.cron'; +import { openBid, withdrawNft } from '../../src/runtime/firebase/nft/index'; import { getAddress } from '../../src/utils/address.utils'; import { Bech32AddressHelper } from '../../src/utils/bech32-address.helper'; import { dateToTimestamp } from '../../src/utils/dateTime.utils'; @@ -27,94 +26,90 @@ import { Helper } from './Helper'; describe('Minted nft trading', () => { const helper = new Helper(); - it.each([false, true])( - 'Should bid twice on minted nft and withdraw it', - async (hasExpiration: boolean) => { - await helper.beforeEach(Network.RMS); - await helper.createAndOrderNft(); - await helper.mintCollection(); - - await helper.setAvailableForAuction(); - - mockWalletReturnValue(helper.walletSpy, helper.member!, { nft: helper.nft!.uid }); - await expectThrow(testEnv.wrap(withdrawNft)({}), WenError.you_must_be_the_owner_of_nft.key); - - const expiresAt = hasExpiration ? dateToTimestamp(dayjs().add(2, 'h').toDate()) : undefined; - - mockWalletReturnValue(helper.walletSpy, helper.member!, { nft: helper.nft!.uid }); - const bidOrder = await testEnv.wrap(openBid)({}); - await requestFundsFromFaucet( - Network.RMS, - bidOrder.payload.targetAddress, - MIN_IOTA_AMOUNT, - expiresAt, - ); - - await wait(async () => { - const nft = await build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`).get(); - return !isEmpty(nft.auctionHighestTransaction); - }); - - const bidOrder2 = await testEnv.wrap(openBid)({}); - await requestFundsFromFaucet( - Network.RMS, - bidOrder2.payload.targetAddress, - 2 * MIN_IOTA_AMOUNT, - expiresAt, - ); - - await wait(async () => { - const nft = await build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`).get(); - - const payment = ( - await build5Db() - .collection(COL.TRANSACTION) - .where('type', '==', TransactionType.PAYMENT) - .where('payload.sourceTransaction', 'array-contains', bidOrder2.uid) - .get() - )[0]; - return nft.auctionHighestTransaction === payment?.uid; - }); - - await build5Db() - .doc(`${COL.NFT}/${helper.nft!.uid}`) - .update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'm').toDate()) }); - - await finalizeAllNftAuctions(); - - await wait(async () => { - const nft = await build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`).get(); - return nft.owner === helper.member; - }); - - mockWalletReturnValue(helper.walletSpy, helper.member!, { nft: helper.nft!.uid }); - await testEnv.wrap(withdrawNft)({}); - - const nft = await build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`).get(); - expect(nft.status).toBe(NftStatus.WITHDRAWN); - - await wait(async () => { - const transaction = ( - await build5Db() - .collection(COL.TRANSACTION) - .where('type', '==', TransactionType.WITHDRAW_NFT) - .where('payload.nft', '==', helper.nft!.uid) - .get() - )[0]; - return transaction?.payload?.walletReference?.confirmed; - }); - - const output = ( - await helper.walletService!.client.getOutput( - await helper.walletService!.client.nftOutputId(nft.mintingData?.nftId!), - ) - ).output; - const ownerAddress = Bech32AddressHelper.bech32FromUnlockConditions( - output as NftOutput, - 'rms', - ); - const member = await build5Db().doc(`${COL.MEMBER}/${helper.member}`).get(); - expect(ownerAddress).toBe(getAddress(member, Network.RMS)); - }, - ); + it('Should bid twice on minted nft and withdraw it', async () => { + await helper.beforeEach(Network.RMS); + await helper.createAndOrderNft(); + await helper.mintCollection(); + + await helper.setAvailableForAuction(); + + const nftDocRef = build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`); + + mockWalletReturnValue(helper.walletSpy, helper.member!, { nft: helper.nft!.uid }); + await expectThrow(testEnv.wrap(withdrawNft)({}), WenError.you_must_be_the_owner_of_nft.key); + + const expiresAt = dateToTimestamp(dayjs().add(2, 'h').toDate()); + + mockWalletReturnValue(helper.walletSpy, helper.member!, { nft: helper.nft!.uid }); + const bidOrder = await testEnv.wrap(openBid)({}); + await requestFundsFromFaucet( + Network.RMS, + bidOrder.payload.targetAddress, + MIN_IOTA_AMOUNT, + expiresAt, + ); + + await wait(async () => { + helper.nft = await nftDocRef.get(); + return !isEmpty(helper.nft.auctionHighestBidder); + }); + + const bidOrder2 = await testEnv.wrap(openBid)({}); + await requestFundsFromFaucet( + Network.RMS, + bidOrder2.payload.targetAddress, + 2 * MIN_IOTA_AMOUNT, + expiresAt, + ); + + await wait(async () => { + const nft = await nftDocRef.get(); + + const payment = ( + await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.PAYMENT) + .where('payload.sourceTransaction', 'array-contains', bidOrder2.uid) + .get() + )[0]; + return nft.auctionHighestBidder === payment?.member; + }); + + await build5Db() + .doc(`${COL.AUCTION}/${helper.nft!.auction}`) + .update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'm').toDate()) }); + + await finalizeAuctions(); + + await wait(async () => { + const nft = await nftDocRef.get(); + return nft.owner === helper.member; + }); + + mockWalletReturnValue(helper.walletSpy, helper.member!, { nft: helper.nft!.uid }); + await testEnv.wrap(withdrawNft)({}); + + helper.nft = await nftDocRef.get(); + expect(helper.nft.status).toBe(NftStatus.WITHDRAWN); + + await wait(async () => { + const transaction = ( + await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.WITHDRAW_NFT) + .where('payload.nft', '==', helper.nft!.uid) + .get() + )[0]; + return transaction?.payload?.walletReference?.confirmed; + }); + + const output = ( + await helper.walletService!.client.getOutput( + await helper.walletService!.client.nftOutputId(helper.nft.mintingData?.nftId!), + ) + ).output; + const ownerAddress = Bech32AddressHelper.bech32FromUnlockConditions(output as NftOutput, 'rms'); + const member = await build5Db().doc(`${COL.MEMBER}/${helper.member}`).get(); + expect(ownerAddress).toBe(getAddress(member, Network.RMS)); + }); }); diff --git a/packages/functions/test-tangle/minted-nft-trading/minted-nft-trading_1_b.spec.ts b/packages/functions/test-tangle/minted-nft-trading/minted-nft-trading_1_b.spec.ts index 39640ac8dd..914d625d6e 100644 --- a/packages/functions/test-tangle/minted-nft-trading/minted-nft-trading_1_b.spec.ts +++ b/packages/functions/test-tangle/minted-nft-trading/minted-nft-trading_1_b.spec.ts @@ -13,9 +13,8 @@ import { import { NftOutput } from '@iota/sdk'; import dayjs from 'dayjs'; import { isEmpty } from 'lodash'; -import { finalizeAllNftAuctions } from '../../src/cron/nft.cron'; -import { openBid } from '../../src/runtime/firebase/nft'; -import { withdrawNft } from '../../src/runtime/firebase/nft/index'; +import { finalizeAuctions } from '../../src/cron/auction.cron'; +import { openBid, withdrawNft } from '../../src/runtime/firebase/nft/index'; import { getAddress } from '../../src/utils/address.utils'; import { Bech32AddressHelper } from '../../src/utils/bech32-address.helper'; import { dateToTimestamp } from '../../src/utils/dateTime.utils'; @@ -27,91 +26,78 @@ import { Helper } from './Helper'; describe('Minted nft trading', () => { const helper = new Helper(); - it.each([false])( - 'Should bid twice on minted nft and withdraw it', - async (hasExpiration: boolean) => { - await helper.beforeEach(Network.ATOI); - await helper.createAndOrderNft(); - await helper.mintCollection(); - - await helper.setAvailableForAuction(); - - mockWalletReturnValue(helper.walletSpy, helper.member!, { nft: helper.nft!.uid }); - await expectThrow(testEnv.wrap(withdrawNft)({}), WenError.you_must_be_the_owner_of_nft.key); - - const expiresAt = hasExpiration ? dateToTimestamp(dayjs().add(2, 'h').toDate()) : undefined; - - mockWalletReturnValue(helper.walletSpy, helper.member!, { nft: helper.nft!.uid }); - const bidOrder = await testEnv.wrap(openBid)({}); - await requestFundsFromFaucet( - Network.RMS, - bidOrder.payload.targetAddress, - MIN_IOTA_AMOUNT, - expiresAt, - ); - - await wait(async () => { - const nft = await build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`).get(); - return !isEmpty(nft.auctionHighestTransaction); - }); - - const bidOrder2 = await testEnv.wrap(openBid)({}); - await requestFundsFromFaucet( - Network.RMS, - bidOrder2.payload.targetAddress, - 2 * MIN_IOTA_AMOUNT, - expiresAt, - ); - - await wait(async () => { - const nft = await build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`).get(); - - const payment = ( - await build5Db() - .collection(COL.TRANSACTION) - .where('type', '==', TransactionType.PAYMENT) - .where('payload.sourceTransaction', 'array-contains', bidOrder2.uid) - .get() - )[0]; - return nft.auctionHighestTransaction === payment?.uid; - }); - - await build5Db() - .doc(`${COL.NFT}/${helper.nft!.uid}`) - .update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'm').toDate()) }); - - await finalizeAllNftAuctions(); - - await wait(async () => { - const nft = await build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`).get(); - return nft.owner === helper.member; - }); - - mockWalletReturnValue(helper.walletSpy, helper.member!, { nft: helper.nft!.uid }); - await testEnv.wrap(withdrawNft)({}); - - const nft = await build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`).get(); - expect(nft.status).toBe(NftStatus.WITHDRAWN); - - await wait(async () => { - const transaction = ( - await build5Db() - .collection(COL.TRANSACTION) - .where('type', '==', TransactionType.WITHDRAW_NFT) - .where('payload.nft', '==', helper.nft!.uid) - .get() - )[0]; - return transaction?.payload?.walletReference?.confirmed; - }); - - const output = ( - await helper.walletService!.client.getOutput( - await helper.walletService!.client.nftOutputId(nft.mintingData?.nftId!), - ) - ).output as NftOutput; - const ownerAddress = Bech32AddressHelper.bech32FromUnlockConditions(output, 'rms'); - const member = await build5Db().doc(`${COL.MEMBER}/${helper.member}`).get(); - expect(ownerAddress).toBe(getAddress(member, Network.ATOI)); - }, - ); + it('Should bid twice on minted nft and withdraw it', async () => { + await helper.beforeEach(Network.ATOI); + await helper.createAndOrderNft(); + await helper.mintCollection(); + + await helper.setAvailableForAuction(); + + const nftDocRef = build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`); + + mockWalletReturnValue(helper.walletSpy, helper.member!, { nft: helper.nft!.uid }); + await expectThrow(testEnv.wrap(withdrawNft)({}), WenError.you_must_be_the_owner_of_nft.key); + + mockWalletReturnValue(helper.walletSpy, helper.member!, { nft: helper.nft!.uid }); + const bidOrder = await testEnv.wrap(openBid)({}); + await requestFundsFromFaucet(Network.RMS, bidOrder.payload.targetAddress, MIN_IOTA_AMOUNT); + + await wait(async () => { + helper.nft = await nftDocRef.get(); + return !isEmpty(helper.nft.auctionHighestBidder); + }); + + const bidOrder2 = await testEnv.wrap(openBid)({}); + await requestFundsFromFaucet(Network.RMS, bidOrder2.payload.targetAddress, 2 * MIN_IOTA_AMOUNT); + + await wait(async () => { + helper.nft = await nftDocRef.get(); + + const payment = ( + await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.PAYMENT) + .where('payload.sourceTransaction', 'array-contains', bidOrder2.uid) + .get() + )[0]; + return helper.nft.auctionHighestBidder === payment?.member; + }); + + await build5Db() + .doc(`${COL.AUCTION}/${helper.nft!.auction}`) + .update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'm').toDate()) }); + + await finalizeAuctions(); + + await wait(async () => { + const nft = await nftDocRef.get(); + return nft.owner === helper.member; + }); + + mockWalletReturnValue(helper.walletSpy, helper.member!, { nft: helper.nft!.uid }); + await testEnv.wrap(withdrawNft)({}); + + helper.nft = await nftDocRef.get(); + expect(helper.nft.status).toBe(NftStatus.WITHDRAWN); + + await wait(async () => { + const transaction = ( + await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.WITHDRAW_NFT) + .where('payload.nft', '==', helper.nft!.uid) + .get() + )[0]; + return transaction?.payload?.walletReference?.confirmed; + }); + + const output = ( + await helper.walletService!.client.getOutput( + await helper.walletService!.client.nftOutputId(helper.nft.mintingData?.nftId!), + ) + ).output as NftOutput; + const ownerAddress = Bech32AddressHelper.bech32FromUnlockConditions(output, 'rms'); + const member = await build5Db().doc(`${COL.MEMBER}/${helper.member}`).get(); + expect(ownerAddress).toBe(getAddress(member, Network.ATOI)); + }); }); diff --git a/packages/functions/test-tangle/nft-bid/Helper.ts b/packages/functions/test-tangle/nft-bid/Helper.ts index ca84da1980..d22950fc44 100644 --- a/packages/functions/test-tangle/nft-bid/Helper.ts +++ b/packages/functions/test-tangle/nft-bid/Helper.ts @@ -126,7 +126,11 @@ export class Helper { const uid = nft || this.nft?.uid!; mockWalletReturnValue(this.walletSpy, this.guardian!, this.dummyAuctionData(uid)); await testEnv.wrap(setForSaleNft)({}); - await wait(async () => (await build5Db().doc(`${COL.NFT}/${uid}`).get())?.available === 3); + await wait(async () => { + const docRef = build5Db().doc(`${COL.NFT}/${uid}`); + this.nft = await docRef.get(); + return this.nft.available === 3; + }); }; public setAvailableForSale = async () => { diff --git a/packages/functions/test-tangle/nft-bid/nft-bid.otr_1.spec.ts b/packages/functions/test-tangle/nft-bid/nft-bid.otr_1.spec.ts index b38e75a74f..9c7f4e55d1 100644 --- a/packages/functions/test-tangle/nft-bid/nft-bid.otr_1.spec.ts +++ b/packages/functions/test-tangle/nft-bid/nft-bid.otr_1.spec.ts @@ -12,7 +12,7 @@ import { import { NftOutput } from '@iota/sdk'; import dayjs from 'dayjs'; import { isEmpty } from 'lodash'; -import { finalizeAllNftAuctions } from '../../src/cron/nft.cron'; +import { finalizeAuctions } from '../../src/cron/auction.cron'; import { Bech32AddressHelper } from '../../src/utils/bech32-address.helper'; import { dateToTimestamp } from '../../src/utils/dateTime.utils'; import { wait } from '../../test/controls/common'; @@ -53,14 +53,14 @@ describe('Nft otr bid', () => { const nftDocRef = build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`); await wait(async () => { const nft = await nftDocRef.get(); - return !isEmpty(nft?.auctionHighestTransaction); + return !isEmpty(nft?.auctionHighestBidder); }); await build5Db() - .doc(`${COL.NFT}/${helper.nft!.uid}`) + .doc(`${COL.AUCTION}/${helper.nft!.auction}`) .update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'm').toDate()) }); - await finalizeAllNftAuctions(); + await finalizeAuctions(); await wait(async () => { const transaction = ( diff --git a/packages/functions/test-tangle/nft-bid/nft-bid.otr_2.spec.ts b/packages/functions/test-tangle/nft-bid/nft-bid.otr_2.spec.ts index 8dc5d0e5ce..fc3ff949dc 100644 --- a/packages/functions/test-tangle/nft-bid/nft-bid.otr_2.spec.ts +++ b/packages/functions/test-tangle/nft-bid/nft-bid.otr_2.spec.ts @@ -10,7 +10,7 @@ import { } from '@build-5/interfaces'; import dayjs from 'dayjs'; import { isEmpty } from 'lodash'; -import { finalizeAllNftAuctions } from '../../src/cron/nft.cron'; +import { finalizeAuctions } from '../../src/cron/auction.cron'; import { dateToTimestamp } from '../../src/utils/dateTime.utils'; import { wait } from '../../test/controls/common'; import { getTangleOrder } from '../common'; @@ -51,14 +51,14 @@ describe('Nft otr bid', () => { const nftDocRef = build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`); await wait(async () => { const nft = await nftDocRef.get(); - return !isEmpty(nft?.auctionHighestTransaction); + return !isEmpty(nft?.auctionHighestBidder); }); await build5Db() - .doc(`${COL.NFT}/${helper.nft!.uid}`) + .doc(`${COL.AUCTION}/${helper.nft!.auction}`) .update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'm').toDate()) }); - await finalizeAllNftAuctions(); + await finalizeAuctions(); await wait(async () => { const nft = await build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`).get(); diff --git a/packages/functions/test-tangle/nft-bid/nft-bid.otr_3.spec.ts b/packages/functions/test-tangle/nft-bid/nft-bid.otr_3.spec.ts index 33eeecf52d..c499b3abb8 100644 --- a/packages/functions/test-tangle/nft-bid/nft-bid.otr_3.spec.ts +++ b/packages/functions/test-tangle/nft-bid/nft-bid.otr_3.spec.ts @@ -10,7 +10,7 @@ import { } from '@build-5/interfaces'; import dayjs from 'dayjs'; import { isEmpty } from 'lodash'; -import { finalizeAllNftAuctions } from '../../src/cron/nft.cron'; +import { finalizeAuctions } from '../../src/cron/auction.cron'; import { IotaWallet } from '../../src/services/wallet/IotaWalletService'; import { MnemonicService } from '../../src/services/wallet/mnemonic'; import { dateToTimestamp } from '../../src/utils/dateTime.utils'; @@ -53,14 +53,14 @@ describe('Nft otr bid', () => { const nftDocRef = build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`); await wait(async () => { const nft = await nftDocRef.get(); - return !isEmpty(nft?.auctionHighestTransaction); + return !isEmpty(nft?.auctionHighestBidder); }); await build5Db() - .doc(`${COL.NFT}/${helper.nft!.uid}`) + .doc(`${COL.AUCTION}/${helper.nft!.auction}`) .update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'm').toDate()) }); - await finalizeAllNftAuctions(); + await finalizeAuctions(); await wait(async () => { const nft = await build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`).get(); diff --git a/packages/functions/test-tangle/nft-bid/nft-bid.otr_4.spec.ts b/packages/functions/test-tangle/nft-bid/nft-bid.otr_4.spec.ts index 75485ce515..7fdffd94b0 100644 --- a/packages/functions/test-tangle/nft-bid/nft-bid.otr_4.spec.ts +++ b/packages/functions/test-tangle/nft-bid/nft-bid.otr_4.spec.ts @@ -10,7 +10,7 @@ import { TransactionType, } from '@build-5/interfaces'; import dayjs from 'dayjs'; -import { finalizeAllNftAuctions } from '../../src/cron/nft.cron'; +import { finalizeAuctions } from '../../src/cron/auction.cron'; import { dateToTimestamp } from '../../src/utils/dateTime.utils'; import { wait } from '../../test/controls/common'; import { getTangleOrder } from '../common'; @@ -62,19 +62,19 @@ describe('Nft otr bid', () => { const nftDocRef = build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`); await wait(async () => { - const nft = await nftDocRef.get(); - return nft?.auctionHighestBidder === address2.bech32; + helper.nft = await nftDocRef.get(); + return helper.nft.auctionHighestBidder === address2.bech32; }); await build5Db() - .doc(`${COL.NFT}/${helper.nft!.uid}`) + .doc(`${COL.AUCTION}/${helper.nft!.auction}`) .update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'm').toDate()) }); - await finalizeAllNftAuctions(); + await finalizeAuctions(); await wait(async () => { - const nft = await build5Db().doc(`${COL.NFT}/${helper.nft!.uid}`).get(); - return nft.owner === address2.bech32; + helper.nft = await nftDocRef.get(); + return helper.nft.owner === address2.bech32; }); const creditQuery = build5Db() diff --git a/packages/functions/test-tangle/nft-set-for-sale/nft-set-for-sale_3.spec.ts b/packages/functions/test-tangle/nft-set-for-sale/nft-set-for-sale_3.spec.ts index ee6c5fa6d2..f599bffd40 100644 --- a/packages/functions/test-tangle/nft-set-for-sale/nft-set-for-sale_3.spec.ts +++ b/packages/functions/test-tangle/nft-set-for-sale/nft-set-for-sale_3.spec.ts @@ -18,7 +18,7 @@ import { getTangleOrder } from '../common'; import { requestFundsFromFaucet } from '../faucet'; import { Helper } from './Helper'; -describe('Nft set for acution OTR', () => { +describe('Nft set for auction OTR', () => { const helper = new Helper(); let tangleOrder: Transaction; @@ -112,7 +112,7 @@ describe('Nft set for acution OTR', () => { }); const snap = await credit.get(); const creditTransction = snap.find( - (t) => t.payload.response?.code === WenError.nft_auction_already_in_progress.code, + (t) => t.payload.response?.code === WenError.auction_already_in_progress.code, ); expect(creditTransction).toBeDefined(); }); diff --git a/packages/functions/test-tangle/withdraw-deposit-nft/deposit-withraw-nft_4.spec.ts b/packages/functions/test-tangle/withdraw-deposit-nft/deposit-withraw-nft_4.spec.ts index 046f9ae352..fd4b512e50 100644 --- a/packages/functions/test-tangle/withdraw-deposit-nft/deposit-withraw-nft_4.spec.ts +++ b/packages/functions/test-tangle/withdraw-deposit-nft/deposit-withraw-nft_4.spec.ts @@ -32,7 +32,6 @@ describe('Collection minting', () => { auctionLength: null, auctionHighestBid: null, auctionHighestBidder: null, - auctionHighestTransaction: null, }); await helper.setAvailableForAuction(); diff --git a/packages/functions/test/controls/auction/Helper.ts b/packages/functions/test/controls/auction/Helper.ts new file mode 100644 index 0000000000..b5ff5c1c00 --- /dev/null +++ b/packages/functions/test/controls/auction/Helper.ts @@ -0,0 +1,47 @@ +import { IDocument, build5Db } from '@build-5/database'; +import { Auction, COL, MIN_IOTA_AMOUNT, Network } from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { auctionCreate, bidAuction } from '../../../src/runtime/firebase/auction/index'; +import * as wallet from '../../../src/utils/wallet.utils'; +import { testEnv } from '../../set-up'; +import { createMember, mockWalletReturnValue, submitMilestoneFunc } from '../common'; + +export class Helper { + public spy: any = {} as any; + public member: string = {} as any; + public members: string[] = []; + public auction: Auction = {} as any; + public auctionDocRef: IDocument = {} as any; + + public beforeAll = async () => { + this.spy = jest.spyOn(wallet, 'decodeAuth'); + }; + + public beforeEach = async (now: dayjs.Dayjs) => { + this.member = await createMember(this.spy); + const memberPromises = Array.from(Array(3)).map(() => createMember(this.spy)); + this.members = await Promise.all(memberPromises); + + mockWalletReturnValue(this.spy, this.member, auctionRequest(now)); + this.auction = await testEnv.wrap(auctionCreate)({}); + this.auctionDocRef = build5Db().doc(`${COL.AUCTION}/${this.auction.uid}`); + }; + + public bidOnAuction = async (memberId: string, amount: number) => { + mockWalletReturnValue(this.spy, memberId, { auction: this.auction.uid }); + const bidOrder = await testEnv.wrap(bidAuction)({}); + await submitMilestoneFunc(bidOrder, amount); + return bidOrder; + }; +} + +const auctionRequest = (now: dayjs.Dayjs, auctionLength = 60000 * 4) => ({ + auctionFrom: now.toDate(), + auctionFloorPrice: 2 * MIN_IOTA_AMOUNT, + auctionLength, + extendedAuctionLength: auctionLength + 6000, + extendAuctionWithin: 60000 * 4, + maxBids: 2, + network: Network.RMS, + topUpBased: true, +}); diff --git a/packages/functions/test/controls/auction/auction.bid.spec.ts b/packages/functions/test/controls/auction/auction.bid.spec.ts new file mode 100644 index 0000000000..5526557426 --- /dev/null +++ b/packages/functions/test/controls/auction/auction.bid.spec.ts @@ -0,0 +1,91 @@ +import { build5Db } from '@build-5/database'; +import { + Auction, + AuctionType, + COL, + MIN_IOTA_AMOUNT, + Network, + Transaction, + TransactionType, +} from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { finalizeAuctions } from '../../../src/cron/auction.cron'; +import { dateToTimestamp } from '../../../src/utils/dateTime.utils'; +import { Helper } from './Helper'; + +describe('Open auction bid', () => { + const h = new Helper(); + const now = dayjs(); + + beforeAll(async () => { + await h.beforeAll(); + }); + + beforeEach(async () => { + await h.beforeEach(now); + }); + + it('Should create auction', async () => { + expect(dayjs(h.auction.auctionFrom.toDate()).isSame(now)).toBe(true); + expect(dayjs(h.auction.auctionTo.toDate()).isSame(now.add(60000 * 4))); + expect(h.auction.auctionLength).toBe(60000 * 4); + + expect(dayjs(h.auction.extendedAuctionTo?.toDate()).isSame(now.add(60000 * 4 + 6000))).toBe( + true, + ); + expect(h.auction.extendedAuctionLength).toBe(60000 * 4 + 6000); + expect(h.auction.extendAuctionWithin).toBe(60000 * 4); + + expect(h.auction.auctionFloorPrice).toBe(2 * MIN_IOTA_AMOUNT); + expect(h.auction.maxBids).toBe(2); + expect(h.auction.type).toBe(AuctionType.OPEN); + expect(h.auction.network).toBe(Network.RMS); + expect(h.auction.nftId).toBeUndefined(); + expect(h.auction.active).toBe(true); + expect(h.auction.topUpBased).toBe(true); + }); + + it('Should bid on auction', async () => { + await h.bidOnAuction(h.members[0], 2 * MIN_IOTA_AMOUNT); + await h.bidOnAuction(h.members[1], 3 * MIN_IOTA_AMOUNT); + await h.bidOnAuction(h.members[0], 2 * MIN_IOTA_AMOUNT); + + h.auction = await h.auctionDocRef.get(); + expect(h.auction.bids.length).toBe(2); + expect(h.auction.bids[0].amount).toBe(4 * MIN_IOTA_AMOUNT); + expect(h.auction.bids[0].bidder).toBe(h.members[0]); + expect(h.auction.bids[1].amount).toBe(3 * MIN_IOTA_AMOUNT); + expect(h.auction.bids[1].bidder).toBe(h.members[1]); + + expect(h.auction.auctionHighestBidder).toBe(h.members[0]); + expect(h.auction.auctionHighestBid).toBe(4 * MIN_IOTA_AMOUNT); + + await h.bidOnAuction(h.members[2], 5 * MIN_IOTA_AMOUNT); + + h.auction = await h.auctionDocRef.get(); + expect(h.auction.bids.length).toBe(2); + expect(h.auction.bids[0].amount).toBe(5 * MIN_IOTA_AMOUNT); + expect(h.auction.bids[0].bidder).toBe(h.members[2]); + expect(h.auction.bids[1].amount).toBe(4 * MIN_IOTA_AMOUNT); + expect(h.auction.bids[1].bidder).toBe(h.members[0]); + expect(h.auction.auctionHighestBidder).toBe(h.members[2]); + expect(h.auction.auctionHighestBid).toBe(5 * MIN_IOTA_AMOUNT); + + const credits = await build5Db() + .collection(COL.TRANSACTION) + .where('member', 'in', h.members) + .where('type', '==', TransactionType.CREDIT) + .get(); + expect(credits.length).toBe(1); + expect(credits[0].member).toBe(h.members[1]); + }); + + it('Should finalize auction', async () => { + await h.bidOnAuction(h.members[0], 2 * MIN_IOTA_AMOUNT); + + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${h.auction.uid}`); + await auctionDocRef.update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'minute')) }); + + await finalizeAuctions(); + }); +}); diff --git a/packages/functions/test/controls/nft-bidding.spec.ts b/packages/functions/test/controls/nft-bidding.spec.ts deleted file mode 100644 index 4157006f32..0000000000 --- a/packages/functions/test/controls/nft-bidding.spec.ts +++ /dev/null @@ -1,513 +0,0 @@ -import { build5Db, IDocument } from '@build-5/database'; -import { - Access, - Categories, - COL, - Collection, - CollectionType, - MIN_IOTA_AMOUNT, - NetworkAddress, - Nft, - NftAccess, - NftAvailable, - NotificationType, - Space, - Transaction, - TransactionPayloadType, - TransactionType, - TransactionValidationType, - WenError, -} from '@build-5/interfaces'; -import dayjs from 'dayjs'; -import { set } from 'lodash'; -import { finalizeAllNftAuctions } from '../../src/cron/nft.cron'; -import { approveCollection, createCollection } from '../../src/runtime/firebase/collection/index'; -import { openBid } from '../../src/runtime/firebase/nft'; -import { createNft, orderNft, setForSaleNft } from '../../src/runtime/firebase/nft/index'; -import { dateToTimestamp } from '../../src/utils/dateTime.utils'; -import * as wallet from '../../src/utils/wallet.utils'; -import { MEDIA, testEnv } from '../set-up'; -import { - createMember, - createSpace, - expectThrow, - mockWalletReturnValue, - submitMilestoneFunc, - wait, -} from './common'; - -let walletSpy: any; - -const dummyNft = (collection: string, description = 'babba') => ({ - name: 'Collection A', - description, - collection, - availableFrom: dayjs().toDate(), - price: 10 * 1000 * 1000, -}); - -const submitOrderFunc = async (address: NetworkAddress, params: T) => { - mockWalletReturnValue(walletSpy, address, params); - return await testEnv.wrap(orderNft)({}); -}; - -const dummyAuctionData = (uid: string, auctionLength = 60000 * 4, from: dayjs.Dayjs = dayjs()) => ({ - nft: uid, - price: MIN_IOTA_AMOUNT, - availableFrom: from.toDate(), - auctionFrom: from.toDate(), - auctionFloorPrice: MIN_IOTA_AMOUNT, - auctionLength, - access: NftAccess.OPEN, -}); - -const dummySaleData = (uid: string) => ({ - nft: uid, - price: MIN_IOTA_AMOUNT, - availableFrom: dayjs().toDate(), - access: NftAccess.OPEN, -}); - -const bidNft = async (memberId: string, amount: number) => { - mockWalletReturnValue(walletSpy, memberId, { nft: nft.uid }); - const bidOrder = await testEnv.wrap(openBid)({}); - await submitMilestoneFunc(bidOrder, amount); - return bidOrder; -}; - -let memberAddress: NetworkAddress; -let members: string[]; -let space: Space; -let collection: Collection; -let nft: any; - -beforeEach(async () => { - walletSpy = jest.spyOn(wallet, 'decodeAuth'); - memberAddress = await createMember(walletSpy); - const memberPromises = Array.from(Array(3)).map(() => createMember(walletSpy)); - members = await Promise.all(memberPromises); - space = await createSpace(walletSpy, memberAddress); - - mockWalletReturnValue(walletSpy, memberAddress, { - name: 'Collection A', - description: 'babba', - type: CollectionType.CLASSIC, - royaltiesFee: 0.6, - category: Categories.ART, - access: Access.OPEN, - space: space.uid, - royaltiesSpace: space.uid, - onePerMemberOnly: false, - availableFrom: dayjs().toDate(), - price: 10 * 1000 * 1000, - }); - - collection = await testEnv.wrap(createCollection)({}); - mockWalletReturnValue(walletSpy, memberAddress, { uid: collection.uid }); - await testEnv.wrap(approveCollection)({}); - - mockWalletReturnValue(walletSpy, memberAddress, { media: MEDIA, ...dummyNft(collection.uid) }); - nft = await testEnv.wrap(createNft)({}); - - const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${collection.uid}`); - await wait(async () => { - collection = await collectionDocRef.get(); - return collection.availableNfts === 1; - }); - - const nftOrder = await submitOrderFunc(memberAddress, { - collection: collection.uid, - nft: nft.uid, - }); - await submitMilestoneFunc(nftOrder); - await wait(async () => { - collection = await collectionDocRef.get(); - return collection.availableNfts === 0; - }); -}); - -describe('Nft controller: setForSale', () => { - it('Should set nft for sale', async () => { - mockWalletReturnValue(walletSpy, memberAddress, dummySaleData(nft.uid)); - await testEnv.wrap(setForSaleNft)({}); - - const nftDocRef = build5Db().doc(`${COL.NFT}/${nft.uid}`); - await wait(async () => { - const nft = await nftDocRef.get(); - return nft.available === 1; - }); - - const saleNft = await nftDocRef.get(); - expect(saleNft.available).toBe(1); - expect(saleNft.availableFrom).toBeDefined(); - - const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${saleNft.collection}`); - const collection = await collectionDocRef.get(); - expect(collection.nftsOnAuction).toBe(0); - expect(collection.nftsOnSale).toBe(1); - }); - - it('Should throw, nft set as avatar', async () => { - const nftDocRef = build5Db().doc(`${COL.NFT}/${nft.uid}`); - await nftDocRef.update({ setAsAvatar: true }); - - mockWalletReturnValue(walletSpy, memberAddress, dummySaleData(nft.uid)); - await expectThrow(testEnv.wrap(setForSaleNft)({}), WenError.nft_set_as_avatar.key); - }); - - it('Should set nft for auction', async () => { - mockWalletReturnValue(walletSpy, memberAddress, dummyAuctionData(nft.uid)); - await testEnv.wrap(setForSaleNft)({}); - - const nftDocRef = build5Db().doc(`${COL.NFT}/${nft.uid}`); - await wait(async () => { - const nft = await nftDocRef.get(); - return nft.available === 3; - }); - - const auctionNft = await build5Db().doc(`${COL.NFT}/${nft.uid}`).get(); - expect(auctionNft.available).toBe(3); - expect(auctionNft.auctionFrom).toBeDefined(); - expect(auctionNft.auctionTo).toBeDefined(); - expect(auctionNft.auctionLength).toBeDefined(); - - const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${auctionNft.collection}`); - const collection = await collectionDocRef.get(); - expect(collection.nftsOnAuction).toBe(1); - expect(collection.nftsOnSale).toBe(1); - }); - - it('Should throw, auction already in progress', async () => { - mockWalletReturnValue(walletSpy, memberAddress, dummyAuctionData(nft.uid)); - await testEnv.wrap(setForSaleNft)({}); - mockWalletReturnValue(walletSpy, memberAddress, dummyAuctionData(nft.uid)); - await expectThrow( - testEnv.wrap(setForSaleNft)({}), - WenError.nft_auction_already_in_progress.key, - ); - }); - - it('Should throw, invalid nft', async () => { - mockWalletReturnValue(walletSpy, memberAddress, { - ...dummyAuctionData(nft.uid), - nft: wallet.getRandomEthAddress(), - }); - await expectThrow(testEnv.wrap(setForSaleNft)({}), WenError.nft_does_not_exists.key); - }); - - it('Should throw, not owner', async () => { - mockWalletReturnValue(walletSpy, members[0], dummyAuctionData(nft.uid)); - await expectThrow(testEnv.wrap(setForSaleNft)({}), WenError.you_must_be_the_owner_of_nft.key); - }); -}); - -describe('Nft bidding', () => { - beforeEach(async () => { - mockWalletReturnValue(walletSpy, memberAddress, dummyAuctionData(nft.uid)); - await testEnv.wrap(setForSaleNft)({}); - await wait( - async () => (await build5Db().doc(`${COL.NFT}/${nft.uid}`).get())?.available === 3, - ); - }); - - it('Should create bid request', async () => { - await bidNft(members[0], MIN_IOTA_AMOUNT); - const snap = await build5Db() - .collection(COL.TRANSACTION) - .where('payload.type', '==', TransactionPayloadType.NFT_BID) - .where('member', '==', members[0]) - .get(); - expect(snap.length).toBe(1); - const tran = snap[0]; - expect(tran.payload.beneficiary).toBe('member'); - expect(tran.payload.beneficiaryUid).toBe(memberAddress); - expect(tran.payload.royaltiesFee).toBe(collection.royaltiesFee); - expect(tran.payload.royaltiesSpace).toBe(collection.royaltiesSpace); - expect(tran.payload.expiresOn).toBeDefined(); - expect(tran.payload.reconciled).toBe(false); - expect(tran.payload.validationType).toBe(TransactionValidationType.ADDRESS); - expect(tran.payload.nft).toBe(nft.uid); - expect(tran.payload.collection).toBe(collection.uid); - - const nftDocRef = build5Db().collection(COL.NFT).doc(nft.uid); - nft = await nftDocRef.get(); - expect(nft.lastTradedOn).toBeDefined(); - expect(nft.totalTrades).toBe(1); - - const collectionDocRef = build5Db().collection(COL.COLLECTION).doc(nft.collection); - collection = await collectionDocRef.get(); - expect(collection.lastTradedOn).toBeDefined(); - expect(collection.totalTrades).toBe(1); - }); - - it('Should bid and send amount', async () => { - await bidNft(members[0], MIN_IOTA_AMOUNT); - const nftData = await build5Db().doc(`${COL.NFT}/${nft.uid}`).get(); - expect(nftData.auctionHighestBidder).toBe(members[0]); - }); - - it('Should credit lowest bidder', async () => { - mockWalletReturnValue(walletSpy, members[0], { nft: nft.uid }); - await bidNft(members[0], 2 * MIN_IOTA_AMOUNT); - const nftData = await build5Db().doc(`${COL.NFT}/${nft.uid}`).get(); - expect(nftData.auctionHighestBidder).toBe(members[0]); - - await bidNft(members[1], 3 * MIN_IOTA_AMOUNT); - const updated = await build5Db().doc(`${COL.NFT}/${nft.uid}`).get(); - expect(updated.auctionHighestBidder).toBe(members[1]); - - const snap = await build5Db() - .collection(COL.TRANSACTION) - .where('type', '==', TransactionType.CREDIT) - .where('member', '==', members[0]) - .where('payload.nft', '==', nft.uid) - .get(); - expect(snap.length).toBe(1); - const tran = snap[0]; - - expect(tran.payload.amount).toBe(2 * MIN_IOTA_AMOUNT); - expect(tran.payload.nft).toBe(nft.uid); - expect(tran.payload.reconciled).toBe(true); - expect(tran.payload.sourceAddress).toBeDefined(); - expect(tran.payload.targetAddress).toBeDefined(); - expect(tran.payload.sourceTransaction!.length).toBe(1); - }); - - it('Should reject smaller bid', async () => { - await bidNft(members[0], 2 * MIN_IOTA_AMOUNT); - const nftData = await build5Db().doc(`${COL.NFT}/${nft.uid}`).get(); - expect(nftData.auctionHighestBidder).toBe(members[0]); - - await bidNft(members[1], MIN_IOTA_AMOUNT); - const updated = await build5Db().doc(`${COL.NFT}/${nft.uid}`).get(); - expect(updated.auctionHighestBidder).toBe(members[0]); - - const snap = await build5Db() - .collection(COL.TRANSACTION) - .where('type', '==', TransactionType.CREDIT) - .where('member', '==', members[1]) - .where('payload.nft', '==', nft.uid) - .get(); - expect(snap.length).toBe(1); - }); - - it('Should bid in parallel', async () => { - const bidPromises = [ - bidNft(members[0], 2 * MIN_IOTA_AMOUNT), - bidNft(members[1], 3 * MIN_IOTA_AMOUNT), - bidNft(members[2], MIN_IOTA_AMOUNT), - ]; - await Promise.all(bidPromises); - const nftData = await build5Db().doc(`${COL.NFT}/${nft.uid}`).get(); - expect(nftData.auctionHighestBidder).toBe(members[1]); - - const transactionSnap = await build5Db() - .collection(COL.TRANSACTION) - .where('type', '==', TransactionType.CREDIT) - .where('member', 'in', [members[0], members[2]]) - .where('payload.nft', '==', nft.uid) - .get(); - expect(transactionSnap.length).toBe(2); - }); -}); - -describe('Nft bidding with extended auction', () => { - let now: dayjs.Dayjs; - let nftDocRef: IDocument; - - const setForSale = async (auctionCustomLength?: number, extendAuctionWithin?: number) => { - now = dayjs(); - const auctionData = { - ...dummyAuctionData(nft.uid, auctionCustomLength, now), - extendedAuctionLength: 60000 * 10, - }; - extendAuctionWithin && set(auctionData, 'extendAuctionWithin', extendAuctionWithin); - mockWalletReturnValue(walletSpy, memberAddress, auctionData); - await testEnv.wrap(setForSaleNft)({}); - - nftDocRef = build5Db().doc(`${COL.NFT}/${nft.uid}`); - await wait(async () => (await nftDocRef.get())?.available === 3); - }; - - it('Should bid and auction date to extended date', async () => { - await setForSale(); - - let nftData = await nftDocRef.get(); - - expect(nftData?.auctionLength).toBe(60000 * 4); - let auctionToDate = dayjs(nftData?.auctionTo?.toDate()); - const expectedAuctionToDate = dayjs(dateToTimestamp(now, true).toDate()).add( - nftData?.auctionLength!, - ); - expect(auctionToDate.isSame(expectedAuctionToDate)).toBe(true); - - let auctionExtendedDate = dayjs(nftData?.extendedAuctionTo?.toDate()); - const expectedAuctionExtendedToDate = dayjs(dateToTimestamp(now, true).toDate()).add( - nftData?.extendedAuctionLength!, - ); - expect(auctionExtendedDate.isSame(expectedAuctionExtendedToDate)).toBe(true); - expect(nftData?.extendedAuctionLength).toBe(60000 * 10); - - await bidNft(members[0], MIN_IOTA_AMOUNT); - nftData = await build5Db().doc(`${COL.NFT}/${nft.uid}`).get(); - expect(nftData.auctionHighestBidder).toBe(members[0]); - - nftData = await nftDocRef.get(); - auctionToDate = dayjs(nftData?.auctionTo?.toDate()); - auctionExtendedDate = dayjs(nftData?.extendedAuctionTo?.toDate()); - expect(auctionToDate.isSame(expectedAuctionExtendedToDate)).toBe(true); - expect(nftData?.auctionLength).toBe(nftData?.extendedAuctionLength); - }); - - it('Should bid but not set auction date to extended date', async () => { - await setForSale(60000 * 6); - let nftData = await nftDocRef.get(); - - expect(nftData?.auctionLength).toBe(60000 * 6); - let auctionToDate = dayjs(nftData?.auctionTo?.toDate()); - const expectedAuctionToDate = dayjs(dateToTimestamp(now, true).toDate()).add( - nftData?.auctionLength!, - ); - expect(auctionToDate.isSame(expectedAuctionToDate)).toBe(true); - - let auctionExtendedDate = dayjs(nftData?.extendedAuctionTo?.toDate()); - const expectedAuctionExtendedToDate = dayjs(dateToTimestamp(now, true).toDate()).add( - nftData?.extendedAuctionLength!, - ); - expect(auctionExtendedDate.isSame(expectedAuctionExtendedToDate)).toBe(true); - expect(nftData?.extendedAuctionLength).toBe(60000 * 10); - - await bidNft(members[0], MIN_IOTA_AMOUNT); - nftData = await build5Db().doc(`${COL.NFT}/${nft.uid}`).get(); - expect(nftData.auctionHighestBidder).toBe(members[0]); - - nftData = await nftDocRef.get(); - auctionToDate = dayjs(nftData?.auctionTo?.toDate()); - expect(auctionToDate.isSame(expectedAuctionToDate)).toBe(true); - expect(nftData?.auctionLength).toBe(60000 * 6); - }); - - it('Should throw, extended auction lenght must be greater then auction lenght', async () => { - const auctionData = { - ...dummyAuctionData(nft.uid), - extendedAuctionLength: 60000 * 3, - }; - mockWalletReturnValue(walletSpy, memberAddress, auctionData); - await expectThrow(testEnv.wrap(setForSaleNft)({}), WenError.invalid_params.key); - }); - - it('Should bid but custom extend within time', async () => { - await setForSale(60000 * 6, 60000 * 6); - let nftData = await nftDocRef.get(); - - expect(nftData?.auctionLength).toBe(60000 * 6); - let auctionToDate = dayjs(nftData?.auctionTo?.toDate()); - const expectedAuctionToDate = dayjs(dateToTimestamp(now, true).toDate()).add( - nftData?.auctionLength!, - ); - expect(auctionToDate.isSame(expectedAuctionToDate)).toBe(true); - - let auctionExtendedDate = dayjs(nftData?.extendedAuctionTo?.toDate()); - const expectedAuctionExtendedToDate = dayjs(dateToTimestamp(now, true).toDate()).add( - nftData?.extendedAuctionLength!, - ); - expect(auctionExtendedDate.isSame(expectedAuctionExtendedToDate)).toBe(true); - expect(nftData?.extendedAuctionLength).toBe(60000 * 10); - - await bidNft(members[0], MIN_IOTA_AMOUNT); - nftData = await build5Db().doc(`${COL.NFT}/${nft.uid}`).get(); - expect(nftData.auctionHighestBidder).toBe(members[0]); - - nftData = await nftDocRef.get(); - auctionToDate = dayjs(nftData?.auctionTo?.toDate()); - auctionExtendedDate = dayjs(nftData?.extendedAuctionTo?.toDate()); - expect(auctionToDate.isSame(auctionExtendedDate)).toBe(true); - expect(nftData?.auctionLength).toBe(60000 * 10); - }); - - it('Should throw, invalid extendAuctionWithin', async () => { - const auctionData = { - ...dummyAuctionData(nft.uid), - extendedAuctionLength: 60000 * 10, - extendAuctionWithin: 0, - }; - mockWalletReturnValue(walletSpy, memberAddress, auctionData); - await expectThrow(testEnv.wrap(setForSaleNft)({}), WenError.invalid_params.key); - }); -}); - -describe('Should finalize bidding', () => { - beforeEach(async () => { - mockWalletReturnValue(walletSpy, memberAddress, dummyAuctionData(nft.uid)); - await testEnv.wrap(setForSaleNft)({}); - await wait( - async () => (await build5Db().doc(`${COL.NFT}/${nft.uid}`).get())?.available === 3, - ); - }); - - it.each([false, true])('Should bid and finalize it', async (noRoyaltySpace: boolean) => { - if (noRoyaltySpace) { - await build5Db() - .doc(`${COL.COLLECTION}/${collection.uid}`) - .update({ royaltiesSpace: '', royaltiesFee: 0 }); - } - const bidOrder = await bidNft(members[0], MIN_IOTA_AMOUNT); - expect(bidOrder.payload.restrictions.collection).toEqual({ - access: collection.access, - accessAwards: collection.accessAwards || [], - accessCollections: collection.accessCollections || [], - }); - expect(bidOrder.payload.restrictions.nft).toEqual({ - saleAccess: nft.saleAccess || null, - saleAccessMembers: nft.saleAccessMembers || [], - }); - - const nftData = await build5Db().doc(`${COL.NFT}/${nft.uid}`).get(); - expect(nftData.auctionHighestBidder).toBe(members[0]); - - const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${nftData.collection}`); - collection = await collectionDocRef.get(); - expect(collection.nftsOnAuction).toBe(1); - - await build5Db() - .doc(`${COL.NFT}/${nft.uid}`) - .update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'minute')) }); - - await finalizeAllNftAuctions(); - - const nftDocRef = build5Db().doc(`${COL.NFT}/${nft.uid}`); - await wait(async () => { - const updatedNft = await nftDocRef.get(); - return updatedNft.available === NftAvailable.UNAVAILABLE; - }); - const updatedNft = await nftDocRef.get(); - expect(updatedNft.owner).toBe(members[0]); - expect(updatedNft.auctionFrom).toBeNull(); - expect(updatedNft.auctionTo).toBeNull(); - - const snap = await build5Db() - .collection(COL.NOTIFICATION) - .where('member', '==', members[0]) - .where('type', '==', NotificationType.WIN_BID) - .get(); - expect(snap.length).toBe(1); - - collection = await collectionDocRef.get(); - expect(collection.nftsOnAuction).toBe(0); - expect(collection.lastTradedOn).toBeDefined(); - expect(collection.totalTrades).toBe(2); - - nft = await nftDocRef.get(); - expect(nft.lastTradedOn).toBeDefined(); - expect(nft.totalTrades).toBe(2); - - const billPayments = await build5Db() - .collection(COL.TRANSACTION) - .where('type', '==', TransactionType.BILL_PAYMENT) - .where('payload.nft', '==', nft.uid) - .get(); - for (const billPayment of billPayments) { - expect(billPayment.payload.restrictions).toEqual(bidOrder.payload.restrictions); - } - }); -}); diff --git a/packages/functions/test/controls/nft/Helper.ts b/packages/functions/test/controls/nft/Helper.ts new file mode 100644 index 0000000000..e5de63b000 --- /dev/null +++ b/packages/functions/test/controls/nft/Helper.ts @@ -0,0 +1,122 @@ +import { build5Db } from '@build-5/database'; +import { + Access, + COL, + Categories, + Collection, + CollectionType, + MIN_IOTA_AMOUNT, + Nft, + NftAccess, + Space, +} from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { approveCollection, createCollection } from '../../../src/runtime/firebase/collection'; +import { openBid, createNft, orderNft } from '../../../src/runtime/firebase/nft'; +import * as wallet from '../../../src/utils/wallet.utils'; +import { MEDIA, testEnv } from '../../set-up'; +import { + createMember, + createSpace, + mockWalletReturnValue, + submitMilestoneFunc, + wait, +} from '../common'; + +export class Helper { + public spy: any = {} as any; + public member: string = {} as any; + public members: string[] = []; + public space: Space = {} as any; + public collection: Collection = {} as any; + public nft: Nft = {} as any; + + public beforeAll = async () => { + this.spy = jest.spyOn(wallet, 'decodeAuth'); + }; + + public beforeEach = async () => { + this.member = await createMember(this.spy); + const memberPromises = Array.from(Array(3)).map(() => createMember(this.spy)); + this.members = await Promise.all(memberPromises); + this.space = await createSpace(this.spy, this.member); + + mockWalletReturnValue(this.spy, this.member, { + name: 'Collection A', + description: 'babba', + type: CollectionType.CLASSIC, + royaltiesFee: 0.6, + category: Categories.ART, + access: Access.OPEN, + space: this.space.uid, + royaltiesSpace: this.space.uid, + onePerMemberOnly: false, + availableFrom: dayjs().toDate(), + price: 10 * 1000 * 1000, + }); + + this.collection = await testEnv.wrap(createCollection)({}); + mockWalletReturnValue(this.spy, this.member, { uid: this.collection.uid }); + await testEnv.wrap(approveCollection)({}); + + mockWalletReturnValue(this.spy, this.member, { + media: MEDIA, + ...dummyNft(this.collection.uid), + }); + this.nft = await testEnv.wrap(createNft)({}); + + const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${this.collection.uid}`); + await wait(async () => { + this.collection = await collectionDocRef.get(); + return this.collection.availableNfts === 1; + }); + + mockWalletReturnValue(this.spy, this.member, { + collection: this.collection.uid, + nft: this.nft.uid, + }); + const nftOrder = await testEnv.wrap(orderNft)({}); + await submitMilestoneFunc(nftOrder); + + await wait(async () => { + this.collection = await collectionDocRef.get(); + return this.collection.availableNfts === 0; + }); + }; + + public bidNft = async (memberId: string, amount: number) => { + mockWalletReturnValue(this.spy, memberId, { nft: this.nft.uid }); + const bidOrder = await testEnv.wrap(openBid)({}); + await submitMilestoneFunc(bidOrder, amount); + return bidOrder; + }; +} + +const dummyNft = (collection: string, description = 'babba') => ({ + name: 'Collection A', + description, + collection, + availableFrom: dayjs().toDate(), + price: 10 * 1000 * 1000, +}); + +export const dummySaleData = (uid: string) => ({ + nft: uid, + price: MIN_IOTA_AMOUNT, + availableFrom: dayjs().toDate(), + access: NftAccess.OPEN, +}); + +export const dummyAuctionData = ( + uid: string, + auctionLength = 60000 * 4, + from: dayjs.Dayjs = dayjs(), +) => ({ + nft: uid, + price: MIN_IOTA_AMOUNT, + availableFrom: from.toDate(), + auctionFrom: from.toDate(), + auctionFloorPrice: MIN_IOTA_AMOUNT, + auctionLength, + access: NftAccess.OPEN, +}); diff --git a/packages/functions/test/controls/nft/nft.bidding.extends.spec.ts b/packages/functions/test/controls/nft/nft.bidding.extends.spec.ts new file mode 100644 index 0000000000..3d09652fcc --- /dev/null +++ b/packages/functions/test/controls/nft/nft.bidding.extends.spec.ts @@ -0,0 +1,154 @@ +import { IDocument, build5Db } from '@build-5/database'; +import { Auction, COL, MIN_IOTA_AMOUNT, Nft, WenError } from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { set } from 'lodash'; +import { setForSaleNft } from '../../../src/runtime/firebase/nft'; +import { dateToTimestamp } from '../../../src/utils/dateTime.utils'; +import { testEnv } from '../../set-up'; +import { expectThrow, mockWalletReturnValue, wait } from '../common'; +import { Helper, dummyAuctionData } from './Helper'; + +describe('Nft bidding with extended auction', () => { + const h = new Helper(); + + let now: dayjs.Dayjs; + let nftDocRef: IDocument; + + beforeAll(async () => { + await h.beforeAll(); + }); + + beforeEach(async () => { + await h.beforeEach(); + }); + + const setForSale = async (auctionCustomLength?: number, extendAuctionWithin?: number) => { + now = dayjs(); + const auctionData = { + ...dummyAuctionData(h.nft.uid, auctionCustomLength, now), + extendedAuctionLength: 60000 * 10, + }; + extendAuctionWithin && set(auctionData, 'extendAuctionWithin', extendAuctionWithin); + mockWalletReturnValue(h.spy, h.member, auctionData); + await testEnv.wrap(setForSaleNft)({}); + + nftDocRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + await wait(async () => { + h.nft = await nftDocRef.get(); + return h.nft.available === 3; + }); + }; + + it('Should bid and auction date to extended date', async () => { + await setForSale(); + + expect(h.nft.auctionLength).toBe(60000 * 4); + let auctionToDate = dayjs(h.nft.auctionTo?.toDate()); + const expectedAuctionToDate = dayjs(dateToTimestamp(now, true).toDate()).add( + h.nft.auctionLength!, + ); + expect(auctionToDate.isSame(expectedAuctionToDate)).toBe(true); + + let auctionExtendedDate = dayjs(h.nft.extendedAuctionTo?.toDate()); + const expectedAuctionExtendedToDate = dayjs(dateToTimestamp(now, true).toDate()).add( + h.nft.extendedAuctionLength!, + ); + expect(auctionExtendedDate.isSame(expectedAuctionExtendedToDate)).toBe(true); + expect(h.nft.extendedAuctionLength).toBe(60000 * 10); + + await h.bidNft(h.members[0], MIN_IOTA_AMOUNT); + h.nft = await build5Db().doc(`${COL.NFT}/${h.nft.uid}`).get(); + expect(h.nft.auctionHighestBidder).toBe(h.members[0]); + + h.nft = await nftDocRef.get(); + auctionToDate = dayjs(h.nft.auctionTo?.toDate()); + auctionExtendedDate = dayjs(h.nft.extendedAuctionTo?.toDate()); + expect(auctionToDate.isSame(expectedAuctionExtendedToDate)).toBe(true); + expect(h.nft.auctionLength).toBe(h.nft.extendedAuctionLength); + + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${h.nft.auction}`); + const auction = await auctionDocRef.get(); + auctionToDate = dayjs(auction.auctionTo?.toDate()); + auctionExtendedDate = dayjs(auction.extendedAuctionTo?.toDate()); + expect(auctionToDate.isSame(expectedAuctionExtendedToDate)).toBe(true); + expect(auction.auctionLength).toBe(auction.extendedAuctionLength); + }); + + it('Should bid but not set auction date to extended date', async () => { + await setForSale(60000 * 6); + h.nft = await nftDocRef.get(); + + expect(h.nft.auctionLength).toBe(60000 * 6); + let auctionToDate = dayjs(h.nft.auctionTo?.toDate()); + const expectedAuctionToDate = dayjs(dateToTimestamp(now, true).toDate()).add( + h.nft.auctionLength!, + ); + expect(auctionToDate.isSame(expectedAuctionToDate)).toBe(true); + + let auctionExtendedDate = dayjs(h.nft.extendedAuctionTo?.toDate()); + const expectedAuctionExtendedToDate = dayjs(dateToTimestamp(now, true).toDate()).add( + h.nft.extendedAuctionLength!, + ); + expect(auctionExtendedDate.isSame(expectedAuctionExtendedToDate)).toBe(true); + expect(h.nft.extendedAuctionLength).toBe(60000 * 10); + + await h.bidNft(h.members[0], MIN_IOTA_AMOUNT); + h.nft = await nftDocRef.get(); + expect(h.nft.auctionHighestBidder).toBe(h.members[0]); + auctionToDate = dayjs(h.nft.auctionTo?.toDate()); + expect(auctionToDate.isSame(expectedAuctionToDate)).toBe(true); + expect(h.nft.auctionLength).toBe(60000 * 6); + + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${h.nft.auction}`); + const auction = await auctionDocRef.get(); + auctionToDate = dayjs(auction.auctionTo?.toDate()); + expect(auctionToDate.isSame(expectedAuctionToDate)).toBe(true); + expect(auction.auctionLength).toBe(60000 * 6); + }); + + it('Should throw, extended auction lenght must be greater then auction lenght', async () => { + const auctionData = { + ...dummyAuctionData(h.nft.uid), + extendedAuctionLength: 60000 * 3, + }; + mockWalletReturnValue(h.spy, h.member, auctionData); + await expectThrow(testEnv.wrap(setForSaleNft)({}), WenError.invalid_params.key); + }); + + it('Should bid but custom extend within time', async () => { + await setForSale(60000 * 6, 60000 * 6); + h.nft = await nftDocRef.get(); + + expect(h.nft.auctionLength).toBe(60000 * 6); + let auctionToDate = dayjs(h.nft.auctionTo?.toDate()); + const expectedAuctionToDate = dayjs(dateToTimestamp(now, true).toDate()).add( + h.nft.auctionLength!, + ); + expect(auctionToDate.isSame(expectedAuctionToDate)).toBe(true); + + let auctionExtendedDate = dayjs(h.nft.extendedAuctionTo?.toDate()); + const expectedAuctionExtendedToDate = dayjs(dateToTimestamp(now, true).toDate()).add( + h.nft.extendedAuctionLength!, + ); + expect(auctionExtendedDate.isSame(expectedAuctionExtendedToDate)).toBe(true); + expect(h.nft.extendedAuctionLength).toBe(60000 * 10); + + await h.bidNft(h.members[0], MIN_IOTA_AMOUNT); + h.nft = await nftDocRef.get(); + expect(h.nft.auctionHighestBidder).toBe(h.members[0]); + auctionToDate = dayjs(h.nft.auctionTo?.toDate()); + auctionExtendedDate = dayjs(h.nft.extendedAuctionTo?.toDate()); + expect(auctionToDate.isSame(auctionExtendedDate)).toBe(true); + expect(h.nft.auctionLength).toBe(60000 * 10); + }); + + it('Should throw, invalid extendAuctionWithin', async () => { + const auctionData = { + ...dummyAuctionData(h.nft.uid), + extendedAuctionLength: 60000 * 10, + extendAuctionWithin: 0, + }; + mockWalletReturnValue(h.spy, h.member, auctionData); + await expectThrow(testEnv.wrap(setForSaleNft)({}), WenError.invalid_params.key); + }); +}); diff --git a/packages/functions/test/controls/nft/nft.bidding.finalize.spec.ts b/packages/functions/test/controls/nft/nft.bidding.finalize.spec.ts new file mode 100644 index 0000000000..608060aa06 --- /dev/null +++ b/packages/functions/test/controls/nft/nft.bidding.finalize.spec.ts @@ -0,0 +1,110 @@ +import { build5Db } from '@build-5/database'; +import { + Auction, + COL, + Collection, + MIN_IOTA_AMOUNT, + Nft, + NftAvailable, + NotificationType, + Transaction, + TransactionType, +} from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { finalizeAuctions } from '../../../src/cron/auction.cron'; +import { setForSaleNft } from '../../../src/runtime/firebase/nft'; +import { dateToTimestamp } from '../../../src/utils/dateTime.utils'; +import { testEnv } from '../../set-up'; +import { mockWalletReturnValue, wait } from '../common'; +import { Helper, dummyAuctionData } from './Helper'; + +describe('Should finalize bidding', () => { + const h = new Helper(); + + beforeAll(async () => { + await h.beforeAll(); + }); + + beforeEach(async () => { + await h.beforeEach(); + }); + + beforeEach(async () => { + await h.beforeEach(); + mockWalletReturnValue(h.spy, h.member, dummyAuctionData(h.nft.uid)); + await testEnv.wrap(setForSaleNft)({}); + await wait(async () => { + const docRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + h.nft = await docRef.get(); + return h.nft.available === 3; + }); + }); + + it.each([true, false])('Should bid and finalize it', async (noRoyaltySpace: boolean) => { + if (noRoyaltySpace) { + await build5Db() + .doc(`${COL.COLLECTION}/${h.collection.uid}`) + .update({ royaltiesSpace: '', royaltiesFee: 0 }); + } + const bidOrder = await h.bidNft(h.members[0], MIN_IOTA_AMOUNT); + expect(bidOrder.payload.restrictions.collection).toEqual({ + access: h.collection.access, + accessAwards: h.collection.accessAwards || [], + accessCollections: h.collection.accessCollections || [], + }); + expect(bidOrder.payload.restrictions.nft).toEqual({ + saleAccess: h.nft.saleAccess || null, + saleAccessMembers: h.nft.saleAccessMembers || [], + }); + + const nftDocRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + h.nft = await nftDocRef.get(); + expect(h.nft.auctionHighestBidder).toBe(h.members[0]); + + const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${h.nft.collection}`); + h.collection = await collectionDocRef.get(); + expect(h.collection.nftsOnAuction).toBe(1); + + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${h.nft.auction}`); + await auctionDocRef.update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'minute')) }); + + await finalizeAuctions(); + + await wait(async () => { + h.nft = await nftDocRef.get(); + return h.nft.available === NftAvailable.UNAVAILABLE; + }); + expect(h.nft.owner).toBe(h.members[0]); + expect(h.nft.auctionFrom).toBeNull(); + expect(h.nft.auctionTo).toBeNull(); + expect(h.nft.auction).toBeNull(); + + const snap = await build5Db() + .collection(COL.NOTIFICATION) + .where('member', '==', h.members[0]) + .where('type', '==', NotificationType.WIN_BID) + .get(); + expect(snap.length).toBe(1); + + h.collection = await collectionDocRef.get(); + expect(h.collection.nftsOnAuction).toBe(0); + expect(h.collection.lastTradedOn).toBeDefined(); + expect(h.collection.totalTrades).toBe(2); + + h.nft = await nftDocRef.get(); + expect(h.nft.lastTradedOn).toBeDefined(); + expect(h.nft.totalTrades).toBe(2); + + const billPayments = await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.BILL_PAYMENT) + .where('payload.nft', '==', h.nft.uid) + .get(); + for (const billPayment of billPayments) { + expect(billPayment.payload.restrictions).toEqual(bidOrder.payload.restrictions); + } + + const auction = await auctionDocRef.get(); + expect(auction.active).toBe(false); + }); +}); diff --git a/packages/functions/test/controls/nft/nft.bidding.spec.ts b/packages/functions/test/controls/nft/nft.bidding.spec.ts new file mode 100644 index 0000000000..8aa8599fed --- /dev/null +++ b/packages/functions/test/controls/nft/nft.bidding.spec.ts @@ -0,0 +1,207 @@ +import { build5Db } from '@build-5/database'; +import { + Auction, + COL, + Collection, + MIN_IOTA_AMOUNT, + Nft, + Transaction, + TransactionPayloadType, + TransactionType, + TransactionValidationType, +} from '@build-5/interfaces'; +import { orderNft, setForSaleNft } from '../../../src/runtime/firebase/nft'; +import { testEnv } from '../../set-up'; +import { mockWalletReturnValue, submitMilestoneFunc, wait } from '../common'; +import { Helper, dummyAuctionData } from './Helper'; + +describe('Nft bidding', () => { + const h = new Helper(); + + beforeAll(async () => { + await h.beforeAll(); + }); + + beforeEach(async () => { + await h.beforeEach(); + mockWalletReturnValue(h.spy, h.member, dummyAuctionData(h.nft.uid)); + await testEnv.wrap(setForSaleNft)({}); + await wait(async () => { + const docRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + h.nft = await docRef.get(); + return h.nft.available === 3; + }); + }); + + it('Should create bid request', async () => { + await h.bidNft(h.members[0], MIN_IOTA_AMOUNT); + const snap = await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.ORDER) + .where('payload.type', '==', TransactionPayloadType.NFT_BID) + .where('member', '==', h.members[0]) + .get(); + expect(snap.length).toBe(1); + const tran = snap[0]; + expect(tran.payload.beneficiary).toBe('member'); + expect(tran.payload.beneficiaryUid).toBe(h.member); + expect(tran.payload.royaltiesFee).toBe(h.collection.royaltiesFee); + expect(tran.payload.royaltiesSpace).toBe(h.collection.royaltiesSpace); + expect(tran.payload.expiresOn).toBeDefined(); + expect(tran.payload.reconciled).toBe(true); + expect(tran.payload.validationType).toBe(TransactionValidationType.ADDRESS); + expect(tran.payload.nft).toBe(h.nft.uid); + expect(tran.payload.collection).toBe(h.collection.uid); + + const nftDocRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + h.nft = await nftDocRef.get(); + expect(h.nft.lastTradedOn).toBeDefined(); + expect(h.nft.totalTrades).toBe(1); + expect(h.nft.auctionHighestBid).toBe(MIN_IOTA_AMOUNT); + expect(h.nft.auctionHighestBidder).toBe(h.members[0]); + + const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${h.nft.collection}`); + h.collection = await collectionDocRef.get(); + expect(h.collection.lastTradedOn).toBeDefined(); + expect(h.collection.totalTrades).toBe(1); + + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${h.nft.auction}`); + const auction = await auctionDocRef.get(); + expect(auction.bids.length).toBe(1); + expect(auction.bids[0].amount).toBe(MIN_IOTA_AMOUNT); + expect(auction.bids[0].bidder).toBe(h.members[0]); + }); + + it('Should override 2 bids for same user', async () => { + await h.bidNft(h.members[0], MIN_IOTA_AMOUNT); + await h.bidNft(h.members[0], 2 * MIN_IOTA_AMOUNT); + + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${h.nft.auction}`); + const auction = await auctionDocRef.get(); + expect(auction.bids.length).toBe(1); + expect(auction.bids[0].amount).toBe(2 * MIN_IOTA_AMOUNT); + expect(auction.bids[0].bidder).toBe(h.members[0]); + + const orders = await build5Db() + .collection(COL.TRANSACTION) + .where('member', '==', h.members[0]) + .where('type', '==', TransactionType.ORDER) + .where('payload.type', '==', TransactionPayloadType.NFT_BID) + .get(); + expect(orders.length).toBe(2); + + const credits = await build5Db() + .collection(COL.TRANSACTION) + .where('member', '==', h.members[0]) + .where('type', '==', TransactionType.CREDIT) + .get(); + expect(credits.length).toBe(1); + expect(credits[0].payload.amount).toBe(MIN_IOTA_AMOUNT); + }); + + it('Should overbid 2 bids, topup', async () => { + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${h.nft.auction}`); + await auctionDocRef.update({ topUpBased: true }); + + await h.bidNft(h.members[0], MIN_IOTA_AMOUNT); + await h.bidNft(h.members[0], MIN_IOTA_AMOUNT); + + let auction = await auctionDocRef.get(); + expect(auction.bids.length).toBe(1); + expect(auction.bids[0].amount).toBe(2 * MIN_IOTA_AMOUNT); + expect(auction.bids[0].bidder).toBe(h.members[0]); + expect(auction.auctionHighestBid).toBe(2 * MIN_IOTA_AMOUNT); + expect(auction.auctionHighestBidder).toBe(h.members[0]); + + const nftDocRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + h.nft = await nftDocRef.get(); + expect(h.nft.auctionHighestBid).toBe(2 * MIN_IOTA_AMOUNT); + expect(h.nft.auctionHighestBidder).toBe(h.members[0]); + + await h.bidNft(h.members[1], 3 * MIN_IOTA_AMOUNT); + auction = await auctionDocRef.get(); + expect(auction.bids.length).toBe(1); + expect(auction.bids[0].amount).toBe(3 * MIN_IOTA_AMOUNT); + expect(auction.bids[0].bidder).toBe(h.members[1]); + expect(auction.auctionHighestBid).toBe(3 * MIN_IOTA_AMOUNT); + expect(auction.auctionHighestBidder).toBe(h.members[1]); + + h.nft = await nftDocRef.get(); + expect(h.nft.auctionHighestBid).toBe(3 * MIN_IOTA_AMOUNT); + expect(h.nft.auctionHighestBidder).toBe(h.members[1]); + + const payments = await build5Db() + .collection(COL.TRANSACTION) + .where('member', '==', h.members[0]) + .where('type', '==', TransactionType.PAYMENT) + .get(); + for (const payment of payments) { + expect(payment.payload.invalidPayment).toBe(true); + const credit = await build5Db() + .collection(COL.TRANSACTION) + .where('payload.sourceTransaction', 'array-contains', payment.uid) + .get(); + expect(credit).toBeDefined(); + } + }); + + it('Should reject smaller bid', async () => { + const nftDocRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + await h.bidNft(h.members[0], 2 * MIN_IOTA_AMOUNT); + h.nft = await nftDocRef.get(); + expect(h.nft.auctionHighestBidder).toBe(h.members[0]); + + await h.bidNft(h.members[1], MIN_IOTA_AMOUNT); + h.nft = await nftDocRef.get(); + expect(h.nft.auctionHighestBidder).toBe(h.members[0]); + + const snap = await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.CREDIT) + .where('member', '==', h.members[1]) + .where('payload.nft', '==', h.nft.uid) + .get(); + expect(snap.length).toBe(1); + }); + + it('Should bid in parallel', async () => { + const bidPromises = [ + h.bidNft(h.members[0], 2 * MIN_IOTA_AMOUNT), + h.bidNft(h.members[1], 3 * MIN_IOTA_AMOUNT), + h.bidNft(h.members[2], MIN_IOTA_AMOUNT), + ]; + await Promise.all(bidPromises); + + const nftDocRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + h.nft = await nftDocRef.get(); + expect(h.nft.auctionHighestBidder).toBe(h.members[1]); + + const transactionSnap = await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.CREDIT) + .where('member', 'in', [h.members[0], h.members[2]]) + .where('payload.nft', '==', h.nft.uid) + .get(); + expect(transactionSnap.length).toBe(2); + }); + + it('Should create bid, then credit on sold', async () => { + await h.bidNft(h.members[0], MIN_IOTA_AMOUNT); + await h.bidNft(h.members[0], MIN_IOTA_AMOUNT); + + mockWalletReturnValue(h.spy, h.members[1], { collection: h.nft.collection, nft: h.nft.uid }); + const nftOrder = await testEnv.wrap(orderNft)({}); + await submitMilestoneFunc(nftOrder); + + const nftDocRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + h.nft = await nftDocRef.get(); + expect(h.nft.owner).toBe(h.members[1]); + + const credits = await build5Db() + .collection(COL.TRANSACTION) + .where('member', '==', h.members[0]) + .where('type', '==', TransactionType.CREDIT) + .get(); + expect(credits.length).toBe(2); + }); +}); diff --git a/packages/functions/test/controls/nft/nft.set.for.sale.spec.ts b/packages/functions/test/controls/nft/nft.set.for.sale.spec.ts new file mode 100644 index 0000000000..a8af4e8c80 --- /dev/null +++ b/packages/functions/test/controls/nft/nft.set.for.sale.spec.ts @@ -0,0 +1,89 @@ +import { build5Db } from '@build-5/database'; +import { COL, Collection, Nft, WenError } from '@build-5/interfaces'; +import { setForSaleNft } from '../../../src/runtime/firebase/nft'; +import { getRandomEthAddress } from '../../../src/utils/wallet.utils'; +import { testEnv } from '../../set-up'; +import { expectThrow, mockWalletReturnValue, wait } from '../common'; +import { Helper, dummyAuctionData, dummySaleData } from './Helper'; + +describe('Nft set for sale', () => { + const h = new Helper(); + + beforeAll(async () => { + await h.beforeAll(); + }); + + beforeEach(async () => { + await h.beforeEach(); + }); + + it('Should set nft for sale', async () => { + mockWalletReturnValue(h.spy, h.member, dummySaleData(h.nft.uid)); + await testEnv.wrap(setForSaleNft)({}); + + const nftDocRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + await wait(async () => { + const nft = await nftDocRef.get(); + return nft.available === 1; + }); + + const saleNft = await nftDocRef.get(); + expect(saleNft.available).toBe(1); + expect(saleNft.availableFrom).toBeDefined(); + + const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${saleNft.collection}`); + const collection = await collectionDocRef.get(); + expect(collection.nftsOnAuction).toBe(0); + expect(collection.nftsOnSale).toBe(1); + }); + + it('Should throw, nft set as avatar', async () => { + const nftDocRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + await nftDocRef.update({ setAsAvatar: true }); + + mockWalletReturnValue(h.spy, h.member, dummySaleData(h.nft.uid)); + await expectThrow(testEnv.wrap(setForSaleNft)({}), WenError.nft_set_as_avatar.key); + }); + + it('Should set nft for auction', async () => { + mockWalletReturnValue(h.spy, h.member, dummyAuctionData(h.nft.uid)); + await testEnv.wrap(setForSaleNft)({}); + + const nftDocRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + await wait(async () => { + const nft = await nftDocRef.get(); + return nft.available === 3; + }); + + const auctionNft = await build5Db().doc(`${COL.NFT}/${h.nft.uid}`).get(); + expect(auctionNft.available).toBe(3); + expect(auctionNft.auctionFrom).toBeDefined(); + expect(auctionNft.auctionTo).toBeDefined(); + expect(auctionNft.auctionLength).toBeDefined(); + + const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${auctionNft.collection}`); + const collection = await collectionDocRef.get(); + expect(collection.nftsOnAuction).toBe(1); + expect(collection.nftsOnSale).toBe(1); + }); + + it('Should throw, auction already in progress', async () => { + mockWalletReturnValue(h.spy, h.member, dummyAuctionData(h.nft.uid)); + await testEnv.wrap(setForSaleNft)({}); + mockWalletReturnValue(h.spy, h.member, dummyAuctionData(h.nft.uid)); + await expectThrow(testEnv.wrap(setForSaleNft)({}), WenError.auction_already_in_progress.key); + }); + + it('Should throw, invalid nft', async () => { + mockWalletReturnValue(h.spy, h.member, { + ...dummyAuctionData(h.nft.uid), + nft: getRandomEthAddress(), + }); + await expectThrow(testEnv.wrap(setForSaleNft)({}), WenError.nft_does_not_exists.key); + }); + + it('Should throw, not owner', async () => { + mockWalletReturnValue(h.spy, h.members[0], dummyAuctionData(h.nft.uid)); + await expectThrow(testEnv.wrap(setForSaleNft)({}), WenError.you_must_be_the_owner_of_nft.key); + }); +}); diff --git a/packages/functions/test/dbroll/nft.auction.roll.spec.ts b/packages/functions/test/dbroll/nft.auction.roll.spec.ts new file mode 100644 index 0000000000..ae67ebd22c --- /dev/null +++ b/packages/functions/test/dbroll/nft.auction.roll.spec.ts @@ -0,0 +1,176 @@ +import { IDocument, build5App, build5Db } from '@build-5/database'; +import { + Access, + Auction, + AuctionType, + COL, + Categories, + Collection, + CollectionType, + MIN_IOTA_AMOUNT, + Network, + Nft, + NftAccess, + Space, + Transaction, + TransactionType, +} from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { nftAuctionRoll } from '../../scripts/dbUpgrades/1.0.0/auction.roll'; +import { approveCollection, createCollection } from '../../src/runtime/firebase/collection'; +import { createNft, openBid, orderNft, setForSaleNft } from '../../src/runtime/firebase/nft'; +import * as wallet from '../../src/utils/wallet.utils'; +import { + createMember, + createSpace, + mockWalletReturnValue, + submitMilestoneFunc, + wait, +} from '../controls/common'; +import { MEDIA, testEnv } from '../set-up'; + +describe('Nft auction roll', () => { + let spy: any; + let member: string; + let members: string[]; + let space: Space; + let collection: Collection; + let nft: Nft; + let nftDocRef: IDocument; + + beforeAll(async () => { + spy = jest.spyOn(wallet, 'decodeAuth'); + }); + + beforeEach(async () => { + member = await createMember(spy); + const memberPromises = Array.from(Array(3)).map(() => createMember(spy)); + members = await Promise.all(memberPromises); + space = await createSpace(spy, member); + + mockWalletReturnValue(spy, member, dummyCollection(space.uid)); + collection = await testEnv.wrap(createCollection)({}); + + mockWalletReturnValue(spy, member, { uid: collection.uid }); + await testEnv.wrap(approveCollection)({}); + + mockWalletReturnValue(spy, member, dummyNft(collection.uid)); + nft = await testEnv.wrap(createNft)({}); + nftDocRef = build5Db().doc(`${COL.NFT}/${nft.uid}`); + + const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${collection.uid}`); + await wait(async () => { + collection = await collectionDocRef.get(); + return collection.availableNfts === 1; + }); + + mockWalletReturnValue(spy, member, { + collection: collection.uid, + nft: nft.uid, + }); + const nftOrder = await testEnv.wrap(orderNft)({}); + await submitMilestoneFunc(nftOrder); + + await wait(async () => { + collection = await collectionDocRef.get(); + return collection.availableNfts === 0; + }); + + mockWalletReturnValue(spy, member, dummyAuctionData(nft.uid)); + await testEnv.wrap(setForSaleNft)({}); + await wait(async () => { + nft = await nftDocRef.get(); + return nft.available === 3; + }); + }); + + const bidNft = async (memberId: string, amount: number) => { + mockWalletReturnValue(spy, memberId, { nft: nft.uid }); + const bidOrder = await testEnv.wrap(openBid)({}); + await submitMilestoneFunc(bidOrder, amount); + return bidOrder; + }; + + it('Should roll nft auction and finalize it.', async () => { + const bidOrder = await bidNft(members[0], MIN_IOTA_AMOUNT); + + const prevAuctionId = nft.auction; + + const payments = await build5Db() + .collection(COL.TRANSACTION) + .where('member', '==', members[0]) + .where('type', '==', TransactionType.PAYMENT) + .get(); + + await nftDocRef.update({ + auctionHighestTransaction: payments[0].uid, + auction: null, + mintingData: { network: Network.ATOI }, + }); + + await nftAuctionRoll(build5App()); + + nft = await nftDocRef.get(); + + expect(nft.auction).not.toBe(prevAuctionId); + + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${nft.auction}`); + const auction = await auctionDocRef.get(); + + expect(dayjs(auction.auctionFrom.toDate()).isSame(nft.auctionFrom?.toDate())).toBe(true); + expect(dayjs(auction.auctionTo.toDate()).isSame(nft.auctionTo?.toDate())).toBe(true); + expect(auction.auctionLength).toBe(nft.auctionLength); + expect(dayjs(auction.extendedAuctionTo!.toDate()).isSame(nft.extendedAuctionTo?.toDate())).toBe( + true, + ); + expect(auction.extendedAuctionLength).toBe(nft.extendedAuctionLength); + expect(auction.extendAuctionWithin).toBe(nft.extendAuctionWithin); + expect(auction.auctionFloorPrice).toBe(nft.auctionFloorPrice); + expect(auction.auctionHighestBidder).toBe(nft.auctionHighestBidder); + expect(auction.auctionHighestBid).toBe(nft.auctionHighestBid); + expect(auction.maxBids).toBe(1); + expect(auction.type).toBe(AuctionType.NFT); + expect(auction.network).toBe(Network.ATOI); + expect(auction.nftId).toBe(nft.uid); + expect(auction.active).toBe(true); + expect(auction.topUpBased).toBe(false); + expect(auction.bids.length).toBe(1); + expect(auction.bids[0].amount).toBe(nft.auctionHighestBid); + expect(auction.bids[0].bidder).toBe(nft.auctionHighestBidder); + expect(auction.bids[0].order).toBe(bidOrder.uid); + }); +}); + +const dummyCollection = (space: string) => ({ + name: 'Collection A', + description: 'babba', + type: CollectionType.CLASSIC, + royaltiesFee: 0.6, + category: Categories.ART, + access: Access.OPEN, + space, + royaltiesSpace: space, + onePerMemberOnly: false, + availableFrom: dayjs().toDate(), + price: 10 * 1000 * 1000, +}); + +const dummyNft = (collection: string, description = 'babba') => ({ + media: MEDIA, + name: 'Collection A', + description, + collection, + availableFrom: dayjs().toDate(), + price: 10 * 1000 * 1000, +}); + +const dummyAuctionData = (uid: string, auctionLength = 60000 * 4, from: dayjs.Dayjs = dayjs()) => ({ + nft: uid, + price: MIN_IOTA_AMOUNT, + availableFrom: from.toDate(), + auctionFrom: from.toDate(), + auctionFloorPrice: MIN_IOTA_AMOUNT, + auctionLength, + extendedAuctionLength: 60000 * 5, + access: NftAccess.OPEN, +}); diff --git a/packages/interfaces/src/api/post/AuctionBidRequest.ts b/packages/interfaces/src/api/post/AuctionBidRequest.ts new file mode 100644 index 0000000000..f2af13ed39 --- /dev/null +++ b/packages/interfaces/src/api/post/AuctionBidRequest.ts @@ -0,0 +1,14 @@ +/** + * This file was automatically generated by joi-to-typescript + * Do not modify this file manually + */ + +/** + * Request object to create a bid order. + */ +export interface AuctionBidRequest { + /** + * Build5 id of the auction. + */ + auction: string; +} diff --git a/packages/interfaces/src/api/post/AuctionCreateRequest.ts b/packages/interfaces/src/api/post/AuctionCreateRequest.ts new file mode 100644 index 0000000000..5d79efa800 --- /dev/null +++ b/packages/interfaces/src/api/post/AuctionCreateRequest.ts @@ -0,0 +1,42 @@ +/** + * This file was automatically generated by joi-to-typescript + * Do not modify this file manually + */ + +/** + * Request object to create an auction. + */ +export interface AuctionCreateRequest { + /** + * Floor price of the auction. Minimum 1000000, maximum 1000000000000 + */ + auctionFloorPrice: number; + /** + * Starting date of the auction. Can not be sooner then 10 minutes. + */ + auctionFrom: Date; + /** + * Millisecond value of the auction length. Minimum 240000, maximum 2678400000 + */ + auctionLength: number; + /** + * Auction will be extended if a bid happens this many milliseconds before auction ends. Default value is 300000 minutes + */ + extendAuctionWithin?: number; + /** + * If set, auction will automatically extended by this length if a bid comes in within {@link extendAuctionWithin} before the end of the auction. + */ + extendedAuctionLength?: number; + /** + * Specifies the maximum number of active bids. Minimum 1, maximum 10 + */ + maxBids: number; + /** + * Network on which this auction accepts bids. + */ + network: 'iota' | 'smr' | 'atoi' | 'rms'; + /** + * If set to true, consequent bids from the same user will be treated as topups + */ + topUpBased?: boolean; +} diff --git a/packages/interfaces/src/api/post/AwardCreateRequest.ts b/packages/interfaces/src/api/post/AwardCreateRequest.ts index 5cdaa7f8a9..f8a0810738 100644 --- a/packages/interfaces/src/api/post/AwardCreateRequest.ts +++ b/packages/interfaces/src/api/post/AwardCreateRequest.ts @@ -16,7 +16,7 @@ export interface AwardCreateBadgeRequest { */ image: string; /** - * The time for wich the reward nft will be locked. + * The time for which the reward nft will be locked. */ lockTime: number; /** @@ -58,7 +58,7 @@ export interface AwardCreateRequest { */ name: string; /** - * Network on wich the award will be minted and issued + * Network on which the award will be minted and issued */ network: 'iota' | 'smr' | 'atoi' | 'rms'; /** diff --git a/packages/interfaces/src/api/post/NftDepositRequest.ts b/packages/interfaces/src/api/post/NftDepositRequest.ts index 59965b4f51..4bcd8a272c 100644 --- a/packages/interfaces/src/api/post/NftDepositRequest.ts +++ b/packages/interfaces/src/api/post/NftDepositRequest.ts @@ -8,7 +8,7 @@ */ export interface NftDepositRequest { /** - * Network on wich the nft was minted. + * Network on which the nft was minted. */ network: 'iota' | 'smr' | 'atoi' | 'rms'; } diff --git a/packages/interfaces/src/api/post/index.ts b/packages/interfaces/src/api/post/index.ts index da9930411e..3f554d33e7 100644 --- a/packages/interfaces/src/api/post/index.ts +++ b/packages/interfaces/src/api/post/index.ts @@ -4,6 +4,8 @@ */ export * from './AddressValidationRequest'; +export * from './AuctionBidRequest'; +export * from './AuctionCreateRequest'; export * from './CutomTokenRequest'; export * from './AwardAddOwnerRequest'; export * from './AwardApproveParticipantRequest'; diff --git a/packages/interfaces/src/api/tangle/AuctionBidTangleRequest.ts b/packages/interfaces/src/api/tangle/AuctionBidTangleRequest.ts new file mode 100644 index 0000000000..0bbc7b08e2 --- /dev/null +++ b/packages/interfaces/src/api/tangle/AuctionBidTangleRequest.ts @@ -0,0 +1,18 @@ +/** + * This file was automatically generated by joi-to-typescript + * Do not modify this file manually + */ + +/** + * Tangle request object to create an auction bid + */ +export interface AuctionBidTangleRequest { + /** + * Build5 id of the auction to bid on. + */ + auction: string; + /** + * Type of the tangle request. + */ + requestType: 'BID_AUCTION'; +} diff --git a/packages/interfaces/src/api/tangle/AuctionCreateTangleRequest.ts b/packages/interfaces/src/api/tangle/AuctionCreateTangleRequest.ts new file mode 100644 index 0000000000..a17ee993c4 --- /dev/null +++ b/packages/interfaces/src/api/tangle/AuctionCreateTangleRequest.ts @@ -0,0 +1,46 @@ +/** + * This file was automatically generated by joi-to-typescript + * Do not modify this file manually + */ + +/** + * Request object to create an auction. + */ +export interface AuctionCreateTangleRequest { + /** + * Floor price of the auction. Minimum 1000000, maximum 1000000000000 + */ + auctionFloorPrice: number; + /** + * Starting date of the auction. Can not be sooner then 10 minutes. + */ + auctionFrom: Date; + /** + * Millisecond value of the auction length. Minimum 240000, maximum 2678400000 + */ + auctionLength: number; + /** + * Auction will be extended if a bid happens this many milliseconds before auction ends. Default value is 300000 minutes + */ + extendAuctionWithin?: number; + /** + * If set, auction will automatically extended by this length if a bid comes in within {@link extendAuctionWithin} before the end of the auction. + */ + extendedAuctionLength?: number; + /** + * Specifies the maximum number of active bids. Minimum 1, maximum 10 + */ + maxBids: number; + /** + * Network on which this auction accepts bids. + */ + network: 'iota' | 'smr' | 'atoi' | 'rms'; + /** + * Type of the tangle request. + */ + requestType: 'CREATE_AUCTION'; + /** + * If set to true, consequent bids from the same user will be treated as topups + */ + topUpBased?: boolean; +} diff --git a/packages/interfaces/src/api/tangle/AwardCreateTangleRequest.ts b/packages/interfaces/src/api/tangle/AwardCreateTangleRequest.ts index 9ab0dce718..be2240c5d7 100644 --- a/packages/interfaces/src/api/tangle/AwardCreateTangleRequest.ts +++ b/packages/interfaces/src/api/tangle/AwardCreateTangleRequest.ts @@ -14,7 +14,7 @@ export interface AwardCreateTangleRequest { description?: string | null | ''; image?: string; /** - * The time for wich the reward nft will be locked. + * The time for which the reward nft will be locked. */ lockTime: number; /** @@ -47,7 +47,7 @@ export interface AwardCreateTangleRequest { */ name: string; /** - * Network on wich the award will be minted and issued + * Network on which the award will be minted and issued */ network: 'iota' | 'smr' | 'atoi' | 'rms'; /** diff --git a/packages/interfaces/src/api/tangle/NftBidTangleRequest.ts b/packages/interfaces/src/api/tangle/NftBidTangleRequest.ts index e2846a083b..748e88eb2f 100644 --- a/packages/interfaces/src/api/tangle/NftBidTangleRequest.ts +++ b/packages/interfaces/src/api/tangle/NftBidTangleRequest.ts @@ -12,7 +12,7 @@ export interface NftBidTangleRequest { */ disableWithdraw?: boolean; /** - * Build5 if of the nft to bid on. + * Build5 id of the nft to bid on. */ nft: string; /** diff --git a/packages/interfaces/src/api/tangle/common.ts b/packages/interfaces/src/api/tangle/common.ts index 402ec23231..bb968847a4 100644 --- a/packages/interfaces/src/api/tangle/common.ts +++ b/packages/interfaces/src/api/tangle/common.ts @@ -31,4 +31,7 @@ export enum TangleRequestType { MINT_METADATA_NFT = 'MINT_METADATA_NFT', STAMP = 'STAMP', + + CREATE_AUCTION = 'CREATE_AUCTION', + BID_AUCTION = 'BID_AUCTION', } diff --git a/packages/interfaces/src/api/tangle/index.ts b/packages/interfaces/src/api/tangle/index.ts index 18ada23f62..4a480c992d 100644 --- a/packages/interfaces/src/api/tangle/index.ts +++ b/packages/interfaces/src/api/tangle/index.ts @@ -4,6 +4,9 @@ */ export * from './AddressValidationTangleRequest'; +export * from './AuctionBidTangleRequest'; +export * from './AuctionCreateTangleRequest'; +export * from './NftBidTangleRequest'; export * from './AwardAppParticipantTangleRequest'; export * from './AwardCreateTangleRequest'; export * from './AwardFundTangleRequest'; diff --git a/packages/interfaces/src/errors.ts b/packages/interfaces/src/errors.ts index d63ee3f554..c543224a9c 100644 --- a/packages/interfaces/src/errors.ts +++ b/packages/interfaces/src/errors.ts @@ -126,7 +126,7 @@ export const WenError = { key: 'Collection available from date is after NFT available from date.', }, you_must_be_the_owner_of_nft: { code: 2066, key: 'You must be the owner of NFT.' }, - nft_auction_already_in_progress: { code: 2067, key: 'NFT already have auction in progress.' }, + auction_already_in_progress: { code: 2067, key: 'Auction already in progress.' }, nft_placeholder_cant_be_updated: { code: 2068, key: "Can't update placeholder NFT." }, you_cant_buy_your_nft: { code: 2069, key: 'You already own this NFT!' }, you_are_not_allowed_member_to_purchase_this_nft: { @@ -308,4 +308,6 @@ export const WenError = { you_are_not_admin_of_project: { code: 2139, key: 'You are not an admin of the project.' }, invalid_project_api_key: { code: 2140, key: 'Invalid project api key.' }, invalid_target_address: { code: 2141, key: 'Invalid target address.' }, + auction_not_active: { code: 2142, key: 'Auction no longer active.' }, + auction_does_not_exist: { code: 2143, key: 'Auction does not exist.' }, }; diff --git a/packages/interfaces/src/functions/index.ts b/packages/interfaces/src/functions/index.ts index c3dc2ccdc6..6e8087e088 100644 --- a/packages/interfaces/src/functions/index.ts +++ b/packages/interfaces/src/functions/index.ts @@ -50,7 +50,7 @@ export enum WEN_FUNC { // ORDER functions orderNft = 'ordernft', - openBid = 'openbid', + openBid = 'openBid', validateAddress = 'validateaddress', // TOKEN functions @@ -89,6 +89,8 @@ export enum WEN_FUNC { createProjetApiKey = 'createprojetapikey', stamp = 'stamp', + createauction = 'createauction', + bidAuction = 'bidauction', } export interface cMemberNotExists { diff --git a/packages/interfaces/src/models/auction.ts b/packages/interfaces/src/models/auction.ts new file mode 100644 index 0000000000..364e0639e0 --- /dev/null +++ b/packages/interfaces/src/models/auction.ts @@ -0,0 +1,37 @@ +import { BaseRecord, Timestamp } from './base'; +import { Network } from './transaction'; + +export interface AuctionBid { + amount: number; + bidder: string; + order: string; +} + +export enum AuctionType { + OPEN = 'OPEN', + NFT = 'NFT', +} + +export interface Auction extends BaseRecord { + auctionFrom: Timestamp; + auctionTo: Timestamp; + auctionLength: number; + + extendedAuctionTo?: Timestamp | null; + extendedAuctionLength?: number | null; + extendAuctionWithin?: number | null; + + auctionFloorPrice: number; + + bids: AuctionBid[]; + auctionHighestBidder?: string; + auctionHighestBid?: number; + maxBids: number; + + type: AuctionType; + network: Network; + nftId?: string; + + active: boolean; + topUpBased?: boolean; +} diff --git a/packages/interfaces/src/models/base.ts b/packages/interfaces/src/models/base.ts index 8ddf46e8d4..745b9bc436 100644 --- a/packages/interfaces/src/models/base.ts +++ b/packages/interfaces/src/models/base.ts @@ -65,6 +65,7 @@ export enum COL { AIRDROP = 'airdrop', PROJECT = 'project', STAMP = 'stamp', + AUCTION = 'auction', MNEMONIC = '_mnemonic', SYSTEM = '_system', diff --git a/packages/interfaces/src/models/index.ts b/packages/interfaces/src/models/index.ts index 37a31a3be3..498ffaaac5 100644 --- a/packages/interfaces/src/models/index.ts +++ b/packages/interfaces/src/models/index.ts @@ -1,3 +1,4 @@ +export * from './auction'; export * from './award'; export * from './badge'; export * from './base'; diff --git a/packages/interfaces/src/models/nft.ts b/packages/interfaces/src/models/nft.ts index bce19703f1..a22f8cd941 100644 --- a/packages/interfaces/src/models/nft.ts +++ b/packages/interfaces/src/models/nft.ts @@ -125,10 +125,6 @@ export interface Nft extends BaseRecord { * NFT Auction current highest bidder {@link Member} */ auctionHighestBidder?: string | null; - /** - * NFT Auction highest transaction {@link Transaction} - */ - auctionHighestTransaction?: string | null; /** * NFT current price based on previous sales */ @@ -241,4 +237,8 @@ export interface Nft extends BaseRecord { * NFT is set as avatar. */ setAsAvatar?: boolean; + /** + * The build5 id of the auction this nft belongs to + */ + auction?: string | null; } diff --git a/packages/interfaces/src/models/notification.ts b/packages/interfaces/src/models/notification.ts index fdbb91ffd3..56f2f2f1c7 100644 --- a/packages/interfaces/src/models/notification.ts +++ b/packages/interfaces/src/models/notification.ts @@ -5,38 +5,18 @@ export enum NotificationType { WIN_BID = 'WIN_BID', } -export interface NotificationBidParams { +interface NotificationParams { member: { name: string; }; amount: number; - nft: { - uid: string; - name: string; - }; + auction: string; } +export interface NotificationBidParams extends NotificationParams {} -export interface NotificationWinBidParams { - member: { - name: string; - }; - amount: number; - nft: { - uid: string; - name: string; - }; -} +export interface NotificationWinBidParams extends NotificationParams {} -export interface NotificationLostBidParams { - member: { - name: string; - }; - amount: number; - nft: { - uid: string; - name: string; - }; -} +export interface NotificationLostBidParams extends NotificationParams {} /** * Notification record. diff --git a/packages/interfaces/src/models/transaction/common.ts b/packages/interfaces/src/models/transaction/common.ts index 7767bd471c..0f48f3cd4b 100644 --- a/packages/interfaces/src/models/transaction/common.ts +++ b/packages/interfaces/src/models/transaction/common.ts @@ -55,6 +55,7 @@ export enum Network { export enum TransactionPayloadType { NFT_PURCHASE = 'NFT_PURCHASE', NFT_BID = 'NFT_BID', + AUCTION_BID = 'AUCTION_BID', SPACE_ADDRESS_VALIDATION = 'SPACE_ADDRESS_VALIDATION', MEMBER_ADDRESS_VALIDATION = 'MEMBER_ADDRESS_VALIDATION', TOKEN_PURCHASE = 'TOKEN_PURCHASE', diff --git a/packages/interfaces/src/models/transaction/payload.ts b/packages/interfaces/src/models/transaction/payload.ts index 66aac120ec..711a2e2026 100644 --- a/packages/interfaces/src/models/transaction/payload.ts +++ b/packages/interfaces/src/models/transaction/payload.ts @@ -153,4 +153,6 @@ export interface TransactionPayload { stamp?: string; tokenTradeOderTargetAddress?: string; + + auction?: string; } From 2512d6ff8c43352b172d49dfd0b04c0f4429ad46 Mon Sep 17 00:00:00 2001 From: Boldizsar Mezei Date: Tue, 31 Oct 2023 12:45:12 +0100 Subject: [PATCH 2/2] Min increment, targetAddress Fixes Fixes Fixes --- .../scripts/dbUpgrades/1.0.0/auction.roll.ts | 2 + .../auction/AuctionCreateRequestSchema.ts | 11 +- .../auction/auction.create.control.ts | 5 +- .../nft/NftSetForSaleRequestSchema.ts | 7 + packages/functions/src/cron/auction.cron.ts | 65 ++++++++- packages/functions/src/index.ts | 8 +- .../payment/auction/auction-bid.service.ts | 36 +++-- .../auction/auction.create.service.ts | 21 ++- .../nft/nft-set-for-sale.service.ts | 5 +- .../auction-tangle/auction.bit.tangle.spec.ts | 30 ++-- .../functions/test/controls/auction/Helper.ts | 18 ++- .../test/controls/auction/auction.bid.spec.ts | 131 +++++++++++++++++- .../functions/test/controls/nft/Helper.ts | 3 +- .../test/controls/nft/nft.bidding.spec.ts | 19 +++ .../src/api/post/AuctionCreateRequest.ts | 12 ++ .../src/api/post/NftSetForSaleRequest.ts | 4 + .../api/tangle/AuctionCreateTangleRequest.ts | 12 ++ .../api/tangle/NftSetForSaleTangleRequest.ts | 4 + packages/interfaces/src/models/auction.ts | 5 + 19 files changed, 353 insertions(+), 45 deletions(-) diff --git a/packages/functions/scripts/dbUpgrades/1.0.0/auction.roll.ts b/packages/functions/scripts/dbUpgrades/1.0.0/auction.roll.ts index 7773e0cec3..60f8497361 100644 --- a/packages/functions/scripts/dbUpgrades/1.0.0/auction.roll.ts +++ b/packages/functions/scripts/dbUpgrades/1.0.0/auction.roll.ts @@ -53,12 +53,14 @@ export const nftAuctionRoll = async (app: FirebaseApp) => { const getAuctionData = async (nft: Nft) => { const auction: Auction = { uid: getRandomEthAddress(), + space: nft.space, createdBy: nft.owner, project: nft.owner || SOON_PROJECT_ID, projects: nft.projects || { [SOON_PROJECT_ID]: true }, auctionFrom: nft.auctionFrom!, auctionTo: nft.auctionTo!, auctionFloorPrice: nft.auctionFloorPrice || 0, + minimalBidIncrement: 0, auctionLength: nft.auctionLength || 0, bids: [], diff --git a/packages/functions/src/controls/auction/AuctionCreateRequestSchema.ts b/packages/functions/src/controls/auction/AuctionCreateRequestSchema.ts index 3ed5f814e1..ab7db5fb40 100644 --- a/packages/functions/src/controls/auction/AuctionCreateRequestSchema.ts +++ b/packages/functions/src/controls/auction/AuctionCreateRequestSchema.ts @@ -8,7 +8,7 @@ import { } from '@build-5/interfaces'; import dayjs from 'dayjs'; import Joi from 'joi'; -import { toJoiObject } from '../../services/joi/common'; +import { CommonJoi, toJoiObject } from '../../services/joi/common'; import { AVAILABLE_NETWORKS } from '../common'; const minAvailableFrom = 10; @@ -16,6 +16,7 @@ const minBids = 1; const maxBids = 10; export const auctionCreateSchema = { + space: CommonJoi.uid().description('Build5 id of the space'), auctionFrom: Joi.date() .greater(dayjs().subtract(minAvailableFrom, 'minutes').toDate()) .required() @@ -29,6 +30,13 @@ export const auctionCreateSchema = { .description( `Floor price of the auction. Minimum ${MIN_IOTA_AMOUNT}, maximum ${MAX_IOTA_AMOUNT}`, ), + minimalBidIncrement: Joi.number() + .min(MIN_IOTA_AMOUNT) + .max(MAX_IOTA_AMOUNT) + .optional() + .description( + `Defines the minimum increment of a subsequent bid. Minimum ${MIN_IOTA_AMOUNT}, maximum ${MAX_IOTA_AMOUNT}`, + ), auctionLength: Joi.number() .min(TRANSACTION_AUTO_EXPIRY_MS) .max(TRANSACTION_MAX_EXPIRY_MS) @@ -65,6 +73,7 @@ export const auctionCreateSchema = { topUpBased: Joi.boolean().description( 'If set to true, consequent bids from the same user will be treated as topups', ), + targetAddress: Joi.string().description('A valid network address where funds will be sent.'), }; export const auctionCreateSchemaObject = toJoiObject(auctionCreateSchema) diff --git a/packages/functions/src/controls/auction/auction.create.control.ts b/packages/functions/src/controls/auction/auction.create.control.ts index 20c4a21597..8341d08f29 100644 --- a/packages/functions/src/controls/auction/auction.create.control.ts +++ b/packages/functions/src/controls/auction/auction.create.control.ts @@ -2,6 +2,7 @@ import { build5Db } from '@build-5/database'; import { Auction, AuctionCreateRequest, COL, Member, WenError } from '@build-5/interfaces'; import { getAuctionData } from '../../services/payment/tangle-service/auction/auction.create.service'; import { invalidArgument } from '../../utils/error.utils'; +import { assertIsSpaceMember } from '../../utils/space.utils'; import { Context } from '../common'; export const auctionCreateControl = async ({ @@ -15,7 +16,9 @@ export const auctionCreateControl = async ({ throw invalidArgument(WenError.member_does_not_exists); } - const auction = getAuctionData(project, owner, params); + await assertIsSpaceMember(params.space, owner); + + const auction = getAuctionData(project, member, params); const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auction.uid}`); await auctionDocRef.create(auction); diff --git a/packages/functions/src/controls/nft/NftSetForSaleRequestSchema.ts b/packages/functions/src/controls/nft/NftSetForSaleRequestSchema.ts index 22d5ac1f53..0969b4173d 100644 --- a/packages/functions/src/controls/nft/NftSetForSaleRequestSchema.ts +++ b/packages/functions/src/controls/nft/NftSetForSaleRequestSchema.ts @@ -33,6 +33,13 @@ export const baseNftSetForSaleSchema = { .min(MIN_IOTA_AMOUNT) .max(MAX_IOTA_AMOUNT) .description(`Floor price of the nft. Minimum ${MIN_IOTA_AMOUNT}, maximum ${MAX_IOTA_AMOUNT}`), + minimalBidIncrement: Joi.number() + .min(MIN_IOTA_AMOUNT) + .max(MAX_IOTA_AMOUNT) + .optional() + .description( + `Defines the minimum increment of a subsequent bid. Minimum ${MIN_IOTA_AMOUNT}, maximum ${MAX_IOTA_AMOUNT}`, + ), auctionLength: Joi.number() .min(TRANSACTION_AUTO_EXPIRY_MS) .max(TRANSACTION_MAX_EXPIRY_MS) diff --git a/packages/functions/src/cron/auction.cron.ts b/packages/functions/src/cron/auction.cron.ts index 7a7f80528a..b80d662190 100644 --- a/packages/functions/src/cron/auction.cron.ts +++ b/packages/functions/src/cron/auction.cron.ts @@ -1,8 +1,18 @@ import { build5Db } from '@build-5/database'; -import { Auction, AuctionType, COL } from '@build-5/interfaces'; +import { + Auction, + AuctionType, + COL, + Member, + Transaction, + TransactionType, +} from '@build-5/interfaces'; import dayjs from 'dayjs'; import { AuctionFinalizeService } from '../services/payment/auction/auction.finalize.service'; import { TransactionService } from '../services/payment/transaction-service'; +import { getAddress } from '../utils/address.utils'; +import { getProject, getProjects } from '../utils/common.utils'; +import { getRandomEthAddress } from '../utils/wallet.utils'; export const finalizeAuctions = async () => { const snap = await build5Db() @@ -10,9 +20,12 @@ export const finalizeAuctions = async () => { .where('auctionTo', '<=', dayjs().toDate()) .where('active', '==', true) .get(); - const promises = snap.map(async (a) => { - if (a.type === AuctionType.NFT) { - await finalizeNftAuction(a.uid); + const promises = snap.map((a) => { + switch (a.type) { + case AuctionType.NFT: + return finalizeNftAuction(a.uid); + case AuctionType.OPEN: + return finalizeOpenAuction(a); } }); await Promise.all(promises); @@ -25,3 +38,47 @@ const finalizeNftAuction = (auction: string) => await service.markAsFinalized(auction); tranService.submit(); }); + +const finalizeOpenAuction = async (auction: Auction) => { + const batch = build5Db().batch(); + + let targetAddress = auction.targetAddress; + if (!targetAddress) { + const memberDocRef = build5Db().doc(`${COL.MEMBER}/${auction.createdBy}`); + const member = await memberDocRef.get(); + targetAddress = getAddress(member, auction.network); + } + + const payments = await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.PAYMENT) + .where('payload.invalidPayment', '==', false) + .where('payload.auction', '==', auction.uid) + .get(); + + for (const payment of payments) { + const billPayment: Transaction = { + project: getProject(payment), + projects: getProjects([payment]), + type: TransactionType.BILL_PAYMENT, + uid: getRandomEthAddress(), + space: auction.space, + member: payment.member, + network: payment.network, + payload: { + amount: payment.payload.amount!, + sourceAddress: payment.payload.targetAddress, + targetAddress, + sourceTransaction: [payment.uid], + reconciled: true, + royalty: false, + void: false, + auction: auction.uid, + }, + }; + const billPaymentDocRef = build5Db().doc(`${COL.TRANSACTION}/${billPayment.uid}`); + batch.create(billPaymentDocRef, billPayment); + } + + await batch.commit(); +}; diff --git a/packages/functions/src/index.ts b/packages/functions/src/index.ts index 4c8808cb47..fedcfa1040 100644 --- a/packages/functions/src/index.ts +++ b/packages/functions/src/index.ts @@ -21,8 +21,12 @@ export const https = Object.entries(flattenObject(onRequests)).reduce( // Trigger functions const getFirestoreHandler = (config: TriggeredFunction) => { + const triggerConfig = { + document: config.document, + timeoutSeconds: config.runtimeOptions.timeoutSeconds, + }; if (config.type === TriggeredFunctionType.ON_CREATE) { - return functions.firestore.onDocumentCreated(config.document, async (event) => { + return functions.firestore.onDocumentCreated(triggerConfig, async (event) => { const data = { curr: event.data?.data(), path: event.document, @@ -35,7 +39,7 @@ const getFirestoreHandler = (config: TriggeredFunction) => { config.type === TriggeredFunctionType.ON_UPDATE ? functions.firestore.onDocumentUpdated : functions.firestore.onDocumentWritten; - return firestoreFunc(config.document, async (event) => { + return firestoreFunc(triggerConfig, async (event) => { const data = { prev: event.data?.before?.data(), curr: event.data?.after?.data(), diff --git a/packages/functions/src/services/payment/auction/auction-bid.service.ts b/packages/functions/src/services/payment/auction/auction-bid.service.ts index 97b5814bdd..9b081232c4 100644 --- a/packages/functions/src/services/payment/auction/auction-bid.service.ts +++ b/packages/functions/src/services/payment/auction/auction-bid.service.ts @@ -10,7 +10,7 @@ import { TransactionType, } from '@build-5/interfaces'; import dayjs from 'dayjs'; -import { head, last, set } from 'lodash'; +import { head, set } from 'lodash'; import { NotificationService } from '../../notification/notification'; import { HandlerParams } from '../base'; import { TransactionService } from '../transaction-service'; @@ -35,6 +35,7 @@ export class AuctionBidService { } this.transactionService.markAsReconciled(order, match.msgId); + const payment = await this.transactionService.createPayment(order, match); await this.addNewBid(owner, auction, order, payment); }; @@ -45,13 +46,12 @@ export class AuctionBidService { order: Transaction, payment: Transaction, ): Promise => { - if (paidAmountIsBelowFloor(payment, auction) || newPaymentTooLow(payment, auction)) { + if (!isValidBid(payment, auction)) { await this.creditAsInvalidPayment(payment); return; } const { bids, invalidBid } = placeBid(auction, order.uid, owner, payment.payload.amount!); - const auctionUpdateData = this.getAuctionUpdateData(auction, bids); if (invalidBid) { @@ -99,6 +99,7 @@ export class AuctionBidService { action: 'update', }); const paymentPayload = payment.payload; + set(payment, 'payload.invalidPayment', true); await this.transactionService.createCredit(TransactionPayloadType.INVALID_PAYMENT, payment, { msgId: paymentPayload.chainReference!, to: { @@ -158,6 +159,26 @@ export class AuctionBidService { }; } +const isValidBid = (payment: Transaction, auction: Auction) => { + const amount = payment.payload.amount!; + const prevBid = auction.bids.find((b) => b.bidder === payment.member); + const prevBidAmount = prevBid?.amount || 0; + + if (auction.topUpBased) { + return ( + prevBidAmount + amount >= auction.auctionFloorPrice && + amount >= auction.minimalBidIncrement && + (prevBid !== undefined || amount > (auction.bids[auction.maxBids - 1]?.amount || 0)) + ); + } + + return ( + amount > (auction.auctionHighestBid || 0) && + amount >= auction.auctionFloorPrice && + amount - prevBidAmount >= auction.minimalBidIncrement + ); +}; + const placeBid = (auction: Auction, order: string, bidder: string, amount: number) => { const bids = [...auction.bids]; const currentBid = bids.find((b) => b.bidder === bidder); @@ -169,7 +190,8 @@ const placeBid = (auction: Auction, order: string, bidder: string, amount: numbe } else { currentBid.amount = Math.max(currentBid.amount, amount); bids.sort((a, b) => b.amount - a.amount); - bids.push({ bidder, amount: Math.min(currentBid.amount, amount), order }); + const invalidBid = { bidder, amount: Math.min(currentBid.amount, amount), order }; + return { bids, invalidBid }; } } else { bids.push({ bidder, amount, order }); @@ -181,9 +203,3 @@ const placeBid = (auction: Auction, order: string, bidder: string, amount: numbe invalidBid: head(bids.slice(auction.maxBids)), }; }; - -const paidAmountIsBelowFloor = (payment: Transaction, auction: Auction) => - payment.payload.amount! < auction.auctionFloorPrice; - -const newPaymentTooLow = (payment: Transaction, auction: Auction) => - !auction.topUpBased && (last(auction.bids)?.amount || 0) > payment.payload.amount!; diff --git a/packages/functions/src/services/payment/tangle-service/auction/auction.create.service.ts b/packages/functions/src/services/payment/tangle-service/auction/auction.create.service.ts index 6873a36597..69ed9b60b5 100644 --- a/packages/functions/src/services/payment/tangle-service/auction/auction.create.service.ts +++ b/packages/functions/src/services/payment/tangle-service/auction/auction.create.service.ts @@ -5,9 +5,11 @@ import { AuctionCreateTangleRequest, AuctionType, COL, + Member, Network, } from '@build-5/interfaces'; import dayjs from 'dayjs'; +import { assertMemberHasValidAddress, getAddress } from '../../../../utils/address.utils'; import { getProjects } from '../../../../utils/common.utils'; import { dateToTimestamp } from '../../../../utils/dateTime.utils'; import { assertValidationAsync } from '../../../../utils/schema.utils'; @@ -22,7 +24,10 @@ export class TangleAuctionCreateService { public handleRequest = async ({ request, project, owner }: HandlerParams) => { const params = await assertValidationAsync(auctionCreateTangleSchema, request); - const auction = getAuctionData(project, owner, params); + const memberDocRef = build5Db().doc(`${COL.MEMBER}/${owner}`); + const member = await memberDocRef.get(); + + const auction = getAuctionData(project, member, params); const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auction.uid}`); this.transactionService.push({ ref: auctionDocRef, data: auction, action: 'set' }); @@ -33,19 +38,27 @@ export class TangleAuctionCreateService { export const getAuctionData = ( project: string, - owner: string, + member: Member, params: AuctionCreateRequest | AuctionCreateTangleRequest, ) => { + let targetAddress = params.targetAddress; + if (!targetAddress) { + assertMemberHasValidAddress(member, params.network as Network); + targetAddress = getAddress(member, params.network as Network); + } + const auction: Auction = { uid: getRandomEthAddress(), + space: params.space, project, projects: getProjects([], project), - createdBy: owner, + createdBy: member.uid, auctionFrom: dateToTimestamp(params.auctionFrom), auctionTo: dateToTimestamp(dayjs(params.auctionFrom).add(params.auctionLength)), auctionLength: params.auctionLength, auctionFloorPrice: params.auctionFloorPrice || 0, + minimalBidIncrement: params.minimalBidIncrement || 0, maxBids: params.maxBids, @@ -56,6 +69,8 @@ export const getAuctionData = ( topUpBased: params.topUpBased || false, bids: [], + + targetAddress, }; if (params.extendedAuctionLength && params.extendAuctionWithin) { diff --git a/packages/functions/src/services/payment/tangle-service/nft/nft-set-for-sale.service.ts b/packages/functions/src/services/payment/tangle-service/nft/nft-set-for-sale.service.ts index 1a48547793..5868ac613e 100644 --- a/packages/functions/src/services/payment/tangle-service/nft/nft-set-for-sale.service.ts +++ b/packages/functions/src/services/payment/tangle-service/nft/nft-set-for-sale.service.ts @@ -143,14 +143,17 @@ const getAuctionData = (project: string, owner: string, params: NftSetForSaleReq } const auction: Auction = { uid: getRandomEthAddress(), + space: nft.space, createdBy: owner, project, projects: getProjects([], project), auctionFrom: dateToTimestamp(params.auctionFrom), auctionTo: dateToTimestamp(dayjs(params.auctionFrom).add(params.auctionLength || 0)), - auctionFloorPrice: params.auctionFloorPrice || 0, auctionLength: params.auctionLength!, + auctionFloorPrice: params.auctionFloorPrice || 0, + minimalBidIncrement: params.minimalBidIncrement || 0, + bids: [], maxBids: 1, type: AuctionType.NFT, diff --git a/packages/functions/test-tangle/auction-tangle/auction.bit.tangle.spec.ts b/packages/functions/test-tangle/auction-tangle/auction.bit.tangle.spec.ts index dd5a9eabb2..1143897aeb 100644 --- a/packages/functions/test-tangle/auction-tangle/auction.bit.tangle.spec.ts +++ b/packages/functions/test-tangle/auction-tangle/auction.bit.tangle.spec.ts @@ -6,6 +6,7 @@ import { MIN_IOTA_AMOUNT, Member, Network, + Space, TangleRequestType, Transaction, TransactionType, @@ -16,7 +17,7 @@ import { Wallet } from '../../src/services/wallet/wallet'; import { AddressDetails } from '../../src/services/wallet/wallet.service'; import { getAddress } from '../../src/utils/address.utils'; import * as wallet from '../../src/utils/wallet.utils'; -import { createMember, wait } from '../../test/controls/common'; +import { createMember, createSpace, wait } from '../../test/controls/common'; import { getWallet } from '../../test/set-up'; import { getTangleOrder } from '../common'; import { requestFundsFromFaucet } from '../faucet'; @@ -25,6 +26,7 @@ let walletSpy: any; describe('Auction tangle test', () => { let member: string; + let space: Space; let memberAddress: AddressDetails; let w: Wallet; let tangleOrder: Transaction; @@ -40,6 +42,7 @@ describe('Auction tangle test', () => { beforeEach(async () => { member = await createMember(walletSpy); + space = await createSpace(walletSpy, member); const memberDocRef = build5Db().doc(`${COL.MEMBER}/${member}`); const memberData = await memberDocRef.get(); @@ -51,7 +54,7 @@ describe('Auction tangle test', () => { customMetadata: { request: { requestType: TangleRequestType.CREATE_AUCTION, - ...auctionRequest(now), + ...auctionRequest(space.uid, now), }, }, }); @@ -73,7 +76,7 @@ describe('Auction tangle test', () => { auction = await auctionDocRef.get(); }); - it('Should bid on auction', async () => { + it('Should create auction', async () => { expect(dayjs(auction.auctionFrom.toDate()).isSame(now)).toBe(true); expect(dayjs(auction.auctionTo.toDate()).isSame(now.add(60000 * 4))); expect(auction.auctionLength).toBe(60000 * 4); @@ -92,20 +95,14 @@ describe('Auction tangle test', () => { }); it('Should bid on auction', async () => { - const block = await w.send( - memberAddress, - tangleOrder.payload.targetAddress!, - 2 * MIN_IOTA_AMOUNT, - { - customMetadata: { - request: { - requestType: TangleRequestType.BID_AUCTION, - auction: auction.uid, - }, + await w.send(memberAddress, tangleOrder.payload.targetAddress!, 2 * MIN_IOTA_AMOUNT, { + customMetadata: { + request: { + requestType: TangleRequestType.BID_AUCTION, + auction: auction.uid, }, }, - ); - console.log(block); + }); await wait(async () => { auction = await auctionDocRef.get(); return auction.auctionHighestBidder === member; @@ -115,7 +112,8 @@ describe('Auction tangle test', () => { }); }); -const auctionRequest = (now: dayjs.Dayjs, auctionLength = 60000 * 4) => ({ +const auctionRequest = (space: string, now: dayjs.Dayjs, auctionLength = 60000 * 4) => ({ + space, auctionFrom: now.toDate(), auctionFloorPrice: 2 * MIN_IOTA_AMOUNT, auctionLength, diff --git a/packages/functions/test/controls/auction/Helper.ts b/packages/functions/test/controls/auction/Helper.ts index b5ff5c1c00..04762b2c69 100644 --- a/packages/functions/test/controls/auction/Helper.ts +++ b/packages/functions/test/controls/auction/Helper.ts @@ -1,13 +1,14 @@ import { IDocument, build5Db } from '@build-5/database'; -import { Auction, COL, MIN_IOTA_AMOUNT, Network } from '@build-5/interfaces'; +import { Auction, COL, MIN_IOTA_AMOUNT, Network, Space } from '@build-5/interfaces'; import dayjs from 'dayjs'; import { auctionCreate, bidAuction } from '../../../src/runtime/firebase/auction/index'; import * as wallet from '../../../src/utils/wallet.utils'; import { testEnv } from '../../set-up'; -import { createMember, mockWalletReturnValue, submitMilestoneFunc } from '../common'; +import { createMember, createSpace, mockWalletReturnValue, submitMilestoneFunc } from '../common'; export class Helper { public spy: any = {} as any; + public space: Space = {} as any; public member: string = {} as any; public members: string[] = []; public auction: Auction = {} as any; @@ -19,10 +20,18 @@ export class Helper { public beforeEach = async (now: dayjs.Dayjs) => { this.member = await createMember(this.spy); + this.space = await createSpace(this.spy, this.member); const memberPromises = Array.from(Array(3)).map(() => createMember(this.spy)); this.members = await Promise.all(memberPromises); - mockWalletReturnValue(this.spy, this.member, auctionRequest(now)); + await this.createAuction(now); + }; + + public createAuction = async (now: dayjs.Dayjs, customAuctionParams?: { [key: string]: any }) => { + mockWalletReturnValue(this.spy, this.member, { + ...auctionRequest(this.space.uid, now), + ...customAuctionParams, + }); this.auction = await testEnv.wrap(auctionCreate)({}); this.auctionDocRef = build5Db().doc(`${COL.AUCTION}/${this.auction.uid}`); }; @@ -35,7 +44,8 @@ export class Helper { }; } -const auctionRequest = (now: dayjs.Dayjs, auctionLength = 60000 * 4) => ({ +const auctionRequest = (space: string, now: dayjs.Dayjs, auctionLength = 60000 * 4) => ({ + space, auctionFrom: now.toDate(), auctionFloorPrice: 2 * MIN_IOTA_AMOUNT, auctionLength, diff --git a/packages/functions/test/controls/auction/auction.bid.spec.ts b/packages/functions/test/controls/auction/auction.bid.spec.ts index 5526557426..5507ffa948 100644 --- a/packages/functions/test/controls/auction/auction.bid.spec.ts +++ b/packages/functions/test/controls/auction/auction.bid.spec.ts @@ -1,16 +1,20 @@ -import { build5Db } from '@build-5/database'; +import { IQuery, build5Db } from '@build-5/database'; import { Auction, AuctionType, COL, MIN_IOTA_AMOUNT, + Member, Network, Transaction, TransactionType, } from '@build-5/interfaces'; import dayjs from 'dayjs'; import { finalizeAuctions } from '../../../src/cron/auction.cron'; +import { getAddress } from '../../../src/utils/address.utils'; import { dateToTimestamp } from '../../../src/utils/dateTime.utils'; +import { getWallet } from '../../set-up'; +import { wait } from '../common'; import { Helper } from './Helper'; describe('Open auction bid', () => { @@ -80,12 +84,135 @@ describe('Open auction bid', () => { expect(credits[0].member).toBe(h.members[1]); }); - it('Should finalize auction', async () => { + it('Should finalize open auction', async () => { await h.bidOnAuction(h.members[0], 2 * MIN_IOTA_AMOUNT); + await h.bidOnAuction(h.members[0], 3 * MIN_IOTA_AMOUNT); const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${h.auction.uid}`); await auctionDocRef.update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'minute')) }); await finalizeAuctions(); + + const memberDocRef = build5Db().doc(`${COL.MEMBER}/${h.member}`); + const member = await memberDocRef.get(); + const address = getAddress(member, h.auction.network); + + const billPayments = await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.BILL_PAYMENT) + .where('member', '==', h.members[0]) + .get(); + billPayments.sort((a, b) => a.payload.amount! - b.payload.amount!); + expect(billPayments.length).toBe(2); + expect(billPayments[0].payload.amount!).toBe(2 * MIN_IOTA_AMOUNT); + expect(billPayments[0].payload.targetAddress).toBe(address); + expect(billPayments[1].payload.amount!).toBe(3 * MIN_IOTA_AMOUNT); + expect(billPayments[1].payload.targetAddress).toBe(address); + }); + + it('Should finalize open auction with custom target address', async () => { + const wallet = await getWallet(h.auction.network); + const targetAddress = await wallet.getNewIotaAddressDetails(); + + await h.createAuction(dayjs(), { targetAddress: targetAddress.bech32 }); + + await h.bidOnAuction(h.members[0], 2 * MIN_IOTA_AMOUNT); + await h.bidOnAuction(h.members[0], 3 * MIN_IOTA_AMOUNT); + + const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${h.auction.uid}`); + await auctionDocRef.update({ auctionTo: dateToTimestamp(dayjs().subtract(1, 'minute')) }); + + await finalizeAuctions(); + + const billPayments = await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.BILL_PAYMENT) + .where('member', '==', h.members[0]) + .get(); + billPayments.sort((a, b) => a.payload.amount! - b.payload.amount!); + expect(billPayments.length).toBe(2); + expect(billPayments[0].payload.amount!).toBe(2 * MIN_IOTA_AMOUNT); + expect(billPayments[0].payload.targetAddress).toBe(targetAddress.bech32); + expect(billPayments[1].payload.amount!).toBe(3 * MIN_IOTA_AMOUNT); + expect(billPayments[1].payload.targetAddress).toBe(targetAddress.bech32); + }); + + const awaitPayments = (query: IQuery, count: number) => + wait(async () => { + const snap = await query.get(); + return snap.length === count; + }); + + it('Should bid when custom bid increment, topUpBased', async () => { + await h.createAuction(dayjs(), { minimalBidIncrement: 1.5 * MIN_IOTA_AMOUNT }); + + const validPaymentsQuery = build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.PAYMENT) + .where('member', '==', h.members[0]) + .where('payload.invalidPayment', '==', false); + const invalidPaymentsQuery = build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.PAYMENT) + .where('member', '==', h.members[0]) + .where('payload.invalidPayment', '==', true); + + // Below floor, credit + await h.bidOnAuction(h.members[0], 1.5 * MIN_IOTA_AMOUNT); + await awaitPayments(invalidPaymentsQuery, 1); + await awaitPayments(validPaymentsQuery, 0); + + // Should be valid + await h.bidOnAuction(h.members[0], 2 * MIN_IOTA_AMOUNT); + await awaitPayments(invalidPaymentsQuery, 1); + await awaitPayments(validPaymentsQuery, 1); + + // Below minimal bid, credit + await h.bidOnAuction(h.members[0], MIN_IOTA_AMOUNT); + await awaitPayments(invalidPaymentsQuery, 2); + await awaitPayments(validPaymentsQuery, 1); + + // Should be valid + await h.bidOnAuction(h.members[0], 1.5 * MIN_IOTA_AMOUNT); + await awaitPayments(invalidPaymentsQuery, 2); + await awaitPayments(validPaymentsQuery, 2); + }); + + it('Should bid when custom bid increment, not topUpBased', async () => { + await h.createAuction(dayjs(), { + minimalBidIncrement: 1.5 * MIN_IOTA_AMOUNT, + topUpBased: false, + }); + + const validPaymentsQuery = build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.PAYMENT) + .where('member', '==', h.members[0]) + .where('payload.invalidPayment', '==', false); + const invalidPaymentsQuery = build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.PAYMENT) + .where('member', '==', h.members[0]) + .where('payload.invalidPayment', '==', true); + + // Below floor, credit + await h.bidOnAuction(h.members[0], 1.5 * MIN_IOTA_AMOUNT); + await awaitPayments(invalidPaymentsQuery, 1); + await awaitPayments(validPaymentsQuery, 0); + + // Should be valid + await h.bidOnAuction(h.members[0], 2 * MIN_IOTA_AMOUNT); + await awaitPayments(invalidPaymentsQuery, 1); + await awaitPayments(validPaymentsQuery, 1); + + // Below minimal bid, credit + await h.bidOnAuction(h.members[0], 3 * MIN_IOTA_AMOUNT); + await awaitPayments(invalidPaymentsQuery, 2); + await awaitPayments(validPaymentsQuery, 1); + + // Should be valid + await h.bidOnAuction(h.members[0], 3.5 * MIN_IOTA_AMOUNT); + await awaitPayments(invalidPaymentsQuery, 3); + await awaitPayments(validPaymentsQuery, 1); }); }); diff --git a/packages/functions/test/controls/nft/Helper.ts b/packages/functions/test/controls/nft/Helper.ts index e5de63b000..b6efb9e3a9 100644 --- a/packages/functions/test/controls/nft/Helper.ts +++ b/packages/functions/test/controls/nft/Helper.ts @@ -12,7 +12,7 @@ import { } from '@build-5/interfaces'; import dayjs from 'dayjs'; import { approveCollection, createCollection } from '../../../src/runtime/firebase/collection'; -import { openBid, createNft, orderNft } from '../../../src/runtime/firebase/nft'; +import { createNft, openBid, orderNft } from '../../../src/runtime/firebase/nft'; import * as wallet from '../../../src/utils/wallet.utils'; import { MEDIA, testEnv } from '../../set-up'; import { @@ -117,6 +117,7 @@ export const dummyAuctionData = ( availableFrom: from.toDate(), auctionFrom: from.toDate(), auctionFloorPrice: MIN_IOTA_AMOUNT, + minimalBidIncrement: MIN_IOTA_AMOUNT, auctionLength, access: NftAccess.OPEN, }); diff --git a/packages/functions/test/controls/nft/nft.bidding.spec.ts b/packages/functions/test/controls/nft/nft.bidding.spec.ts index 8aa8599fed..6e0411cbd6 100644 --- a/packages/functions/test/controls/nft/nft.bidding.spec.ts +++ b/packages/functions/test/controls/nft/nft.bidding.spec.ts @@ -164,6 +164,25 @@ describe('Nft bidding', () => { expect(snap.length).toBe(1); }); + it('Should reject bid where min inc is too small', async () => { + const nftDocRef = build5Db().doc(`${COL.NFT}/${h.nft.uid}`); + await h.bidNft(h.members[0], MIN_IOTA_AMOUNT); + + h.nft = await nftDocRef.get(); + expect(h.nft.auctionHighestBidder).toBe(h.members[0]); + + await h.bidNft(h.members[0], 1.5 * MIN_IOTA_AMOUNT); + + const snap = await build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.CREDIT) + .where('member', '==', h.members[0]) + .where('payload.nft', '==', h.nft.uid) + .get(); + expect(snap.length).toBe(1); + expect(snap[0].payload.amount).toBe(1.5 * MIN_IOTA_AMOUNT); + }); + it('Should bid in parallel', async () => { const bidPromises = [ h.bidNft(h.members[0], 2 * MIN_IOTA_AMOUNT), diff --git a/packages/interfaces/src/api/post/AuctionCreateRequest.ts b/packages/interfaces/src/api/post/AuctionCreateRequest.ts index 5d79efa800..0092b65d46 100644 --- a/packages/interfaces/src/api/post/AuctionCreateRequest.ts +++ b/packages/interfaces/src/api/post/AuctionCreateRequest.ts @@ -31,10 +31,22 @@ export interface AuctionCreateRequest { * Specifies the maximum number of active bids. Minimum 1, maximum 10 */ maxBids: number; + /** + * Defines the minimum increment of a subsequent bid. Minimum 1000000, maximum 1000000000000 + */ + minimalBidIncrement?: number; /** * Network on which this auction accepts bids. */ network: 'iota' | 'smr' | 'atoi' | 'rms'; + /** + * Build5 id of the space + */ + space: string; + /** + * A valid network address where funds will be sent. + */ + targetAddress?: string; /** * If set to true, consequent bids from the same user will be treated as topups */ diff --git a/packages/interfaces/src/api/post/NftSetForSaleRequest.ts b/packages/interfaces/src/api/post/NftSetForSaleRequest.ts index 3941e7a4cb..e02f90067c 100644 --- a/packages/interfaces/src/api/post/NftSetForSaleRequest.ts +++ b/packages/interfaces/src/api/post/NftSetForSaleRequest.ts @@ -39,6 +39,10 @@ export interface NftSetForSaleRequest { * If set, auction will automatically extended by this length if a bid comes in within {@link extendAuctionWithin} before the end of the auction. */ extendedAuctionLength?: number; + /** + * Defines the minimum increment of a subsequent bid. Minimum 1000000, maximum 1000000000000 + */ + minimalBidIncrement?: number; /** * Build5 id of the nft. */ diff --git a/packages/interfaces/src/api/tangle/AuctionCreateTangleRequest.ts b/packages/interfaces/src/api/tangle/AuctionCreateTangleRequest.ts index a17ee993c4..5df4884551 100644 --- a/packages/interfaces/src/api/tangle/AuctionCreateTangleRequest.ts +++ b/packages/interfaces/src/api/tangle/AuctionCreateTangleRequest.ts @@ -31,6 +31,10 @@ export interface AuctionCreateTangleRequest { * Specifies the maximum number of active bids. Minimum 1, maximum 10 */ maxBids: number; + /** + * Defines the minimum increment of a subsequent bid. Minimum 1000000, maximum 1000000000000 + */ + minimalBidIncrement?: number; /** * Network on which this auction accepts bids. */ @@ -39,6 +43,14 @@ export interface AuctionCreateTangleRequest { * Type of the tangle request. */ requestType: 'CREATE_AUCTION'; + /** + * Build5 id of the space + */ + space: string; + /** + * A valid network address where funds will be sent. + */ + targetAddress?: string; /** * If set to true, consequent bids from the same user will be treated as topups */ diff --git a/packages/interfaces/src/api/tangle/NftSetForSaleTangleRequest.ts b/packages/interfaces/src/api/tangle/NftSetForSaleTangleRequest.ts index 562124b98b..6a3f156e41 100644 --- a/packages/interfaces/src/api/tangle/NftSetForSaleTangleRequest.ts +++ b/packages/interfaces/src/api/tangle/NftSetForSaleTangleRequest.ts @@ -39,6 +39,10 @@ export interface NftSetForSaleTangleRequest { * If set, auction will automatically extended by this length if a bid comes in within {@link extendAuctionWithin} before the end of the auction. */ extendedAuctionLength?: number; + /** + * Defines the minimum increment of a subsequent bid. Minimum 1000000, maximum 1000000000000 + */ + minimalBidIncrement?: number; /** * Build5 id of the nft. */ diff --git a/packages/interfaces/src/models/auction.ts b/packages/interfaces/src/models/auction.ts index 364e0639e0..d8658e0570 100644 --- a/packages/interfaces/src/models/auction.ts +++ b/packages/interfaces/src/models/auction.ts @@ -13,6 +13,8 @@ export enum AuctionType { } export interface Auction extends BaseRecord { + space: string; + auctionFrom: Timestamp; auctionTo: Timestamp; auctionLength: number; @@ -22,6 +24,7 @@ export interface Auction extends BaseRecord { extendAuctionWithin?: number | null; auctionFloorPrice: number; + minimalBidIncrement: number; bids: AuctionBid[]; auctionHighestBidder?: string; @@ -32,6 +35,8 @@ export interface Auction extends BaseRecord { network: Network; nftId?: string; + targetAddress?: string; + active: boolean; topUpBased?: boolean; }