From b21af43570e1907dbe17e69a945357a8f0576b27 Mon Sep 17 00:00:00 2001 From: Boldizsar Mezei Date: Thu, 11 Jan 2024 17:30:21 +0100 Subject: [PATCH] Bulk nft purchase Fixes Fixes --- ...ions_tangle-online-unit-tests_emulator.yml | 196 +++++++--- .../workflows/functions_tangle-unit-tests.yml | 208 ++++++---- .../nft/NftPurchaseBulkRequestSchema.ts | 19 + .../controls/nft/NftPurchaseRequestSchema.ts | 2 +- .../controls/nft/nft.puchase.bulk.control.ts | 17 + .../src/runtime/firebase/nft/index.ts | 2 + packages/functions/src/runtime/https/index.ts | 8 + .../payment/nft/nft-purchase.bulk.service.ts | 189 ++++++++++ .../payment/nft/nft-purchase.service.ts | 2 +- .../services/payment/payment-processing.ts | 3 + .../tangle-service/TangleRequestService.ts | 3 + .../nft/NftBidTangleRequestSchema.ts | 2 +- .../nft/NftPurchaseBulkTangleRequestSchema.ts | 34 ++ .../nft/NftPurchaseTangleRequestSchema.ts | 2 +- .../nft/nft-purchase.bulk.service.ts | 224 +++++++++++ .../nft/nft-purchase.service.ts | 28 +- .../src/services/wallet/IotaWalletService.ts | 10 +- .../src/services/wallet/wallet.service.ts | 9 +- .../functions/src/services/wallet/wallet.ts | 10 +- .../transaction.trigger.ts | 4 + .../functions/test-tangle/nft-bulk/Helper.ts | 107 ++++++ .../test-tangle/nft-bulk/order.bulk_1.spec.ts | 43 +++ .../test-tangle/nft-bulk/order.bulk_2.spec.ts | 62 +++ .../test-tangle/nft-bulk/order.bulk_3.spec.ts | 55 +++ .../test-tangle/nft-bulk/order.bulk_4.spec.ts | 53 +++ .../test-tangle/nft-bulk/order.bulk_5.spec.ts | 86 +++++ .../test-tangle/nft-bulk/order.bulk_6.spec.ts | 76 ++++ packages/functions/test/set-up.ts | 7 +- packages/interfaces/src/config.ts | 2 + packages/interfaces/src/errors.ts | 4 + packages/interfaces/src/functions/index.ts | 1 + .../src/models/transaction/common.ts | 43 +++ .../src/models/transaction/payload.ts | 357 +++++++++++++++++- .../src/search/post/NftPurchaseBulkRequest.ts | 16 + .../src/search/post/NftPurchaseRequest.ts | 2 +- packages/interfaces/src/search/post/index.ts | 1 + .../tangle/NftPurchaseBulkTangleRequest.ts | 31 ++ .../search/tangle/NftPurchaseTangleRequest.ts | 2 +- .../interfaces/src/search/tangle/common.ts | 1 + .../interfaces/src/search/tangle/index.ts | 2 + .../examples/nft/bulk/nft.bulk.purhcase.ts | 54 +++ .../nft/bulk/nft.otr.buld.purchase.ts | 33 ++ packages/sdk/src/https/datasets/NftDataset.ts | 3 + packages/sdk/src/index.ts | 4 +- .../sdk/src/otr/datasets/NftOtrDataset.ts | 7 + 45 files changed, 1846 insertions(+), 178 deletions(-) create mode 100644 packages/functions/src/controls/nft/NftPurchaseBulkRequestSchema.ts create mode 100644 packages/functions/src/controls/nft/nft.puchase.bulk.control.ts create mode 100644 packages/functions/src/services/payment/nft/nft-purchase.bulk.service.ts create mode 100644 packages/functions/src/services/payment/tangle-service/nft/NftPurchaseBulkTangleRequestSchema.ts create mode 100644 packages/functions/src/services/payment/tangle-service/nft/nft-purchase.bulk.service.ts create mode 100644 packages/functions/test-tangle/nft-bulk/Helper.ts create mode 100644 packages/functions/test-tangle/nft-bulk/order.bulk_1.spec.ts create mode 100644 packages/functions/test-tangle/nft-bulk/order.bulk_2.spec.ts create mode 100644 packages/functions/test-tangle/nft-bulk/order.bulk_3.spec.ts create mode 100644 packages/functions/test-tangle/nft-bulk/order.bulk_4.spec.ts create mode 100644 packages/functions/test-tangle/nft-bulk/order.bulk_5.spec.ts create mode 100644 packages/functions/test-tangle/nft-bulk/order.bulk_6.spec.ts create mode 100644 packages/interfaces/src/search/post/NftPurchaseBulkRequest.ts create mode 100644 packages/interfaces/src/search/tangle/NftPurchaseBulkTangleRequest.ts create mode 100644 packages/sdk/examples/nft/bulk/nft.bulk.purhcase.ts create mode 100644 packages/sdk/examples/nft/bulk/nft.otr.buld.purchase.ts diff --git a/.github/workflows/functions_tangle-online-unit-tests_emulator.yml b/.github/workflows/functions_tangle-online-unit-tests_emulator.yml index ceef05f8de..bd55c3bf8c 100644 --- a/.github/workflows/functions_tangle-online-unit-tests_emulator.yml +++ b/.github/workflows/functions_tangle-online-unit-tests_emulator.yml @@ -1472,6 +1472,80 @@ jobs: path: ./packages/functions/firestore-data/ retention-days: 1 chunk_39: + needs: npm-install + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.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/nft-bulk/order.bulk_1.spec.ts && + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/nft-bulk/order.bulk_2.spec.ts && + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/nft-bulk/order.bulk_3.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_39 + path: ./packages/functions/firestore-data/ + retention-days: 1 + chunk_40: + needs: npm-install + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.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/nft-bulk/order.bulk_4.spec.ts && + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/nft-bulk/order.bulk_5.spec.ts && + npm run test-tangle-online:ci -- --forceExit --findRelatedTests ./test-tangle/nft-bulk/order.bulk_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_40 + path: ./packages/functions/firestore-data/ + retention-days: 1 + chunk_41: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1505,10 +1579,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_41 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_40: + chunk_42: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1542,10 +1616,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_42 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_41: + chunk_43: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1579,10 +1653,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_43 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_42: + chunk_44: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1616,10 +1690,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_44 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_43: + chunk_45: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1653,10 +1727,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_45 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_44: + chunk_46: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1690,10 +1764,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_46 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_45: + chunk_47: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1727,10 +1801,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_47 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_46: + chunk_48: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1764,10 +1838,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_48 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_47: + chunk_49: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1801,10 +1875,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_49 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_48: + chunk_50: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1838,10 +1912,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_50 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_49: + chunk_51: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1875,10 +1949,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_51 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_50: + chunk_52: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1912,10 +1986,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_52 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_51: + chunk_53: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1949,10 +2023,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_53 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_52: + chunk_54: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1986,10 +2060,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_54 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_53: + chunk_55: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2023,10 +2097,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_55 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_54: + chunk_56: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2060,10 +2134,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_56 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_55: + chunk_57: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2097,10 +2171,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_57 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_56: + chunk_58: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2134,10 +2208,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_58 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_57: + chunk_59: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2171,10 +2245,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_59 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_58: + chunk_60: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2208,10 +2282,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_60 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_59: + chunk_61: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2245,10 +2319,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_61 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_60: + chunk_62: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2282,10 +2356,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_62 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_61: + chunk_63: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2319,10 +2393,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_63 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_62: + chunk_64: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2356,10 +2430,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_64 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_63: + chunk_65: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2393,10 +2467,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_63 + name: firestore-data-test-tangle-online-chunk_65 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_64: + chunk_66: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2430,10 +2504,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_64 + name: firestore-data-test-tangle-online-chunk_66 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_65: + chunk_67: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2467,10 +2541,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_65 + name: firestore-data-test-tangle-online-chunk_67 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_66: + chunk_68: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2504,10 +2578,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_66 + name: firestore-data-test-tangle-online-chunk_68 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_67: + chunk_69: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2541,10 +2615,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_67 + name: firestore-data-test-tangle-online-chunk_69 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_68: + chunk_70: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2578,10 +2652,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_68 + name: firestore-data-test-tangle-online-chunk_70 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_69: + chunk_71: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2613,6 +2687,6 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-online-chunk_69 + name: firestore-data-test-tangle-online-chunk_71 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 4ba2338ff4..7308db1e9c 100644 --- a/.github/workflows/functions_tangle-unit-tests.yml +++ b/.github/workflows/functions_tangle-unit-tests.yml @@ -1470,6 +1470,80 @@ jobs: path: ./packages/functions/firestore-data/ retention-days: 1 chunk_39: + needs: npm-install + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.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/nft-bulk/order.bulk_1.spec.ts && + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/nft-bulk/order.bulk_2.spec.ts && + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/nft-bulk/order.bulk_3.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_39 + path: ./packages/functions/firestore-data/ + retention-days: 1 + chunk_40: + needs: npm-install + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.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/nft-bulk/order.bulk_4.spec.ts && + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/nft-bulk/order.bulk_5.spec.ts && + npm run test-tangle:ci -- --forceExit --findRelatedTests ./test-tangle/nft-bulk/order.bulk_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_40 + path: ./packages/functions/firestore-data/ + retention-days: 1 + chunk_41: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1503,10 +1577,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_39 + name: firestore-data-test-tangle-chunk_41 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_40: + chunk_42: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1540,10 +1614,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_40 + name: firestore-data-test-tangle-chunk_42 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_41: + chunk_43: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1577,10 +1651,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_41 + name: firestore-data-test-tangle-chunk_43 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_42: + chunk_44: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1614,10 +1688,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_42 + name: firestore-data-test-tangle-chunk_44 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_43: + chunk_45: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1651,10 +1725,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_43 + name: firestore-data-test-tangle-chunk_45 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_44: + chunk_46: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1688,10 +1762,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_44 + name: firestore-data-test-tangle-chunk_46 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_45: + chunk_47: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1725,10 +1799,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_45 + name: firestore-data-test-tangle-chunk_47 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_46: + chunk_48: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1762,10 +1836,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_46 + name: firestore-data-test-tangle-chunk_48 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_47: + chunk_49: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1799,10 +1873,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_47 + name: firestore-data-test-tangle-chunk_49 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_48: + chunk_50: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1836,10 +1910,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_48 + name: firestore-data-test-tangle-chunk_50 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_49: + chunk_51: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1873,10 +1947,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_49 + name: firestore-data-test-tangle-chunk_51 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_50: + chunk_52: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1910,10 +1984,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_50 + name: firestore-data-test-tangle-chunk_52 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_51: + chunk_53: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1947,10 +2021,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_51 + name: firestore-data-test-tangle-chunk_53 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_52: + chunk_54: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -1984,10 +2058,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_52 + name: firestore-data-test-tangle-chunk_54 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_53: + chunk_55: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2021,10 +2095,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_53 + name: firestore-data-test-tangle-chunk_55 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_54: + chunk_56: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2058,10 +2132,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_54 + name: firestore-data-test-tangle-chunk_56 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_55: + chunk_57: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2095,10 +2169,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_55 + name: firestore-data-test-tangle-chunk_57 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_56: + chunk_58: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2132,10 +2206,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_56 + name: firestore-data-test-tangle-chunk_58 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_57: + chunk_59: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2169,10 +2243,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_57 + name: firestore-data-test-tangle-chunk_59 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_58: + chunk_60: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2206,10 +2280,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_58 + name: firestore-data-test-tangle-chunk_60 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_59: + chunk_61: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2243,10 +2317,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_59 + name: firestore-data-test-tangle-chunk_61 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_60: + chunk_62: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2280,10 +2354,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_60 + name: firestore-data-test-tangle-chunk_62 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_61: + chunk_63: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2317,10 +2391,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_61 + name: firestore-data-test-tangle-chunk_63 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_62: + chunk_64: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2354,10 +2428,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_62 + name: firestore-data-test-tangle-chunk_64 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_63: + chunk_65: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2391,10 +2465,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_63 + name: firestore-data-test-tangle-chunk_65 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_64: + chunk_66: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2428,10 +2502,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_64 + name: firestore-data-test-tangle-chunk_66 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_65: + chunk_67: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2465,10 +2539,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_65 + name: firestore-data-test-tangle-chunk_67 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_66: + chunk_68: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2502,10 +2576,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_66 + name: firestore-data-test-tangle-chunk_68 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_67: + chunk_69: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2539,10 +2613,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_67 + name: firestore-data-test-tangle-chunk_69 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_68: + chunk_70: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2576,10 +2650,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_68 + name: firestore-data-test-tangle-chunk_70 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_69: + chunk_71: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2611,10 +2685,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_69 + name: firestore-data-test-tangle-chunk_71 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_70: + chunk_72: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2646,10 +2720,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_70 + name: firestore-data-test-tangle-chunk_72 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_71: + chunk_73: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2681,10 +2755,10 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_71 + name: firestore-data-test-tangle-chunk_73 path: ./packages/functions/firestore-data/ retention-days: 1 - chunk_72: + chunk_74: needs: npm-install runs-on: ubuntu-latest timeout-minutes: 20 @@ -2716,6 +2790,6 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() }} with: - name: firestore-data-test-tangle-chunk_72 + name: firestore-data-test-tangle-chunk_74 path: ./packages/functions/firestore-data/ retention-days: 1 diff --git a/packages/functions/src/controls/nft/NftPurchaseBulkRequestSchema.ts b/packages/functions/src/controls/nft/NftPurchaseBulkRequestSchema.ts new file mode 100644 index 0000000000..810d63d53f --- /dev/null +++ b/packages/functions/src/controls/nft/NftPurchaseBulkRequestSchema.ts @@ -0,0 +1,19 @@ +import { MAX_NFT_BULK_PURCHASE, NftPurchaseBulkRequest } from '@build-5/interfaces'; +import Joi from 'joi'; +import { toJoiObject } from '../../services/joi/common'; +import { nftPurchaseSchema } from './NftPurchaseRequestSchema'; + +export const nftPurchaseBulkSchema = toJoiObject({ + orders: Joi.array() + .items(nftPurchaseSchema) + .min(1) + .max(MAX_NFT_BULK_PURCHASE) + .description( + `List of collections&nfts to purchase, minimum 1, maximum ${MAX_NFT_BULK_PURCHASE}`, + ) + .required(), +}) + .description('Request object to create an NFT bulk purchase order') + .meta({ + className: 'NftPurchaseBulkRequest', + }); diff --git a/packages/functions/src/controls/nft/NftPurchaseRequestSchema.ts b/packages/functions/src/controls/nft/NftPurchaseRequestSchema.ts index d55e7bf5cc..eb208c9197 100644 --- a/packages/functions/src/controls/nft/NftPurchaseRequestSchema.ts +++ b/packages/functions/src/controls/nft/NftPurchaseRequestSchema.ts @@ -5,7 +5,7 @@ export const nftPurchaseSchema = toJoiObject({ collection: CommonJoi.uid().description( 'Build5 id of the collection in case a random nft is bought.', ), - nft: CommonJoi.uid(false).description('Build5 if of the nft to be purchased.'), + nft: CommonJoi.uid(false).description('Build5 id of the nft to be purchased.'), }) .description('Request object to create an NFT purchase order') .meta({ diff --git a/packages/functions/src/controls/nft/nft.puchase.bulk.control.ts b/packages/functions/src/controls/nft/nft.puchase.bulk.control.ts new file mode 100644 index 0000000000..88df487fbe --- /dev/null +++ b/packages/functions/src/controls/nft/nft.puchase.bulk.control.ts @@ -0,0 +1,17 @@ +import { build5Db } from '@build-5/database'; +import { COL, NftPurchaseBulkRequest, Transaction } from '@build-5/interfaces'; +import { createNftBulkOrder } from '../../services/payment/tangle-service/nft/nft-purchase.bulk.service'; +import { Context } from '../common'; + +export const orderNftBulkControl = async ({ + ip, + owner, + params, + project, +}: Context): Promise => { + const order = await createNftBulkOrder(project, params.orders, owner, ip); + const orderDocRef = build5Db().doc(`${COL.TRANSACTION}/${order.uid}`); + await orderDocRef.create(order); + + return (await orderDocRef.get())!; +}; diff --git a/packages/functions/src/runtime/firebase/nft/index.ts b/packages/functions/src/runtime/firebase/nft/index.ts index 6802e440cf..f1a709dcbd 100644 --- a/packages/functions/src/runtime/firebase/nft/index.ts +++ b/packages/functions/src/runtime/firebase/nft/index.ts @@ -14,6 +14,8 @@ export const depositNft = https[WEN_FUNC.depositNft]; export const orderNft = https[WEN_FUNC.orderNft]; +export const orderNftBulk = https[WEN_FUNC.orderNftBulk]; + export const stakeNft = https[WEN_FUNC.stakeNft]; export const openBid = https[WEN_FUNC.openBid]; diff --git a/packages/functions/src/runtime/https/index.ts b/packages/functions/src/runtime/https/index.ts index 2a0dab01c0..9a880f79b0 100644 --- a/packages/functions/src/runtime/https/index.ts +++ b/packages/functions/src/runtime/https/index.ts @@ -37,6 +37,7 @@ import { updateMemberControl } from '../../controls/member/member.update'; import { nftBidSchema } from '../../controls/nft/NftBidRequestSchema'; import { createSchema, nftCreateSchema } from '../../controls/nft/NftCreateRequestSchema'; import { depositNftSchema } from '../../controls/nft/NftDepositRequestSchema'; +import { nftPurchaseBulkSchema } from '../../controls/nft/NftPurchaseBulkRequestSchema'; import { nftPurchaseSchema } from '../../controls/nft/NftPurchaseRequestSchema'; import { setNftForSaleSchema } from '../../controls/nft/NftSetForSaleRequestSchema'; import { stakeNftSchema } from '../../controls/nft/NftStakeRequestSchema'; @@ -45,6 +46,7 @@ import { nftWithdrawSchema } from '../../controls/nft/NftWithdrawRequestSchema'; import { nftBidControl } from '../../controls/nft/nft.bid.control'; import { createBatchNftControl, createNftControl } from '../../controls/nft/nft.create'; import { depositNftControl } from '../../controls/nft/nft.deposit'; +import { orderNftBulkControl } from '../../controls/nft/nft.puchase.bulk.control'; import { orderNftControl } from '../../controls/nft/nft.puchase.control'; import { setForSaleNftControl } from '../../controls/nft/nft.set.for.sale'; import { nftStakeControl } from '../../controls/nft/nft.stake'; @@ -344,6 +346,12 @@ exports[WEN_FUNC.orderNft] = onRequest({ handler: orderNftControl, }); +exports[WEN_FUNC.orderNftBulk] = onRequest({ + name: WEN_FUNC.orderNftBulk, + schema: nftPurchaseBulkSchema, + handler: orderNftBulkControl, +}); + exports[WEN_FUNC.openBid] = onRequest({ name: WEN_FUNC.openBid, schema: nftBidSchema, diff --git a/packages/functions/src/services/payment/nft/nft-purchase.bulk.service.ts b/packages/functions/src/services/payment/nft/nft-purchase.bulk.service.ts new file mode 100644 index 0000000000..af35c4d344 --- /dev/null +++ b/packages/functions/src/services/payment/nft/nft-purchase.bulk.service.ts @@ -0,0 +1,189 @@ +import { build5Db } from '@build-5/database'; +import { + COL, + Collection, + Entity, + Nft, + NftBulkOrder, + SUB_COL, + Space, + TRANSACTION_AUTO_EXPIRY_MS, + Transaction, + TransactionPayloadType, + TransactionType, + TransactionValidationType, + getMilestoneCol, +} from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { get } from 'lodash'; +import { getAddress } from '../../../utils/address.utils'; +import { getRestrictions } from '../../../utils/common.utils'; +import { dateToTimestamp } from '../../../utils/dateTime.utils'; +import { getSpace } from '../../../utils/space.utils'; +import { getRandomEthAddress } from '../../../utils/wallet.utils'; +import { WalletService } from '../../wallet/wallet.service'; +import { BaseService, HandlerParams } from '../base'; +import { assertNftCanBePurchased, getMember } from '../tangle-service/nft/nft-purchase.service'; +import { NftPurchaseService } from './nft-purchase.service'; + +export class NftPurchaseBulkService extends BaseService { + public handleRequest = async ({ order, match, tranEntry, tran, project }: HandlerParams) => { + const payment = await this.transactionService.createPayment(order, match); + + const promises = (order.payload.nftOrders || []).map((nftOrder) => + this.createNftPurchaseOrder(project, order, nftOrder), + ); + const nftOrders = await Promise.all(promises); + + const orderDocRef = build5Db().doc(`${COL.TRANSACTION}/${order.uid}`); + this.transactionService.push({ + ref: orderDocRef, + data: { + 'payload.nftOrders': nftOrders, + 'payload.reconciled': true, + 'payload.chainReference': match.msgId, + }, + action: 'update', + }); + + const total = nftOrders.reduce((acc, act) => acc + act.price, 0); + if (total < tranEntry.amount) { + const credit = { + project, + type: TransactionType.CREDIT, + uid: getRandomEthAddress(), + space: order.space, + member: order.member || match.from, + network: order.network, + payload: { + type: TransactionPayloadType.NFT_PURCHASE_BULK, + amount: tranEntry.amount - total, + sourceAddress: order.payload.targetAddress, + targetAddress: match.from, + sourceTransaction: [payment.uid], + reconciled: false, + void: false, + }, + }; + const docRef = build5Db().doc(`${COL.TRANSACTION}/${credit.uid}`); + this.transactionService.push({ ref: docRef, data: credit, action: 'set' }); + } + + if (total) { + const targetAddresses = nftOrders + .filter((o) => o.price > 0) + .map((o) => ({ toAddress: o.targetAddress!, amount: o.price })); + const transfer: Transaction = { + project, + type: TransactionType.UNLOCK, + uid: getRandomEthAddress(), + space: order.space || '', + member: order.member || match.from, + network: order.network, + payload: { + type: TransactionPayloadType.TANGLE_TRANSFER_MANY, + amount: total, + sourceAddress: order.payload.targetAddress, + targetAddresses, + sourceTransaction: [payment.uid], + expiresOn: dateToTimestamp(dayjs().add(TRANSACTION_AUTO_EXPIRY_MS)), + milestoneTransactionPath: `${getMilestoneCol(order.network!)}/${tran.milestone}/${ + SUB_COL.TRANSACTIONS + }/${tran.uid}`, + }, + }; + const docRef = build5Db().doc(`${COL.TRANSACTION}/${transfer.uid}`); + this.transactionService.push({ ref: docRef, data: transfer, action: 'set' }); + } + }; + + private createNftPurchaseOrder = async ( + project: string, + order: Transaction, + nftOrder: NftBulkOrder, + ) => { + if (!nftOrder.price) { + return { ...nftOrder, targetAddress: '' }; + } + + const nftDocRef = build5Db().doc(`${COL.NFT}/${nftOrder.nft}`); + const nft = await this.transactionService.get(nftDocRef); + + const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${nft.collection}`); + const collection = await collectionDocRef.get(); + + const spaceDocRef = build5Db().doc(`${COL.SPACE}/${nft.space}`); + const space = await spaceDocRef.get(); + try { + await assertNftCanBePurchased( + space, + collection, + nft, + nftOrder.requestedNft, + order.member!, + true, + ); + + if (nft.auction) { + const service = new NftPurchaseService(this.transactionService); + await service.creditBids(nft.auction); + } + + const wallet = await WalletService.newWallet(order.network); + const targetAddress = await wallet.getNewIotaAddressDetails(); + + const royaltySpace = await getSpace(collection.royaltiesSpace); + + const nftPurchaseOrderId = getRandomEthAddress(); + + const nftDocRef = build5Db().doc(`${COL.NFT}/${nft.uid}`); + this.transactionService.push({ + ref: nftDocRef, + data: { locked: true, lockedBy: order.uid }, + action: 'update', + }); + + const currentOwner = nft.owner ? await getMember(nft.owner) : space; + + const nftPurchaseOrder = { + project, + type: TransactionType.ORDER, + uid: nftPurchaseOrderId, + member: order.member!, + space: space.uid, + network: order.network, + payload: { + type: TransactionPayloadType.NFT_PURCHASE, + amount: nftOrder.price, + targetAddress: targetAddress.bech32, + beneficiary: nft.owner ? Entity.MEMBER : Entity.SPACE, + beneficiaryUid: nft.owner || collection.space, + beneficiaryAddress: getAddress(currentOwner, order.network), + royaltiesFee: collection.royaltiesFee, + royaltiesSpace: collection.royaltiesSpace || '', + royaltiesSpaceAddress: getAddress(royaltySpace, order.network), + expiresOn: dateToTimestamp(dayjs().add(TRANSACTION_AUTO_EXPIRY_MS)), + validationType: TransactionValidationType.ADDRESS_AND_AMOUNT, + reconciled: false, + void: false, + chainReference: null, + nft: nft.uid, + collection: collection.uid, + restrictions: getRestrictions(collection, nft), + }, + linkedTransactions: [], + }; + const docRef = build5Db().doc(`${COL.TRANSACTION}/${nftPurchaseOrder.uid}`); + this.transactionService.push({ ref: docRef, data: nftPurchaseOrder, action: 'set' }); + + return { ...nftOrder, targetAddress: targetAddress.bech32 }; + } catch (error) { + return { + ...nftOrder, + price: 0, + error: get(error, 'details.code', 0), + targetAddress: '', + } as NftBulkOrder; + } + }; +} 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 d33f533a5b..679fa61f3a 100644 --- a/packages/functions/src/services/payment/nft/nft-purchase.service.ts +++ b/packages/functions/src/services/payment/nft/nft-purchase.service.ts @@ -42,7 +42,7 @@ export class NftPurchaseService extends BaseService { } }; - private creditBids = async (auctionId: string) => { + public creditBids = async (auctionId: string) => { const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auctionId}`); const auction = await this.transaction.get(auctionDocRef); this.transactionService.push({ diff --git a/packages/functions/src/services/payment/payment-processing.ts b/packages/functions/src/services/payment/payment-processing.ts index 740221cb33..3a4b553887 100644 --- a/packages/functions/src/services/payment/payment-processing.ts +++ b/packages/functions/src/services/payment/payment-processing.ts @@ -23,6 +23,7 @@ import { CreditService } from './credit-service'; import { MetadataNftService } from './metadataNft-service'; import { CollectionMintingService } from './nft/collection-minting.service'; import { NftDepositService } from './nft/nft-deposit.service'; +import { NftPurchaseBulkService } from './nft/nft-purchase.bulk.service'; import { NftPurchaseService } from './nft/nft-purchase.service'; import { NftStakeService } from './nft/nft-stake.service'; import { SpaceClaimService } from './space/space-service'; @@ -143,6 +144,8 @@ export class ProcessingService { switch (type) { case TransactionPayloadType.NFT_PURCHASE: return new NftPurchaseService(tranService); + case TransactionPayloadType.NFT_PURCHASE_BULK: + return new NftPurchaseBulkService(tranService); case TransactionPayloadType.NFT_BID: case TransactionPayloadType.AUCTION_BID: return new AuctionBidService(tranService); diff --git a/packages/functions/src/services/payment/tangle-service/TangleRequestService.ts b/packages/functions/src/services/payment/tangle-service/TangleRequestService.ts index 51aa92d845..103ef77872 100644 --- a/packages/functions/src/services/payment/tangle-service/TangleRequestService.ts +++ b/packages/functions/src/services/payment/tangle-service/TangleRequestService.ts @@ -22,6 +22,7 @@ import { AwardCreateService } from './award/award.create.service'; import { AwardFundService } from './award/award.fund.service'; import { MintMetadataNftService } from './metadataNft/mint-metadata-nft.service'; import { NftDepositService } from './nft/nft-deposit.service'; +import { TangleNftPurchaseBulkService } from './nft/nft-purchase.bulk.service'; import { TangleNftPurchaseService } from './nft/nft-purchase.service'; import { TangleNftSetForSaleService } from './nft/nft-set-for-sale.service'; import { ProposalApprovalService } from './proposal/ProposalApporvalService'; @@ -96,6 +97,8 @@ export class TangleRequestService extends BaseTangleService { return new TangleStakeService(this.transactionService); case TangleRequestType.NFT_PURCHASE: return new TangleNftPurchaseService(this.transactionService); + case TangleRequestType.NFT_PURCHASE_BULK: + return new TangleNftPurchaseBulkService(this.transactionService); case TangleRequestType.NFT_SET_FOR_SALE: return new TangleNftSetForSaleService(this.transactionService); case TangleRequestType.NFT_BID: diff --git a/packages/functions/src/services/payment/tangle-service/nft/NftBidTangleRequestSchema.ts b/packages/functions/src/services/payment/tangle-service/nft/NftBidTangleRequestSchema.ts index 904dc790bf..a5ea769043 100644 --- a/packages/functions/src/services/payment/tangle-service/nft/NftBidTangleRequestSchema.ts +++ b/packages/functions/src/services/payment/tangle-service/nft/NftBidTangleRequestSchema.ts @@ -5,7 +5,7 @@ import { baseTangleSchema } from '../common'; export const nftBidSchema = toJoiObject({ ...baseTangleSchema(TangleRequestType.NFT_BID), - nft: CommonJoi.uid().description('Build5 if of the nft to bid on.'), + 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.", ), diff --git a/packages/functions/src/services/payment/tangle-service/nft/NftPurchaseBulkTangleRequestSchema.ts b/packages/functions/src/services/payment/tangle-service/nft/NftPurchaseBulkTangleRequestSchema.ts new file mode 100644 index 0000000000..7a15de3f0c --- /dev/null +++ b/packages/functions/src/services/payment/tangle-service/nft/NftPurchaseBulkTangleRequestSchema.ts @@ -0,0 +1,34 @@ +import { + MAX_NFT_BULK_PURCHASE, + NftPurchaseBulkTangleRequest, + TangleRequestType, +} from '@build-5/interfaces'; +import Joi from 'joi'; +import { CommonJoi, toJoiObject } from '../../../joi/common'; +import { baseTangleSchema } from '../common'; + +const nftPurchaseSchema = Joi.object({ + collection: CommonJoi.uid().description( + 'Build5 id of the collection in case a random nft is bought.', + ), + nft: CommonJoi.uid(false).description('Build5 id of the nft to be purchased.'), +}); + +export const nftPurchaseBulkSchema = toJoiObject({ + ...baseTangleSchema(TangleRequestType.NFT_PURCHASE_BULK), + orders: Joi.array() + .items(nftPurchaseSchema) + .min(1) + .max(MAX_NFT_BULK_PURCHASE) + .description( + `List of collections&nfts to purchase, minimum 1, maximum ${MAX_NFT_BULK_PURCHASE}`, + ) + .required(), + 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 bulk purchase order') + .meta({ + className: 'NftPurchaseBulkTangleRequest', + }); diff --git a/packages/functions/src/services/payment/tangle-service/nft/NftPurchaseTangleRequestSchema.ts b/packages/functions/src/services/payment/tangle-service/nft/NftPurchaseTangleRequestSchema.ts index fce8a484e0..2419adc112 100644 --- a/packages/functions/src/services/payment/tangle-service/nft/NftPurchaseTangleRequestSchema.ts +++ b/packages/functions/src/services/payment/tangle-service/nft/NftPurchaseTangleRequestSchema.ts @@ -8,7 +8,7 @@ export const nftPurchaseSchema = toJoiObject({ collection: CommonJoi.uid().description( 'Build5 id of the collection in case a random nft is bought.', ), - nft: CommonJoi.uid(false).description('Build5 if of the nft to be purchased.'), + nft: CommonJoi.uid(false).description('Build5 id of the nft to be purchased.'), disableWithdraw: Joi.boolean().description( "If set to true, NFT will not be sent to the buyer's validated address upon purchase.", ), diff --git a/packages/functions/src/services/payment/tangle-service/nft/nft-purchase.bulk.service.ts b/packages/functions/src/services/payment/tangle-service/nft/nft-purchase.bulk.service.ts new file mode 100644 index 0000000000..eba0d2ecb2 --- /dev/null +++ b/packages/functions/src/services/payment/tangle-service/nft/nft-purchase.bulk.service.ts @@ -0,0 +1,224 @@ +import { build5Db } from '@build-5/database'; +import { + COL, + Collection, + CollectionType, + Member, + Network, + Nft, + TRANSACTION_AUTO_EXPIRY_MS, + TangleResponse, + Transaction, + TransactionPayloadType, + TransactionType, + TransactionValidationType, + WenError, +} from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { Dictionary, flatten, get, groupBy, isEmpty, uniq } from 'lodash'; +import { getNftByMintingId } from '../../../../utils/collection-minting-utils/nft.utils'; +import { getProject } 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 { assertValidationAsync } from '../../../../utils/schema.utils'; +import { getSpace } from '../../../../utils/space.utils'; +import { getRandomEthAddress } from '../../../../utils/wallet.utils'; +import { WalletService } from '../../../wallet/wallet.service'; +import { BaseTangleService, HandlerParams } from '../../base'; +import { nftPurchaseBulkSchema } from './NftPurchaseBulkTangleRequestSchema'; +import { + assertCurrentOwnerAddress, + assertNftCanBePurchased, + getCollection, + getDiscount, + getMember, + getNftAbove, + getNftBelow, + getNftFinalPrice, +} from './nft-purchase.service'; + +export interface NftBulkOrder { + collection: string; + nft?: string; +} + +export class TangleNftPurchaseBulkService extends BaseTangleService { + public handleRequest = async ({ + order: tangleOrder, + request, + owner, + tran, + tranEntry, + payment, + }: HandlerParams) => { + const params = await assertValidationAsync(nftPurchaseBulkSchema, request); + + const order = await createNftBulkOrder(getProject(tangleOrder), params.orders!, owner); + order.payload.tanglePuchase = true; + order.payload.disableWithdraw = params.disableWithdraw || false; + + this.transactionService.push({ + ref: build5Db().doc(`${COL.TRANSACTION}/${order.uid}`), + data: order, + action: 'set', + }); + + if (tranEntry.amount !== order.payload.amount || tangleOrder.network !== order.network) { + return { + status: 'error', + amount: order.payload.amount!, + address: order.payload.targetAddress!, + code: WenError.invalid_base_token_amount.code, + message: WenError.invalid_base_token_amount.key, + }; + } + + this.transactionService.createUnlockTransaction( + payment, + order, + tran, + tranEntry, + TransactionPayloadType.TANGLE_TRANSFER, + tranEntry.outputId, + ); + + return {}; + }; +} + +export const createNftBulkOrder = async ( + project: string, + orders: NftBulkOrder[], + owner: string, + ip = '', +): Promise => { + const member = await getMember(owner); + + const grouped = groupBy(orders, 'collection'); + const awaitedPrices = await getNftPrices(grouped, member, ip); + + const networks = uniq(flatten(awaitedPrices.map((p) => p.networks))); + if (networks.length > 1) { + throw invalidArgument(WenError.nfts_must_be_within_same_network); + } + + const prices = flatten(awaitedPrices.map((p) => p.prices)); + const finalPrice = prices.reduce((acc, act) => acc + act.price, 0); + if (!finalPrice) { + throw invalidArgument(WenError.no_more_nft_available_for_sale); + } + + const wallet = await WalletService.newWallet(networks[0]); + const targetAddress = await wallet.getNewIotaAddressDetails(); + + return { + project, + type: TransactionType.ORDER, + uid: getRandomEthAddress(), + member: owner, + space: '', + network: networks[0], + payload: { + type: TransactionPayloadType.NFT_PURCHASE_BULK, + amount: finalPrice, + targetAddress: targetAddress.bech32, + expiresOn: dateToTimestamp(dayjs().add(TRANSACTION_AUTO_EXPIRY_MS)), + validationType: TransactionValidationType.ADDRESS_AND_AMOUNT, + reconciled: false, + void: false, + chainReference: null, + nftOrders: prices, + }, + linkedTransactions: [], + }; +}; + +const getNftPrices = async ( + colletionNftGroup: Dictionary, + member: Member, + ip: string, +) => { + const defaultNetwork = isProdEnv() ? Network.IOTA : Network.ATOI; + const pricesPromises = Object.entries(colletionNftGroup).map( + async ([collectionId, nftOrders]) => { + const nftIds = nftOrders.map((o) => o.nft!); + const { collection, nfts } = await getNfts(collectionId, nftIds); + const space = (await getSpace(collection.space))!; + + const discount = getDiscount(collection, member); + + const pricePromises = nfts.map(async (nft) => { + const nftIdParam = nftIds.find((id) => id === nft.uid) || ''; + try { + if (isProdEnv()) { + await assertIpNotBlocked(ip, nft.uid, 'nft'); + } + + await assertNftCanBePurchased(space, collection, nft, nftIdParam, member.uid, true); + + const currentOwner = nft.owner ? await getMember(nft.owner) : space; + assertCurrentOwnerAddress(currentOwner, nft); + + const finalPrice = getNftFinalPrice(nft, discount); + + return { + collection: collectionId, + nft: nft.uid, + requestedNft: nftIdParam, + price: finalPrice, + error: 0, + }; + } catch (error) { + return { + collection: collectionId, + nft: nft.uid, + requestedNft: nftIdParam, + price: 0, + error: get(error, 'details.code', 0), + }; + } + }); + const networks = nfts.map((nft) => nft.mintingData?.network || defaultNetwork); + return { prices: await Promise.all(pricePromises), networks }; + }, + ); + return await Promise.all(pricesPromises); +}; + +const getNfts = async (collectionId: string, nftIds: (string | undefined)[]) => { + const collection = await getCollection(collectionId); + const nftPromises = nftIds.filter((id) => !isEmpty(id)).map((nftId) => getNft(nftId!)); + const randomNfts = await getRandomNft(collection, nftIds.filter((id) => isEmpty(id)).length); + const nfts = (await Promise.all(nftPromises)).concat(randomNfts); + return { collection, nfts }; +}; + +const getNft = async (nftId: string) => { + const docRef = build5Db().doc(`${COL.NFT}/${nftId}`); + const nft = (await getNftByMintingId(nftId)) || (await docRef.get()); + if (nft) { + return nft; + } + throw invalidArgument(WenError.nft_does_not_exists); +}; + +const getRandomNft = async (collection: Collection, count: number) => { + if (!count) { + return []; + } + if (collection.type === CollectionType.CLASSIC) { + throw invalidArgument(WenError.nft_does_not_exists); + } + + const randomPosition = Math.floor(Math.random() * collection.total); + + const nftAbove = await getNftAbove(collection, randomPosition, count); + if (nftAbove.length >= count) { + return nftAbove.slice(0, count); + } + + const nftBelow = await getNftBelow(collection, randomPosition, count - nftAbove.length); + return nftAbove.concat(nftBelow.slice(0, count - nftAbove.length)); +}; diff --git a/packages/functions/src/services/payment/tangle-service/nft/nft-purchase.service.ts b/packages/functions/src/services/payment/tangle-service/nft/nft-purchase.service.ts index c9dd536d01..a947a795f9 100644 --- a/packages/functions/src/services/payment/tangle-service/nft/nft-purchase.service.ts +++ b/packages/functions/src/services/payment/tangle-service/nft/nft-purchase.service.ts @@ -157,7 +157,7 @@ export const createNftPuchaseOrder = async ( }; }; -const getCollection = async (id: string) => { +export const getCollection = async (id: string) => { const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${id}`); const collection = await collectionDocRef.get(); if (!collection) { @@ -170,7 +170,7 @@ const getCollection = async (id: string) => { return collection; }; -const getMember = async (id: string) => { +export const getMember = async (id: string) => { const memberDocRef = build5Db().doc(`${COL.MEMBER}/${id}`); return await memberDocRef.get(); }; @@ -204,7 +204,7 @@ const getNft = async (collection: Collection, nftId: string | undefined) => { throw invalidArgument(WenError.no_more_nft_available_for_sale); }; -const getNftAbove = (collection: Collection, position: number) => +export const getNftAbove = (collection: Collection, position: number, limit = 1) => build5Db() .collection(COL.NFT) .where('sold', '==', false) @@ -213,10 +213,10 @@ const getNftAbove = (collection: Collection, position: number) => .where('collection', '==', collection.uid) .where('position', '>=', position) .orderBy('position', 'asc') - .limit(1) + .limit(limit) .get(); -const getNftBelow = (collection: Collection, position: number) => +export const getNftBelow = (collection: Collection, position: number, limit = 1) => build5Db() .collection(COL.NFT) .where('sold', '==', false) @@ -225,10 +225,10 @@ const getNftBelow = (collection: Collection, position: number) => .where('collection', '==', collection.uid) .where('position', '<=', position) .orderBy('position', 'asc') - .limitToLast(1) + .limitToLast(limit) .get(); -const assertNftCanBePurchased = async ( +export const assertNftCanBePurchased = async ( space: Space, collection: Collection, nft: Nft, @@ -281,7 +281,7 @@ const assertNftCanBePurchased = async ( } }; -const assertUserHasAccess = (space: Space, collection: Collection, owner: string) => +export const assertUserHasAccess = (space: Space, collection: Collection, owner: string) => assertHasAccess( space.uid, owner, @@ -290,7 +290,7 @@ const assertUserHasAccess = (space: Space, collection: Collection, owner: string collection.accessCollections || [], ); -const assertUserHasOnlyOneNft = async (collection: Collection, owner: string) => { +export const assertUserHasOnlyOneNft = async (collection: Collection, owner: string) => { const snap = await build5Db() .collection(COL.TRANSACTION) .where('member', '==', owner) @@ -303,7 +303,7 @@ const assertUserHasOnlyOneNft = async (collection: Collection, owner: string) => } }; -const assertNoOrderInProgress = async (owner: string) => { +export const assertNoOrderInProgress = async (owner: string) => { const orderInProgress = await build5Db() .collection(COL.TRANSACTION) .where('payload.reconciled', '==', false) @@ -318,7 +318,7 @@ const assertNoOrderInProgress = async (owner: string) => { } }; -const assertCurrentOwnerAddress = (currentOwner: Space | Member, nft: Nft) => { +export const assertCurrentOwnerAddress = (currentOwner: Space | Member, nft: Nft) => { const network = nft.mintingData?.network || DEFAULT_NETWORK; const currentOwnerAddress = getAddress(currentOwner, network); if (isEmpty(currentOwnerAddress)) { @@ -329,7 +329,7 @@ const assertCurrentOwnerAddress = (currentOwner: Space | Member, nft: Nft) => { } }; -const getDiscount = (collection: Collection, member: Member) => { +export const getDiscount = (collection: Collection, member: Member) => { const spaceRewards = (member.spaces || {})[collection.space || ''] || {}; const descDiscounts = [...(collection.discounts || [])].sort((a, b) => b.amount - a.amount); for (const discount of descDiscounts) { @@ -342,7 +342,7 @@ const getDiscount = (collection: Collection, member: Member) => { return 1; }; -const lockNft = async (nftId: string, orderId: string) => +export const lockNft = async (nftId: string, orderId: string) => build5Db().runTransaction(async (transaction) => { const docRef = build5Db().doc(`${COL.NFT}/${nftId}`); const nft = await transaction.get(docRef); @@ -352,7 +352,7 @@ const lockNft = async (nftId: string, orderId: string) => transaction.update(docRef, { locked: true, lockedBy: orderId }); }); -const getNftFinalPrice = (nft: Nft, discount: number) => { +export const getNftFinalPrice = (nft: Nft, discount: number) => { let finalPrice = nft.availablePrice || nft.price; if (discount < 1 && !nft.owner) { finalPrice = Math.ceil(discount * nft.price); diff --git a/packages/functions/src/services/wallet/IotaWalletService.ts b/packages/functions/src/services/wallet/IotaWalletService.ts index ccbfbd7b90..f45836f181 100644 --- a/packages/functions/src/services/wallet/IotaWalletService.ts +++ b/packages/functions/src/services/wallet/IotaWalletService.ts @@ -1,4 +1,10 @@ -import { NativeToken, NetworkAddress, Timestamp, Transaction } from '@build-5/interfaces'; +import { + NativeToken, + NetworkAddress, + SendToManyTargets, + Timestamp, + Transaction, +} from '@build-5/interfaces'; import { AddressUnlockCondition, AliasOutput, @@ -24,7 +30,7 @@ import { getSecretManager } from '../../utils/secret.manager.utils'; import { NftWallet } from './NftWallet'; import { MnemonicService } from './mnemonic'; import { Wallet, WalletParams } from './wallet'; -import { AddressDetails, SendToManyTargets, setConsumedOutputIds } from './wallet.service'; +import { AddressDetails, setConsumedOutputIds } from './wallet.service'; export interface Expiration { readonly expiresAt: Timestamp; diff --git a/packages/functions/src/services/wallet/wallet.service.ts b/packages/functions/src/services/wallet/wallet.service.ts index f73aa26c01..709be20eb0 100644 --- a/packages/functions/src/services/wallet/wallet.service.ts +++ b/packages/functions/src/services/wallet/wallet.service.ts @@ -1,5 +1,5 @@ import { build5Db } from '@build-5/database'; -import { COL, DEFAULT_NETWORK, NativeToken, Network } from '@build-5/interfaces'; +import { COL, DEFAULT_NETWORK, Network } from '@build-5/interfaces'; import { Client } from '@iota/sdk'; import { getRandomIndex } from '../../utils/common.utils'; import { IotaWallet } from './IotaWalletService'; @@ -12,13 +12,6 @@ export interface AddressDetails { mnemonic: string; } -export interface SendToManyTargets { - toAddress: string; - amount: number; - customMetadata?: Record; - nativeTokens?: NativeToken[]; -} - const NODES = { [Network.SMR]: ['https://smr1.svrs.io/', 'https://smr3.svrs.io/'], [Network.RMS]: ['https://rms1.svrs.io/', 'https://rms1.svrs.io/'], // Second ulr is for testing purposes, diff --git a/packages/functions/src/services/wallet/wallet.ts b/packages/functions/src/services/wallet/wallet.ts index 1e1da0a0fa..7d6236a498 100644 --- a/packages/functions/src/services/wallet/wallet.ts +++ b/packages/functions/src/services/wallet/wallet.ts @@ -1,7 +1,13 @@ -import { NativeToken, Network, Timestamp, Transaction } from '@build-5/interfaces'; +import { + NativeToken, + Network, + SendToManyTargets, + Timestamp, + Transaction, +} from '@build-5/interfaces'; import { AliasOutput, BasicOutput, Client, FoundryOutput, INodeInfo, NftOutput } from '@iota/sdk'; import { Expiration } from './IotaWalletService'; -import { AddressDetails, SendToManyTargets } from './wallet.service'; +import { AddressDetails } from './wallet.service'; export interface WalletParams { readonly data?: string; diff --git a/packages/functions/src/triggers/transaction-trigger/transaction.trigger.ts b/packages/functions/src/triggers/transaction-trigger/transaction.trigger.ts index 90430d6afd..02f2bf3566 100644 --- a/packages/functions/src/triggers/transaction-trigger/transaction.trigger.ts +++ b/packages/functions/src/triggers/transaction-trigger/transaction.trigger.ts @@ -428,6 +428,10 @@ const submitUnlockTransaction = async ( transaction.payload.outputToConsume, ); } + case TransactionPayloadType.TANGLE_TRANSFER_MANY: { + const sourceAddress = await wallet.getAddressDetails(transaction.payload.sourceAddress!); + return wallet.sendToMany(sourceAddress, transaction.payload.targetAddresses!, params); + } case TransactionPayloadType.UNLOCK_NFT: { const nftWallet = new NftWallet(wallet); return nftWallet.changeNftOwner(transaction, params); diff --git a/packages/functions/test-tangle/nft-bulk/Helper.ts b/packages/functions/test-tangle/nft-bulk/Helper.ts new file mode 100644 index 0000000000..5be0c6ed3a --- /dev/null +++ b/packages/functions/test-tangle/nft-bulk/Helper.ts @@ -0,0 +1,107 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { build5Db } from '@build-5/database'; +import { + Access, + COL, + Categories, + Collection, + CollectionType, + MIN_IOTA_AMOUNT, + Member, + Network, + NetworkAddress, + Nft, + Space, +} from '@build-5/interfaces'; +import dayjs from 'dayjs'; +import { createCollection } from '../../src/runtime/firebase/collection'; +import { createNft } from '../../src/runtime/firebase/nft'; +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 as createMemberTest, + createSpace, + mockWalletReturnValue, +} from '../../test/controls/common'; +import { getWallet, testEnv } from '../../test/set-up'; + +export class Helper { + public walletSpy: any = {} as any; + public member: string = {} as any; + public memberAddress: AddressDetails = {} as any; + public space: Space = {} as any; + public walletService: Wallet = {} as any; + public member2: string = {} as any; + + public beforeEach = async () => { + this.walletSpy = jest.spyOn(wallet, 'decodeAuth'); + this.member = await createMemberTest(this.walletSpy); + this.member2 = await createMemberTest(this.walletSpy); + this.space = await createSpace(this.walletSpy, this.member); + this.walletService = await getWallet(Network.ATOI); + + const memberData = await build5Db().doc(`${COL.MEMBER}/${this.member}`).get(); + this.memberAddress = await this.walletService!.getAddressDetails( + getAddress(memberData, Network.ATOI), + ); + }; + + public createColletionAndNft = async ( + address: NetworkAddress, + space: Space, + type = CollectionType.CLASSIC, + ) => { + mockWalletReturnValue(this.walletSpy, address, this.dummyCollection(space, type)); + const cCollection = await testEnv.wrap(createCollection)({}); + expect(cCollection?.uid).toBeDefined(); + + await build5Db() + .doc(`${COL.COLLECTION}/${cCollection?.uid}`) + .update({ approved: true }); + const collection = cCollection; + + const nft = await this.createNft(address, collection); + return { nft, collection }; + }; + + public createNft = async (address: NetworkAddress, collection: Collection) => { + mockWalletReturnValue(this.walletSpy, address, this.dummyNft(collection)); + const nft = await testEnv.wrap(createNft)({}); + expect(nft?.createdOn).toBeDefined(); + return nft; + }; + + public dummyCollection = ( + space: Space, + type: CollectionType, + ro = 0.5, + priceMi = 1, + date: Date = dayjs().subtract(1, 'minute').toDate(), + ) => ({ + name: 'Collection', + description: 'babba', + type, + category: Categories.ART, + access: Access.OPEN, + royaltiesFee: ro, + space: space.uid, + royaltiesSpace: space.uid, + onePerMemberOnly: false, + availableFrom: date, + price: priceMi * MIN_IOTA_AMOUNT, + }); + + public dummyNft = ( + collection: Collection, + priceMi = 1, + date: Date = dayjs().subtract(1, 'minute').toDate(), + ) => ({ + name: 'Collection A', + description: 'babba', + collection: collection.uid, + availableFrom: date, + price: priceMi * MIN_IOTA_AMOUNT, + }); +} diff --git a/packages/functions/test-tangle/nft-bulk/order.bulk_1.spec.ts b/packages/functions/test-tangle/nft-bulk/order.bulk_1.spec.ts new file mode 100644 index 0000000000..88831d2ff4 --- /dev/null +++ b/packages/functions/test-tangle/nft-bulk/order.bulk_1.spec.ts @@ -0,0 +1,43 @@ +import { build5Db } from '@build-5/database'; +import { COL, Nft, NftPurchaseBulkRequest, Transaction } from '@build-5/interfaces'; +import { orderNftBulk } from '../../src/runtime/firebase/nft'; +import { mockWalletReturnValue, wait } from '../../test/controls/common'; +import { testEnv } from '../../test/set-up'; +import { requestFundsFromFaucet } from '../faucet'; +import { Helper } from './Helper'; + +describe('Nft bulk order', () => { + const h = new Helper(); + + beforeEach(async () => { + await h.beforeEach(); + }); + + it('Should order 2 nfts', async () => { + const { collection: col1, nft: nft1 } = await h.createColletionAndNft(h.member, h.space); + const { collection: col2, nft: nft2 } = await h.createColletionAndNft(h.member, h.space); + + const request: NftPurchaseBulkRequest = { + orders: [ + { collection: col1.uid, nft: nft1.uid }, + { collection: col2.uid, nft: nft2.uid }, + ], + }; + mockWalletReturnValue(h.walletSpy, h.member, request); + const order: Transaction = await testEnv.wrap(orderNftBulk)({}); + + await requestFundsFromFaucet( + order.network, + order.payload.targetAddress!, + order.payload.amount!, + ); + + const nft1DocRef = build5Db().doc(`${COL.NFT}/${nft1.uid}`); + const nft2DocRef = build5Db().doc(`${COL.NFT}/${nft2.uid}`); + await wait(async () => { + const nft1 = await nft1DocRef.get(); + const nft2 = await nft2DocRef.get(); + return nft1.owner === h.member && nft2.owner === h.member; + }); + }); +}); diff --git a/packages/functions/test-tangle/nft-bulk/order.bulk_2.spec.ts b/packages/functions/test-tangle/nft-bulk/order.bulk_2.spec.ts new file mode 100644 index 0000000000..bcbd8d94dc --- /dev/null +++ b/packages/functions/test-tangle/nft-bulk/order.bulk_2.spec.ts @@ -0,0 +1,62 @@ +import { build5Db } from '@build-5/database'; +import { + COL, + MIN_IOTA_AMOUNT, + Nft, + NftPurchaseBulkRequest, + Transaction, + TransactionType, +} from '@build-5/interfaces'; +import { orderNftBulk } from '../../src/runtime/firebase/nft'; +import { mockWalletReturnValue, wait } from '../../test/controls/common'; +import { testEnv } from '../../test/set-up'; +import { requestFundsFromFaucet } from '../faucet'; +import { Helper } from './Helper'; + +describe('Nft bulk order', () => { + const h = new Helper(); + + beforeEach(async () => { + await h.beforeEach(); + }); + + it('Should order 2 nfts, buy only one', async () => { + const { collection: col1, nft: nft1 } = await h.createColletionAndNft(h.member, h.space); + const { collection: col2, nft: nft2 } = await h.createColletionAndNft(h.member, h.space); + + const request: NftPurchaseBulkRequest = { + orders: [ + { collection: col1.uid, nft: nft1.uid }, + { collection: col2.uid, nft: nft2.uid }, + ], + }; + mockWalletReturnValue(h.walletSpy, h.member, request); + const order: Transaction = await testEnv.wrap(orderNftBulk)({}); + + const nft1DocRef = build5Db().doc(`${COL.NFT}/${nft1.uid}`); + const nft2DocRef = build5Db().doc(`${COL.NFT}/${nft2.uid}`); + await nft2DocRef.update({ locked: true }); + + await requestFundsFromFaucet( + order.network, + order.payload.targetAddress!, + order.payload.amount!, + ); + + await wait(async () => { + const nft1 = await nft1DocRef.get(); + return nft1.owner === h.member; + }); + + const query = build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.CREDIT) + .where('member', '==', h.member); + await wait(async () => { + const snap = await query.get(); + return snap.length === 1; + }); + const snap = await query.get(); + expect(snap[0].payload.amount).toBe(1 * MIN_IOTA_AMOUNT); + }); +}); diff --git a/packages/functions/test-tangle/nft-bulk/order.bulk_3.spec.ts b/packages/functions/test-tangle/nft-bulk/order.bulk_3.spec.ts new file mode 100644 index 0000000000..51103f1083 --- /dev/null +++ b/packages/functions/test-tangle/nft-bulk/order.bulk_3.spec.ts @@ -0,0 +1,55 @@ +import { build5Db } from '@build-5/database'; +import { + COL, + MIN_IOTA_AMOUNT, + Network, + Nft, + TangleRequestType, + Transaction, +} from '@build-5/interfaces'; +import { wait } from '../../test/controls/common'; +import { getTangleOrder } from '../common'; +import { requestFundsFromFaucet } from '../faucet'; +import { Helper } from './Helper'; + +describe('Nft bulk order', () => { + const h = new Helper(); + let tangleOrder: Transaction; + + beforeEach(async () => { + await h.beforeEach(); + tangleOrder = await getTangleOrder(Network.ATOI); + }); + + it('Should order 2 nfts with tangle order', async () => { + const { collection: col1, nft: nft1 } = await h.createColletionAndNft(h.member, h.space); + const { collection: col2, nft: nft2 } = await h.createColletionAndNft(h.member, h.space); + + await requestFundsFromFaucet(Network.ATOI, h.memberAddress.bech32, 2 * MIN_IOTA_AMOUNT); + await h.walletService.send( + h.memberAddress, + tangleOrder.payload.targetAddress!, + 2 * MIN_IOTA_AMOUNT, + { + customMetadata: { + request: { + requestType: TangleRequestType.NFT_PURCHASE_BULK, + orders: [ + { collection: col1.uid, nft: nft1.uid }, + { collection: col2.uid, nft: nft2.uid }, + ], + }, + }, + }, + ); + + const nft1DocRef = build5Db().doc(`${COL.NFT}/${nft1.uid}`); + const nft2DocRef = build5Db().doc(`${COL.NFT}/${nft2.uid}`); + + await wait(async () => { + const nft1 = await nft1DocRef.get(); + const nft2 = await nft2DocRef.get(); + return nft1.owner === h.member && nft2.owner === h.member; + }); + }); +}); diff --git a/packages/functions/test-tangle/nft-bulk/order.bulk_4.spec.ts b/packages/functions/test-tangle/nft-bulk/order.bulk_4.spec.ts new file mode 100644 index 0000000000..88b7ece7ad --- /dev/null +++ b/packages/functions/test-tangle/nft-bulk/order.bulk_4.spec.ts @@ -0,0 +1,53 @@ +import { build5Db } from '@build-5/database'; +import { COL, CollectionType, Nft, Transaction } from '@build-5/interfaces'; +import { orderNftBulk } from '../../src/runtime/firebase/nft'; +import { mockWalletReturnValue, wait } from '../../test/controls/common'; +import { testEnv } from '../../test/set-up'; +import { requestFundsFromFaucet } from '../faucet'; +import { Helper } from './Helper'; + +describe('Nft bulk order', () => { + const h = new Helper(); + + beforeEach(async () => { + await h.beforeEach(); + }); + + it('Should order 2 nfts, 2 random', async () => { + const { collection: col1, nft: nft1 } = await h.createColletionAndNft(h.member, h.space); + const { collection: col2, nft: nft2 } = await h.createColletionAndNft(h.member, h.space); + const { collection: col3 } = await h.createColletionAndNft( + h.member, + h.space, + CollectionType.GENERATED, + ); + await h.createNft(h.member, col3); + await h.createNft(h.member, col3); + await h.createNft(h.member, col3); + + const request = { + orders: [ + { collection: col1.uid, nft: nft1.uid }, + { collection: col2.uid, nft: nft2.uid }, + { collection: col3.uid }, + { collection: col3.uid }, + ], + }; + mockWalletReturnValue(h.walletSpy, h.member, request); + const order: Transaction = await testEnv.wrap(orderNftBulk)({}); + await requestFundsFromFaucet( + order.network, + order.payload.targetAddress!, + order.payload.amount!, + ); + + await wait(async () => { + const promises = order.payload.nftOrders!.map(async (nftOrder) => { + const docRef = build5Db().doc(`${COL.NFT}/${nftOrder.nft}`); + return await docRef.get(); + }); + const nfts = await Promise.all(promises); + return nfts.length === 4 && nfts.reduce((acc, act) => acc && act.owner === h.member, true); + }); + }); +}); diff --git a/packages/functions/test-tangle/nft-bulk/order.bulk_5.spec.ts b/packages/functions/test-tangle/nft-bulk/order.bulk_5.spec.ts new file mode 100644 index 0000000000..fcaf44ea3f --- /dev/null +++ b/packages/functions/test-tangle/nft-bulk/order.bulk_5.spec.ts @@ -0,0 +1,86 @@ +import { build5Db } from '@build-5/database'; +import { + COL, + MIN_IOTA_AMOUNT, + Network, + Nft, + TangleRequestType, + Transaction, + TransactionType, +} from '@build-5/interfaces'; +import { MnemonicService } from '../../src/services/wallet/mnemonic'; +import { wait } from '../../test/controls/common'; +import { getTangleOrder } from '../common'; +import { requestFundsFromFaucet } from '../faucet'; +import { Helper } from './Helper'; + +describe('Nft bulk order', () => { + const h = new Helper(); + let tangleOrder: Transaction; + + beforeEach(async () => { + await h.beforeEach(); + tangleOrder = await getTangleOrder(Network.ATOI); + }); + + it('Should order 2 nfts, buy only one with tangle', async () => { + const { collection: col1, nft: nft1 } = await h.createColletionAndNft(h.member, h.space); + const { collection: col2, nft: nft2 } = await h.createColletionAndNft(h.member, h.space); + + await requestFundsFromFaucet(Network.ATOI, h.memberAddress.bech32, 3 * MIN_IOTA_AMOUNT); + await h.walletService.send( + h.memberAddress, + tangleOrder.payload.targetAddress!, + 3 * MIN_IOTA_AMOUNT, + { + customMetadata: { + request: { + requestType: TangleRequestType.NFT_PURCHASE_BULK, + orders: [ + { collection: col1.uid, nft: nft1.uid }, + { collection: col2.uid, nft: nft2.uid }, + ], + }, + }, + }, + ); + await MnemonicService.store(h.memberAddress.bech32, h.memberAddress.mnemonic); + + let query = build5Db() + .collection(COL.TRANSACTION) + .where('member', '==', h.member) + .where('type', '==', TransactionType.CREDIT_TANGLE_REQUEST); + await wait(async () => { + const snap = await query.get(); + return snap.length === 1 && snap[0].payload.walletReference?.confirmed; + }); + + const nft1DocRef = build5Db().doc(`${COL.NFT}/${nft1.uid}`); + const nft2DocRef = build5Db().doc(`${COL.NFT}/${nft2.uid}`); + await nft2DocRef.update({ locked: true }); + + const credit = (await query.get())[0]; + await h.walletService.send( + h.memberAddress, + credit.payload.response!.address as string, + credit.payload.response!.amount as number, + {}, + ); + + await wait(async () => { + const nft1 = await nft1DocRef.get(); + return nft1.owner === h.member; + }); + + query = build5Db() + .collection(COL.TRANSACTION) + .where('type', '==', TransactionType.CREDIT) + .where('member', '==', h.member); + await wait(async () => { + const snap = await query.get(); + return snap.length === 1; + }); + const snap = await query.get(); + expect(snap[0].payload.amount).toBe(1 * MIN_IOTA_AMOUNT); + }); +}); diff --git a/packages/functions/test-tangle/nft-bulk/order.bulk_6.spec.ts b/packages/functions/test-tangle/nft-bulk/order.bulk_6.spec.ts new file mode 100644 index 0000000000..50eec9ae71 --- /dev/null +++ b/packages/functions/test-tangle/nft-bulk/order.bulk_6.spec.ts @@ -0,0 +1,76 @@ +import { build5Db } from '@build-5/database'; +import { + COL, + MIN_IOTA_AMOUNT, + Network, + Nft, + TangleRequestType, + Transaction, + TransactionType, +} from '@build-5/interfaces'; +import { MnemonicService } from '../../src/services/wallet/mnemonic'; +import { wait } from '../../test/controls/common'; +import { getTangleOrder } from '../common'; +import { requestFundsFromFaucet } from '../faucet'; +import { Helper } from './Helper'; + +describe('Nft bulk order', () => { + const h = new Helper(); + let tangleOrder: Transaction; + + beforeEach(async () => { + await h.beforeEach(); + tangleOrder = await getTangleOrder(Network.ATOI); + }); + + it('Should order 2 nfts, only one available', async () => { + const { collection: col1, nft: nft1 } = await h.createColletionAndNft(h.member, h.space); + const { collection: col2, nft: nft2 } = await h.createColletionAndNft(h.member, h.space); + const nft1DocRef = build5Db().doc(`${COL.NFT}/${nft1.uid}`); + const nft2DocRef = build5Db().doc(`${COL.NFT}/${nft2.uid}`); + await nft2DocRef.update({ locked: true }); + + await requestFundsFromFaucet(Network.ATOI, h.memberAddress.bech32, 2 * MIN_IOTA_AMOUNT); + await h.walletService.send( + h.memberAddress, + tangleOrder.payload.targetAddress!, + 2 * MIN_IOTA_AMOUNT, + { + customMetadata: { + request: { + requestType: TangleRequestType.NFT_PURCHASE_BULK, + orders: [ + { collection: col1.uid, nft: nft1.uid }, + { collection: col2.uid, nft: nft2.uid }, + ], + }, + }, + }, + ); + await MnemonicService.store(h.memberAddress.bech32, h.memberAddress.mnemonic); + + let query = build5Db() + .collection(COL.TRANSACTION) + .where('member', '==', h.member) + .where('type', '==', TransactionType.CREDIT_TANGLE_REQUEST); + await wait(async () => { + const snap = await query.get(); + return snap.length === 1 && snap[0].payload.walletReference?.confirmed; + }); + + const credit = (await query.get())[0]; + expect(credit.payload.amount).toBe(2 * MIN_IOTA_AMOUNT); + expect(credit.payload.response!.amount).toBe(MIN_IOTA_AMOUNT); + await h.walletService.send( + h.memberAddress, + credit.payload.response!.address as string, + credit.payload.response!.amount as number, + {}, + ); + + await wait(async () => { + const nft1 = await nft1DocRef.get(); + return nft1.owner === h.member; + }); + }); +}); diff --git a/packages/functions/test/set-up.ts b/packages/functions/test/set-up.ts index 7b3da84f5f..c66ee6a737 100644 --- a/packages/functions/test/set-up.ts +++ b/packages/functions/test/set-up.ts @@ -8,6 +8,7 @@ import { ProjectBilling, SOON_PROJECT_ID, SUB_COL, + SendToManyTargets, } from '@build-5/interfaces'; import dayjs from 'dayjs'; import dotenv from 'dotenv'; @@ -16,11 +17,7 @@ import test from 'firebase-functions-test'; import * as functions from 'firebase-functions/v2'; import { isEmpty } from 'lodash'; import { Wallet, WalletParams } from '../src/services/wallet/wallet'; -import { - AddressDetails, - SendToManyTargets, - WalletService, -} from '../src/services/wallet/wallet.service'; +import { AddressDetails, WalletService } from '../src/services/wallet/wallet.service'; import { dateToTimestamp } from '../src/utils/dateTime.utils'; dotenv.config({ path: '.env.local' }); diff --git a/packages/interfaces/src/config.ts b/packages/interfaces/src/config.ts index 5183a37e6d..d0aeaedf2e 100644 --- a/packages/interfaces/src/config.ts +++ b/packages/interfaces/src/config.ts @@ -216,3 +216,5 @@ export const STAMP_ROYALTY_ADDRESS = { [Network.IOTA]: 'iota1qr5ped7rfdkh8j9acj6qyyxz4mmhfsrqage37y7l0m09snclk07gsyup5dp', [Network.ATOI]: 'atoi1qr9f2a43rw3me665vzda3fq4wdtv69v38pvrkvx9rqu0pm57lyk4vc0mf9j', }; + +export const MAX_NFT_BULK_PURCHASE = 100; diff --git a/packages/interfaces/src/errors.ts b/packages/interfaces/src/errors.ts index 310daa8756..425aa8b933 100644 --- a/packages/interfaces/src/errors.ts +++ b/packages/interfaces/src/errors.ts @@ -326,4 +326,8 @@ export const WenError = { code: 2147, key: 'No active sell orders.', }, + nfts_must_be_within_same_network: { + code: 2148, + key: 'Nfts must be within same network.', + }, }; diff --git a/packages/interfaces/src/functions/index.ts b/packages/interfaces/src/functions/index.ts index 20a01718f5..ad0ec3b0af 100644 --- a/packages/interfaces/src/functions/index.ts +++ b/packages/interfaces/src/functions/index.ts @@ -49,6 +49,7 @@ export enum WEN_FUNC { // ORDER functions orderNft = 'ordernft', + orderNftBulk = 'ordernftbulk', openBid = 'openbid', validateAddress = 'validateaddress', diff --git a/packages/interfaces/src/models/transaction/common.ts b/packages/interfaces/src/models/transaction/common.ts index d12adf9bee..ac4bbb172d 100644 --- a/packages/interfaces/src/models/transaction/common.ts +++ b/packages/interfaces/src/models/transaction/common.ts @@ -5,6 +5,9 @@ export const TRANSACTION_AUTO_EXPIRY_MS = 4 * 60 * 1000; export const TRANSACTION_MAX_EXPIRY_MS = 31 * 24 * 60 * 60 * 1000; export const TRANSACTION_DEFAULT_AUCTION = 3 * 24 * 60 * 60 * 1000; +/** + * Enum to represent why a transaction was ignored to process + */ export enum IgnoreWalletReason { NONE = '', UNREFUNDABLE_DUE_UNLOCK_CONDITIONS = 'UnrefundableDueUnlockConditions', @@ -15,6 +18,9 @@ export enum IgnoreWalletReason { MISSING_TARGET_ADDRESS = 'MISSING_TARGET_ADDRESS', } +/** + * Enum representing all the possible transactions + */ export enum TransactionType { VOTE = 'VOTE', ORDER = 'ORDER', @@ -52,8 +58,12 @@ export enum Network { RMS = 'rms', } +/** + * Enum representing transaction payload type + */ export enum TransactionPayloadType { NFT_PURCHASE = 'NFT_PURCHASE', + NFT_PURCHASE_BULK = 'NFT_PURCHASE_BULK', NFT_BID = 'NFT_BID', AUCTION_BID = 'AUCTION_BID', SPACE_ADDRESS_VALIDATION = 'SPACE_ADDRESS_VALIDATION', @@ -99,6 +109,7 @@ export enum TransactionPayloadType { UNLOCK_FUNDS = 'UNLOCK_FUNDS', UNLOCK_NFT = 'UNLOCK_NFT', TANGLE_TRANSFER = 'TANGLE_TRANSFER', + TANGLE_TRANSFER_MANY = 'TANGLE_TRANSFER_MANY', MINT_NFT = 'MINT_NFT', UPDATE_MINTED_NFT = 'UPDATE_MINTED_NFT', @@ -120,11 +131,17 @@ export enum TransactionPayloadType { STAMP = 'STAMP', } +/** + * Validation type. Defines when a received amount is processed + */ export enum TransactionValidationType { ADDRESS_AND_AMOUNT = 0, ADDRESS = 1, } +/** + * Base transaction record + */ export interface Transaction extends BaseRecord { network: Network; type: TransactionType; @@ -138,6 +155,9 @@ export interface Transaction extends BaseRecord { payload: TransactionPayload; } +/** + * Result after processing a transaction + */ export interface WalletResult { createdOn: Timestamp; processedOn: Timestamp; @@ -152,16 +172,25 @@ export interface WalletResult { nodeIndex?: number; } +/** + * Storage return params + */ export interface StorageReturn { readonly amount: number; readonly address: NetworkAddress; } +/** + * Enum representing the type of the owner or the beneficiary of a transaction + */ export enum Entity { SPACE = 'space', MEMBER = 'member', } +/** + * Interface representing a tangle payload + */ export interface IOTATangleTransaction { tranId: string; network: string; @@ -185,10 +214,18 @@ export interface IOTATangleTransaction { invalidPayment?: boolean; } +/** + * Enum representing why payment was credited + */ export enum CreditPaymentReason { TRADE_CANCELLED = 'trade_cancelled', } +/** + * Function to get the pair of a DLT network + * @param network + * @returns + */ export const getNetworkPair = (network: Network) => { switch (network) { case Network.IOTA: @@ -202,6 +239,9 @@ export const getNetworkPair = (network: Network) => { } }; +/** + * Detail of each DLT network + */ export const NETWORK_DETAIL = { [Network.IOTA]: { label: 'IOTA', @@ -225,6 +265,9 @@ export const NETWORK_DETAIL = { }, }; +/** + * Default network decimals + */ export const DEFAULT_NETWORK_DECIMALS = 6; export const getDefDecimalIfNotSet = (v?: number | null) => { diff --git a/packages/interfaces/src/models/transaction/payload.ts b/packages/interfaces/src/models/transaction/payload.ts index 444182c3c8..d43e3def27 100644 --- a/packages/interfaces/src/models/transaction/payload.ts +++ b/packages/interfaces/src/models/transaction/payload.ts @@ -11,151 +11,486 @@ import { WalletResult, } from './common'; +/** + * Interface representing an NFT bulk order item + */ +export interface NftBulkOrder { + /** + * Id of the collection + */ + collection: string; + /** + * Id of the NFT that was purchased. Do not set, it will be set by system. + */ + nft: string; + /** + * Id of the nft that was requested to be purchased. If not set, system will choose a random nft + */ + requestedNft?: string; + /** + * Price of the NFT. Do not set, it will be set by system. + */ + price: number; + /** + * Error in case purchase could not be done. Do not set, it will be set by system. + */ + error: number; + /** + * In case of minted NFT, it can be withdrawn automatically to this address. + */ + targetAddress?: string; +} + +/** + * Interface used to specify a transaction to multiuple recipients. + * Used only internally + */ +export interface SendToManyTargets { + toAddress: string; + amount: number; + customMetadata?: Record; + nativeTokens?: NativeToken[]; +} + +/** + * Interface representing transaction payload type + */ export interface TransactionPayload { + /** + * Type of the payload + */ type?: TransactionPayloadType; - + /** + * Amount used or transfered + */ amount?: number; + /** + * Source address of the transaction + */ sourceAddress?: NetworkAddress; + /** + * Target address of the transaction + */ targetAddress?: NetworkAddress; + /** + * Target addresses of the transaction incase of multy target transaction + */ + targetAddresses?: SendToManyTargets[]; + /** + * A reference to the source order or payment + */ sourceTransaction?: string | string[]; + /** + * Specifies the processing type + */ validationType?: TransactionValidationType; + /** + * Order will expire on this date. Once order is expired, it can not receive any more requests. + */ expiresOn?: Timestamp | null; + /** + * Boolean value specifying if the order was reconciled or not + */ reconciled?: boolean; + /** + * Boolean value specifying if the order was voided + */ void?: boolean; // MINT_COLLECTION + /** + * Collection id + */ collection?: NetworkAddress | null; + /** + * Specifies what should happen with unsold NFTs upon minting. + */ unsoldMintingOptions?: UnsoldMintingOptions; + /** + * New price of unsold NFTs after minting + */ newPrice?: number; + /** + * Storage deposit needed to mint the collection + */ collectionStorageDeposit?: number; + /** + * Storage deposit needed to mint the NFTs + */ nftsStorageDeposit?: number; + /** + * Storage deposit needed to mint the alias + */ aliasStorageDeposit?: number; + /** + * NFTs to mint + */ nftsToMint?: number; // TransactionPayloadType.CREDIT_LOCKED_FUNDS, + /** + * Transaction id + */ transaction?: NetworkAddress; + /** + * Member id who unlocked the transaction + */ unlockedBy?: NetworkAddress; // TransactionPayloadType.NFT_BID, + /** + * Beneficiary type of the transaction + */ beneficiary?: Entity; + /** + * Beneficiary id of the transaction + */ beneficiaryUid?: NetworkAddress; + /** + * Beneficiary address of the transaction + */ beneficiaryAddress?: NetworkAddress; + /** + * Royalty fee + */ royaltiesFee?: number; + /** + * Royalty space + */ royaltiesSpace?: NetworkAddress; + /** + * Royalty space address + */ royaltiesSpaceAddress?: NetworkAddress; + /** + * Tangle chain reference + */ chainReference?: string | null; + /** + * NFT id + */ nft?: NetworkAddress | null; + /** + * Restrictions applied when purchasing the NFT + */ restrictions?: Restrictions; // TransactionPayloadType.TOKEN_AIRDROP, + /** + * Token id + */ token?: NetworkAddress; + /** + * Quantity of tokens purchased + */ quantity?: number; tokenSymbol?: string; // TransactionPayloadType.TOKEN_PURCHASE + /** + * Unclaimed airdrops count + */ unclaimedAirdrops?: number; + /** + * Total airdrops count + */ totalAirdropCount?: number; // TransactionPayloadType.IMPORT_TOKEN, + /** + * Tangle id of a token + */ tokenId?: NetworkAddress; - // TransactionPayloadType.MINT_TOKEN, + // TransactionPayloadType.MINT_TOKEN + /** + * Storage deposit needed to mint the foundry + */ foundryStorageDeposit?: number; + /** + * Storage deposit used by the vault + */ vaultStorageDeposit?: number; + /** + * Storage deposit needed for the guardian + */ guardianStorageDeposit?: number; + /** + * Tokens stored in the space's vault + */ tokensInVault?: number; - // TransactionPayloadType.MINT_ALIAS , + // TransactionPayloadType.MINT_ALIAS + /** + * Order transaction id + */ orderId?: NetworkAddress; + /** + * Base token amount used by the collection output + */ collectionOutputAmount?: number; - + /** + * Base token amount used by the alias output + */ aliasOutputAmount?: number; + /** + * Base token amount used by the nft output + */ nftOutputAmount?: number; // TransactionPayloadType.MINT_COLLECTION - METADATA_NFT, + /** + * Tanagle id of the alias + */ aliasId?: NetworkAddress; + /** + * Block id in which the alias was minted + */ aliasBlockId?: NetworkAddress; + /** + * Governor address of the alias + */ aliasGovAddress?: NetworkAddress; // TransactionPayloadType.UPDATE_MINTED_NFT - // TransactionPayloadType.MINT_NFT, + // TransactionPayloadType.MINT_NFT + /** + * Tangle id of the collection + */ collectionId?: NetworkAddress | null; + /** + * Tangle id of the nft + */ nftId?: NetworkAddress | null; // TransactionPayloadType.STAKE, + /** + * Native tokens to transfer + */ nativeTokens?: NativeToken[]; + /** + * Previous owner type + */ previousOwnerEntity?: Entity; + /** + * Previous owner + */ previousOwner?: string; + /** + * Current owner type + */ ownerEntity?: Entity; + /** + * Current owner + */ owner?: string; + /** + * If true, the payment is a royalty payment + */ royalty?: boolean; + /** + * Vesting time of the airdrop + */ vestingAt?: Timestamp | null; + /** + * Custom metadata that will be is on the output metadata + */ customMetadata?: { [key: string]: string }; + /** + * Build5 stake id + */ stake?: string; - + /** + * Build5 award id + */ award?: NetworkAddress | null; + /** + * Legacy award fund request id + */ legacyAwardFundRequestId?: NetworkAddress; - + /** + * Length of the stake in weeks + */ weeks?: number; + /** + * Stake type + */ stakeType?: StakeType; // TransactionPayloadType.SELL_TOKEN - // TransactionPayloadType.BUY_TOKEN, + // TransactionPayloadType.BUY_TOKEN + /** + * Token count set during token trading + */ count?: number; + /** + * Token price set during token trading + */ price?: number; // TransactionPayloadType.BADGE + /** + * Build5 id of the token reward + */ tokenReward?: number; + /** + * Edition of the badge + */ edition?: number; + /** + * Participation time + */ participatedOn?: Timestamp; - // TransactionPayloadType.PROPOSAL_VOTE + // TransactionPayloadType.PROPOSAL_VOTE + /** + * Build5 id of the proposal + */ proposalId?: NetworkAddress; + /** + * Vote values + */ voteValues?: number[]; // TransactionPayloadType.MINTED_TOKEN_TRADE + /** + * Address of the source storage deposit + */ storageDepositSourceAddress?: NetworkAddress; + /** + * Storage deposit return params + */ storageReturn?: StorageReturn; - + /** + * Build5 id of the airdrop + */ airdropId?: NetworkAddress; - + /** + * Result after processing the transaction + */ walletReference?: WalletResult; + /** + * Build5 of the minted NFTs + */ nfts?: NetworkAddress[]; + /** + * Tag used on the transaction + */ tag?: string; + /** + * Metadata that will be set on the output + */ metadata?: { [key: string]: unknown }; + /** + * Transaction response in case of processing failure + */ response?: { [key: string]: unknown }; + /** + * Reason for crediting a payment + */ reason?: CreditPaymentReason; + /** + * If true, payment was considered as invalid + */ invalidPayment?: boolean; + /** + * Tangle if of the output that will be consumed + */ outputToConsume?: string; + /** + * True if credit needs to wait for a bill payment to be processed first + */ dependsOnBillPayment?: boolean; + /** + * Build5 path to the transaction + */ milestoneTransactionPath?: string; + /** + * Vote values + */ values?: number[]; + /** + * Amount of the token + */ tokenAmount?: number; + /** + * Weight of the vote + */ weight?: number; + /** + * Multiplier for the vote weight + */ weightMultiplier?: number; + /** + * Votes + */ votes?: number[]; + /** + * Build5 transaction id of a vote + */ creditId?: NetworkAddress; + /** + * True if output was consumed + */ outputConsumed?: boolean; + /** + * Output consumption time + */ outputConsumedOn?: Timestamp; + /** + * Build5 ids of the stakes + */ stakes?: NetworkAddress[]; + /** + * Build5 ids of the stake rewards + */ stakeReward?: NetworkAddress; + /** + * True if the transaction is an OTR + */ tanglePuchase?: boolean; + /** + * If true, NFT won't be withdrawn after purchase + */ disableWithdraw?: boolean; + /** + * If true, collection NFT witll be locked + */ lockCollectionNft?: boolean; + /** + * Build5 if of the stamp + */ stamp?: string; + /** + * Target address of the token trade order + */ tokenTradeOderTargetAddress?: string; + /** + * Build5 id of the auction + */ auction?: string; + /** + * Days for which the stamp is stored + */ days?: number; + /** + * Daily cost of the stamp + */ dailyCost?: number; + + /** + * List representing the NFT bulk order + */ + nftOrders?: NftBulkOrder[]; } diff --git a/packages/interfaces/src/search/post/NftPurchaseBulkRequest.ts b/packages/interfaces/src/search/post/NftPurchaseBulkRequest.ts new file mode 100644 index 0000000000..caa93f7b5e --- /dev/null +++ b/packages/interfaces/src/search/post/NftPurchaseBulkRequest.ts @@ -0,0 +1,16 @@ +/** + * This file was automatically generated by joi-to-typescript + * Do not modify this file manually + */ + +import { NftPurchaseRequest } from '.'; + +/** + * Request object to create an NFT bulk purchase order + */ +export interface NftPurchaseBulkRequest { + /** + * List of collections&nfts to purchase, minimum 1, maximum 100 + */ + orders: NftPurchaseRequest[]; +} diff --git a/packages/interfaces/src/search/post/NftPurchaseRequest.ts b/packages/interfaces/src/search/post/NftPurchaseRequest.ts index 555ab233e9..c249b56971 100644 --- a/packages/interfaces/src/search/post/NftPurchaseRequest.ts +++ b/packages/interfaces/src/search/post/NftPurchaseRequest.ts @@ -12,7 +12,7 @@ export interface NftPurchaseRequest { */ collection: string; /** - * Build5 if of the nft to be purchased. + * Build5 id of the nft to be purchased. */ nft?: string; } diff --git a/packages/interfaces/src/search/post/index.ts b/packages/interfaces/src/search/post/index.ts index 8e9dee0a5f..d5f14dce95 100644 --- a/packages/interfaces/src/search/post/index.ts +++ b/packages/interfaces/src/search/post/index.ts @@ -26,6 +26,7 @@ export * from './UpdateMemberRequest'; export * from './NftBidRequest'; export * from './NftCreateRequest'; export * from './NftDepositRequest'; +export * from './NftPurchaseBulkRequest'; export * from './NftPurchaseRequest'; export * from './NftSetForSaleRequest'; export * from './NftStakeRequest'; diff --git a/packages/interfaces/src/search/tangle/NftPurchaseBulkTangleRequest.ts b/packages/interfaces/src/search/tangle/NftPurchaseBulkTangleRequest.ts new file mode 100644 index 0000000000..72c9fede95 --- /dev/null +++ b/packages/interfaces/src/search/tangle/NftPurchaseBulkTangleRequest.ts @@ -0,0 +1,31 @@ +/** + * This file was automatically generated by joi-to-typescript + * Do not modify this file manually + */ + +/** + * Tangle request object to create an NFT bulk purchase order + */ +export interface NftPurchaseBulkTangleRequest { + /** + * If set to true, NFT will not be sent to the buyer's validated address upon purchase. + */ + disableWithdraw?: boolean; + /** + * List of collections&nfts to purchase, minimum 1, maximum 100 + */ + orders: { + /** + * Build5 id of the collection in case a random nft is bought. + */ + collection: string; + /** + * Build5 id of the nft to be purchased. + */ + nft?: string; + }[]; + /** + * Type of the tangle request. + */ + requestType: 'NFT_PURCHASE_BULK'; +} diff --git a/packages/interfaces/src/search/tangle/NftPurchaseTangleRequest.ts b/packages/interfaces/src/search/tangle/NftPurchaseTangleRequest.ts index 093945e95d..7a4fa72109 100644 --- a/packages/interfaces/src/search/tangle/NftPurchaseTangleRequest.ts +++ b/packages/interfaces/src/search/tangle/NftPurchaseTangleRequest.ts @@ -16,7 +16,7 @@ export interface NftPurchaseTangleRequest { */ disableWithdraw?: boolean; /** - * Build5 if of the nft to be purchased. + * Build5 id of the nft to be purchased. */ nft?: string; /** diff --git a/packages/interfaces/src/search/tangle/common.ts b/packages/interfaces/src/search/tangle/common.ts index bb968847a4..9a94ce5ee3 100644 --- a/packages/interfaces/src/search/tangle/common.ts +++ b/packages/interfaces/src/search/tangle/common.ts @@ -5,6 +5,7 @@ export enum TangleRequestType { STAKE = 'STAKE', NFT_PURCHASE = 'NFT_PURCHASE', + NFT_PURCHASE_BULK = 'NFT_PURCHASE_BULK', NFT_BID = 'NFT_BID', NFT_SET_FOR_SALE = 'NFT_SET_FOR_SALE', diff --git a/packages/interfaces/src/search/tangle/index.ts b/packages/interfaces/src/search/tangle/index.ts index 4911599715..a3e6c15317 100644 --- a/packages/interfaces/src/search/tangle/index.ts +++ b/packages/interfaces/src/search/tangle/index.ts @@ -6,11 +6,13 @@ export * from './AddressValidationTangleRequest'; export * from './AuctionBidTangleRequest'; export * from './AuctionCreateTangleRequest'; +export * from './NftBidTangleRequest'; export * from './AwardAppParticipantTangleRequest'; export * from './AwardCreateTangleRequest'; export * from './AwardFundTangleRequest'; export * from './MetadataNftTangleRequest'; export * from './NftBidTangleRequest'; +export * from './NftPurchaseBulkTangleRequest'; export * from './NftPurchaseTangleRequest'; export * from './NftSetForSaleTangleRequest'; export * from './ProposalApproveTangleRequest'; diff --git a/packages/sdk/examples/nft/bulk/nft.bulk.purhcase.ts b/packages/sdk/examples/nft/bulk/nft.bulk.purhcase.ts new file mode 100644 index 0000000000..c32b906c03 --- /dev/null +++ b/packages/sdk/examples/nft/bulk/nft.bulk.purhcase.ts @@ -0,0 +1,54 @@ +import { Dataset, Network } from '@build-5/interfaces'; +import { Build5, SoonaverseApiKey, https } from '@build-5/sdk'; +import { address } from '../../utils/secret'; +import { walletSign } from '../../utils/utils'; + +const collectionId = 'build5nftcollectionid'; +const nftIds = ['build5nftid1', 'build5nftid2']; + +async function main() { + const origin = Build5.TEST; + + const member = await https(origin).createMember({ + address: address.bech32, + signature: '', + body: { + address: address.bech32, + }, + }); + + try { + const signature = await walletSign(member.uid, address); + const order = await https(origin) + .project(SoonaverseApiKey[origin]) + .dataset(Dataset.NFT) + .bulkPurchase({ + address: address.bech32, + signature: signature.signature, + publicKey: { + hex: signature.publicKey, + network: Network.RMS, + }, + body: { + orders: nftIds.map((nftId) => ({ collection: collectionId, nft: nftId })), + }, + }); + + console.log( + 'Sent: ', + order.payload.amount, + ' to ', + order.payload.targetAddress, + ', full order object: ', + order, + ); + console.log('Once the order is funded, payload.nftOrders will be update.'); + console.log('If price and nft is set, it means that that NFT was bough.'); + console.log('Otherwise error will contain an error code and amount will be credited.'); + } catch (e) { + console.log(e); + return; + } +} + +main().then(() => process.exit()); diff --git a/packages/sdk/examples/nft/bulk/nft.otr.buld.purchase.ts b/packages/sdk/examples/nft/bulk/nft.otr.buld.purchase.ts new file mode 100644 index 0000000000..dbc7adcb8b --- /dev/null +++ b/packages/sdk/examples/nft/bulk/nft.otr.buld.purchase.ts @@ -0,0 +1,33 @@ +import { Dataset } from '@build-5/interfaces'; +import { Build5, Build5OtrAddress, otr } from '@build-5/sdk'; + +const collectionId = 'build5nftcollectionid'; +const nftIds = ['build5nftid1', 'build5nftid2']; + +const origin = Build5.TEST; +const otrAddress = Build5OtrAddress[origin]; + +async function main() { + const fireflyDeepling = otr(otrAddress) + .dataset(Dataset.NFT) + .bulkPurchase({ orders: nftIds.map((nftId) => ({ collection: collectionId, nft: nftId })) }) + .getFireflyDeepLink(); + + console.log(fireflyDeepling); + + console.log('Sending whatever amount:'); + console.log( + 'In case you send a random amount your funds will be credited back' + + ' The response metadata will contain the exact amount needed and target address', + ); + + console.log('\n'); + console.log('Sending correct amount:'); + console.log( + 'In case you send the exact amount needed, your request will be processed and the NFTs will be purchased', + ); + + console.log('\nIn both cases, if an NFT can not be pruchased the amount will be credited back.'); +} + +main().then(() => process.exit()); diff --git a/packages/sdk/src/https/datasets/NftDataset.ts b/packages/sdk/src/https/datasets/NftDataset.ts index a371aea496..c3cf19e186 100644 --- a/packages/sdk/src/https/datasets/NftDataset.ts +++ b/packages/sdk/src/https/datasets/NftDataset.ts @@ -5,6 +5,7 @@ import { NftBidRequest, NftCreateRequest, NftDepositRequest, + NftPurchaseBulkRequest, NftPurchaseRequest, NftSetForSaleRequest, NftUpdateUnsoldRequest, @@ -32,6 +33,8 @@ export class NftDataset extends DatasetClass { openBid = this.sendRequest(WEN_FUNC.openBid); + bulkPurchase = this.sendRequest(WEN_FUNC.orderNftBulk); + getByCollectionLive = ( collection: string, orderBy: string[], diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index c1c10fc7be..bf95528b7d 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,2 +1,2 @@ -export { SoonaverseApiKey, Build5, https } from './https'; -export { otr } from './otr'; +export { Build5, SoonaverseApiKey, https } from './https'; +export { Build5OtrAddress, otr } from './otr'; diff --git a/packages/sdk/src/otr/datasets/NftOtrDataset.ts b/packages/sdk/src/otr/datasets/NftOtrDataset.ts index c3a240f466..33e74691fa 100644 --- a/packages/sdk/src/otr/datasets/NftOtrDataset.ts +++ b/packages/sdk/src/otr/datasets/NftOtrDataset.ts @@ -1,6 +1,7 @@ import { MintMetadataNftTangleRequest, NftBidTangleRequest, + NftPurchaseBulkTangleRequest, NftPurchaseTangleRequest, NftSetForSaleTangleRequest, TangleRequestType, @@ -31,4 +32,10 @@ export class NftOtrDataset extends DatasetClass { requestType: TangleRequestType.MINT_METADATA_NFT, ...params, }); + + bulkPurchase = (params: Omit) => + new OtrRequest(this.otrAddress, { + requestType: TangleRequestType.NFT_PURCHASE_BULK, + ...params, + }); }