From 0f14c6a50cb09c54c2c92ef483ac428281f48220 Mon Sep 17 00:00:00 2001 From: marc0olo Date: Tue, 5 Sep 2023 18:46:49 +0200 Subject: [PATCH 01/11] initial commit after fork with xpr support, tools & soon.market links --- .env | 53 +- Dockerfile | 44 +- README.md | 6 - docker-compose.yml | 4 +- docs/data-types.md | 28 +- docs/plugin-airdrop.md | 3 +- docs/plugin-import.md | 65 +- docs/plugins.md | 8 - docs/testing-guide.md | 16 +- docs/user-guide.md | 100 ++- next.config.js | 8 +- package.json | 51 +- public/xprnetwork.png | Bin 0 -> 36145 bytes src/assets/styles/globals.css | 8 +- src/components/BaseField.tsx | 2 +- .../Card/components/CardContent.tsx | 11 +- src/components/Card/index.tsx | 6 + src/components/Carousel.tsx | 2 +- src/components/CarouselPreview.tsx | 2 +- src/components/Footer.tsx | 103 +-- src/components/Header.tsx | 2 +- src/components/InputPreview.tsx | 8 +- src/components/Loading.tsx | 2 +- src/components/Modal.tsx | 2 +- src/components/SeeMoreButton.tsx | 2 +- src/components/Select.tsx | 2 +- src/components/Switch.tsx | 2 +- src/components/Table.tsx | 2 +- ...luginsContainer.tsx => ToolsContainer.tsx} | 25 +- src/components/TopAppBar/components/Chain.tsx | 4 +- src/components/TopAppBar/components/Login.tsx | 4 +- src/components/TopAppBar/index.tsx | 22 +- src/components/WarningCard.tsx | 2 +- .../collection/CollectionAccountsList.tsx | 4 + .../collection/CollectionAssetsList.tsx | 20 +- src/components/collection/CollectionHints.tsx | 2 +- .../collection/CollectionItemsList.tsx | 2 +- src/components/collection/CollectionStats.tsx | 11 +- .../collection/CollectionTemplatesList.tsx | 15 +- ...lectionPlugins.tsx => CollectionTools.tsx} | 48 +- src/components/collection/CreateNewItem.tsx | 2 +- .../collection/UserCollectionsList.tsx | 25 +- src/components/schema/Attributes.tsx | 2 +- src/configs/chainsConfig.ts | 63 +- src/configs/globalsConfig.ts | 1 + src/libs/authenticators.ts | 11 +- src/pages/404.tsx | 2 +- src/pages/[chainKey]/about.tsx | 112 ++- .../collection/[collectionName].tsx | 4 +- .../[collectionName]/asset/[assetId]/edit.tsx | 2 +- .../collection/[collectionName]/asset/new.tsx | 7 +- .../collection/[collectionName]/edit.tsx | 2 +- .../schema/[schemaName]/edit.tsx | 2 +- .../[collectionName]/schema/new.tsx | 2 +- .../template/[templateId]/edit.tsx | 2 +- .../[collectionName]/template/new.tsx | 2 +- src/pages/[chainKey]/collection/new.tsx | 105 ++- src/pages/[chainKey]/explorer.tsx | 2 +- src/pages/[chainKey]/plugins/index.tsx | 81 -- .../[plugin].tsx => tools/[tool].tsx} | 16 +- src/pages/[chainKey]/tools/index.tsx | 78 ++ src/pages/_app.tsx | 2 +- src/pages/api/{plugins.ts => tools.ts} | 10 +- src/services/account/getAccount.ts | 2 +- .../account/getAccountStatsService.ts | 2 +- src/services/asset/getAssetService.ts | 2 +- src/services/asset/massburnAssetService.ts | 33 + src/services/asset/masscancelSalesService.ts | 32 + .../collection/collectionAccountsService.ts | 2 +- .../collection/collectionAssetsService.ts | 2 +- .../collection/collectionSchemasService.ts | 2 +- .../collection/collectionStatsService.ts | 2 +- .../collection/collectionTemplatesService.ts | 2 +- .../collection/getCollectionService.ts | 2 +- .../collection/listCollectionsService.ts | 2 +- src/services/inventory/getInventoryService.ts | 5 +- src/services/sales/getSalesService.ts | 142 ++++ src/services/schema/getSchemaService.ts | 2 +- src/services/template/getTemplateService.ts | 2 +- .../default/airdrop/config.ts | 8 +- .../default/airdrop/index.tsx | 12 +- .../airdrop/services/getAccountsService.ts | 2 +- .../airdrop/services/getRandomSeedService.ts | 0 .../default/airdrop/utils/utils.ts | 0 .../default/airdrop/utils/validationSchema.ts | 0 src/tools/default/burn/config.ts | 6 + src/tools/default/burn/index.tsx | 516 ++++++++++++ src/tools/default/cancel-sales/config.ts | 6 + src/tools/default/cancel-sales/index.tsx | 496 +++++++++++ .../default/import/components/ipfsPreview.tsx | 0 .../default/import/components/review.tsx | 2 +- .../default/import/config.ts | 2 +- .../default/import/index.tsx | 10 +- .../default/import/utils/utils.ts | 0 src/tools/default/transfer/config.ts | 6 + .../default/transfer/index.tsx} | 44 +- src/utils/blockchains.ts | 6 +- src/utils/collectionTabs.ts | 2 +- src/utils/getChainKeyByChainId.ts | 2 +- src/utils/isValidChainKey.ts | 2 +- tsconfig.json | 56 +- yarn.lock | 789 +++++++++++++++--- 102 files changed, 2663 insertions(+), 776 deletions(-) create mode 100644 public/xprnetwork.png rename src/components/{PluginsContainer.tsx => ToolsContainer.tsx} (62%) rename src/components/collection/{CollectionPlugins.tsx => CollectionTools.tsx} (62%) delete mode 100644 src/pages/[chainKey]/plugins/index.tsx rename src/pages/[chainKey]/{plugins/[plugin].tsx => tools/[tool].tsx} (86%) create mode 100644 src/pages/[chainKey]/tools/index.tsx rename src/pages/api/{plugins.ts => tools.ts} (85%) create mode 100644 src/services/asset/massburnAssetService.ts create mode 100644 src/services/asset/masscancelSalesService.ts create mode 100644 src/services/sales/getSalesService.ts rename src/{plugins => tools}/default/airdrop/config.ts (79%) rename src/{plugins => tools}/default/airdrop/index.tsx (99%) rename src/{plugins => tools}/default/airdrop/services/getAccountsService.ts (93%) rename src/{plugins => tools}/default/airdrop/services/getRandomSeedService.ts (100%) rename src/{plugins => tools}/default/airdrop/utils/utils.ts (100%) rename src/{plugins => tools}/default/airdrop/utils/validationSchema.ts (100%) create mode 100644 src/tools/default/burn/config.ts create mode 100644 src/tools/default/burn/index.tsx create mode 100644 src/tools/default/cancel-sales/config.ts create mode 100644 src/tools/default/cancel-sales/index.tsx rename src/{plugins => tools}/default/import/components/ipfsPreview.tsx (100%) rename src/{plugins => tools}/default/import/components/review.tsx (99%) rename src/{plugins => tools}/default/import/config.ts (92%) rename src/{plugins => tools}/default/import/index.tsx (98%) rename src/{plugins => tools}/default/import/utils/utils.ts (100%) create mode 100644 src/tools/default/transfer/config.ts rename src/{pages/[chainKey]/transfer.tsx => tools/default/transfer/index.tsx} (90%) diff --git a/.env b/.env index d804633..8422557 100644 --- a/.env +++ b/.env @@ -1,39 +1,26 @@ -NEXT_PUBLIC_APP_NAME=FACINGS Creator -NEXT_PUBLIC_APP_DESCRIPTION=The FACINGS Creator is a platform for managing AtomicAssets NFT collections, based upon the open source collection-manager project. -NEXT_PUBLIC_FAVICON=https://facings.fra1.cdn.digitaloceanspaces.com/FACINGS%20Logo%20MASK_Negative%20RGB.svg -NEXT_PUBLIC_META_ICON=https://cdn.facings.io/FACINGS_Logo_mask_negative.png -NEXT_PUBLIC_APP_URL=https://creator.facings.io +NEXT_PUBLIC_APP_NAME=NFT Manager +NEXT_PUBLIC_APP_DESCRIPTION=The NFT manager is a platform for managing AtomicAssets NFT collections, based upon the open source collection-manager project by Facings. +NEXT_PUBLIC_FAVICON=https://soon.market/favicon.png +NEXT_PUBLIC_META_ICON=https://media.soon.market/images/soon-logo.png +NEXT_PUBLIC_APP_URL=https://manager.soon.market -NEXT_PUBLIC_CHAIN_KEY_DEFAULT=eos +NEXT_PUBLIC_CHAIN_KEY_DEFAULT=xpr #IPFS -NEXT_PUBLIC_IPFS_ENDPOINT=https://facings.mypinata.cloud/ipfs -NEXT_PUBLIC_IPFS_API_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySW5mb3JtYXRpb24iOnsiaWQiOiJmYTk1YzM5MS01ZDlkLTQyNDgtYWZlYS0xMjJhNzVhMjNhZjkiLCJlbWFpbCI6InJvYkBlb3NkZXRyb2l0LmlvIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInBpbl9wb2xpY3kiOnsicmVnaW9ucyI6W3siaWQiOiJOWUMxIiwiZGVzaXJlZFJlcGxpY2F0aW9uQ291bnQiOjF9XSwidmVyc2lvbiI6MX0sIm1mYV9lbmFibGVkIjpmYWxzZSwic3RhdHVzIjoiQUNUSVZFIn0sImF1dGhlbnRpY2F0aW9uVHlwZSI6InNjb3BlZEtleSIsInNjb3BlZEtleUtleSI6ImZjMmU0ZGI2NGJiYjRjNjZmNWFkIiwic2NvcGVkS2V5U2VjcmV0IjoiYTRhNWU4OThhMDE2YTVjYjVjNTI2ZTU0NTBmMTI2Y2RlZWYzODIzYTM2Y2FhNjIxZDRmYmU1YjYwNTNjZjBjMSIsImlhdCI6MTY3Nzc5NTkwMX0.ap-Y4EAuWB5b0FjxiXVsHSdV1ZjFMg3FEVp-lZ2BWSE +NEXT_PUBLIC_IPFS_ENDPOINT=https://ipfs-gateway.soon.market/ipfs -#WAX CHAIN -NEXT_PUBLIC_WAX_MAINNET_AA_ENDPOINT=https://api.facings4ever.detroitledger.tech -NEXT_PUBLIC_WAX_MAINNET_CHAIN_ID=1064487b3cd1a897ce03ae5b6a865651747e2e152090f99c1d19d44e01aea5a4 -NEXT_PUBLIC_WAX_MAINNET_PROTOCOL=https -NEXT_PUBLIC_WAX_MAINNET_HOST=api.facings4ever.detroitledger.tech -NEXT_PUBLIC_WAX_MAINNET_PORT=443 +#XPR NETWORK +NEXT_PUBLIC_XPR_NETWORK_MAINNET_AA_ENDPOINT=https://api.soon.market +NEXT_PUBLIC_XPR_NETWORK_MAINNET_CHAIN_ID=384da888112027f0321850a169f737c33e53b388aad48b5adace4bab97f437e0 +NEXT_PUBLIC_XPR_NETWORK_MAINNET_PROTOCOL=https +NEXT_PUBLIC_XPR_NETWORK_MAINNET_HOST=metal-proton-rpc.global.binfra.one +NEXT_PUBLIC_XPR_NETWORK_MAINNET_PORT=443 -#WAX TESTNET CHAIN -NEXT_PUBLIC_WAX_TESTNET_AA_ENDPOINT=https://atomic.testnet.wax.detroitledger.tech -NEXT_PUBLIC_WAX_TESTNET_CHAIN_ID=f16b1833c747c43682f4386fca9cbb327929334a762755ebec17f6f23c9b8a12 -NEXT_PUBLIC_WAX_TESTNET_PROTOCOL=https -NEXT_PUBLIC_WAX_TESTNET_HOST=testnet.wax.detroitledger.tech -NEXT_PUBLIC_WAX_TESTNET_PORT=443 +#XPR_NETWORK TESTNET CHAIN +NEXT_PUBLIC_XPR_NETWORK_TESTNET_AA_ENDPOINT=https://test.proton.api.atomicassets.io +NEXT_PUBLIC_XPR_NETWORK_TESTNET_CHAIN_ID=71ee83bcf52142d61019d95f9cc5427ba6a0d7ff8accd9e2088ae2abeaf3d3dd +NEXT_PUBLIC_XPR_NETWORK_TESTNET_PROTOCOL=https +NEXT_PUBLIC_XPR_NETWORK_TESTNET_HOST=metal-protontest-rpc.global.binfra.one +NEXT_PUBLIC_XPR_NETWORK_TESTNET_PORT=443 -#EOS MAINNET CHAIN -NEXT_PUBLIC_EOS_MAINNET_AA_ENDPOINT=https://api.eos.detroitledger.tech -NEXT_PUBLIC_EOS_MAINNET_CHAIN_ID=aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906 -NEXT_PUBLIC_EOS_MAINNET_PROTOCOL=https -NEXT_PUBLIC_EOS_MAINNET_HOST=api.eos.detroitledger.tech -NEXT_PUBLIC_EOS_MAINNET_PORT=443 - -#EOS JUNGLE4 CHAIN -NEXT_PUBLIC_EOS_JUNGLE4_AA_ENDPOINT=https://api.jungle.detroitledger.tech -NEXT_PUBLIC_EOS_JUNGLE4_CHAIN_ID=73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d -NEXT_PUBLIC_EOS_JUNGLE4_PROTOCOL=https -NEXT_PUBLIC_EOS_JUNGLE4_HOST=api.jungle.detroitledger.tech -NEXT_PUBLIC_EOS_JUNGLE4_PORT=443 +NEXT_PUBLIC_REQUEST_ACCOUNT=soonmarket \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 27ea16d..a3f1b70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,35 +1,33 @@ -FROM node:16-alpine +FROM node:18.14.2-alpine3.16 AS builder -# Setting working directory. All the path will be relative to WORKDIR -WORKDIR /usr/app +ENV NODE_ENV production -# Install PM2 globally -RUN npm install --global pm2 +WORKDIR /app +COPY . . +RUN yarn install --frozen-lockfile -# Installing dependencies -COPY ./yarn.lock ./ -COPY ./package*.json ./ +RUN yarn build -# Install dependencies -RUN yarn install +FROM node:18.14.2-alpine3.16 AS runner +ENV NODE_ENV production +ENV PORT 3000 -# Copying source files -COPY ./ ./ +RUN adduser --disabled-password soonmanager && \ + mkdir -p /app && \ + chown -R soonmanager:soonmanager /app -# Build app -RUN yarn build +WORKDIR /app -RUN chown -R node .next +USER soonmanager -# Expose the listening port -EXPOSE 3000 +COPY --from=builder /app/.env .env -# The node user is provided in the Node.js Alpine base image +COPY --from=builder /app/public ./public +COPY --from=builder --chown=soonmanager:soonmanager /app/.next/standalone ./ +COPY --from=builder --chown=soonmanager:soonmanager /app/.next/static ./.next/static +COPY --from=builder /app/next.config.js ./next.config.js -USER node - -# Run npm start script with PM2 when container starts -#CMD [ "pm2-runtime", "start", "yarn" ] +EXPOSE 3000 -CMD [ "pm2-runtime", "start", "npm", "--", "start" ] +ENTRYPOINT ["node", "server.js"] \ No newline at end of file diff --git a/README.md b/README.md index 68058f9..57b6d72 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ An NFT Collection Manager for the EOS/AtomicAssets ecosystem built by [FACINGS]( This project is meant to both work as a stand-alone NFT publishing platform, as well as a launchpad for NFT developers on EOS. - The core feature set is very simple: 1. Login and view resource usage @@ -18,7 +17,6 @@ Important principles: 2. Allow publishers, developers, and businesses to build faster 3. Grow open-source community around core EOS/AtomicAssets needs - ## Documentation 1. **Getting Started** (this README document) - Project overview and "Getting Started" guide for devs. @@ -29,14 +27,12 @@ Important principles: 6. **[Data Import Plugin](docs/plugin-import.md)** - For users, guide to using the CSV import plugin. 7. **[Data Types](docs/data-types.md)** - An overview of AtomicAssets data types. - ## Dependencies 1. Public AtomicAsset API ([AGPLv3](https://github.com/pinknetworkx/eosio-contract-api); use any [public endpoint](https://support.pink.gg/hc/en-us/articles/4405478859922-Developer-Resources)) 2. Public Node API endpoint ([MIT](https://github.com/EOSIO/eos); use any public [EOS](https://mainnet.eosio.online/endpoints) or [WAX](https://wax.eosio.online/endpoints) endpoint) 3. Public IPFS endpoint ([MIT](https://github.com/ipfs/ipfs); e.g. https://ipfs.ledgerwise.io/ipfs) - ## Getting Started - Development Ensure all project dependencies are installed: @@ -55,7 +51,6 @@ yarn dev Now you may open [http://localhost:3000](http://localhost:3000) to access the app. - ## Running the application with docker Install Docker and execute: @@ -66,7 +61,6 @@ docker-compose up --build --force-recreate After build, open [http://localhost:3000](http://localhost:3000) to access the app. - ## Environment Variables Next.js allows you to set defaults in `.env` (all environments), diff --git a/docker-compose.yml b/docker-compose.yml index 6cccc03..48295bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,9 +15,9 @@ x-common: &commons services: app: - image: collection-manager + image: nft-manager build: . ports: - 3000:3000 healthcheck: - test: ["CMD", "curl", "http://localhost:3000/eos"] \ No newline at end of file + test: ["CMD", "curl", "http://localhost:3000/xpr"] \ No newline at end of file diff --git a/docs/data-types.md b/docs/data-types.md index ec96610..0ea5a0c 100644 --- a/docs/data-types.md +++ b/docs/data-types.md @@ -1,32 +1,37 @@ ## Introduction -To allow efficient use of blockchain resources, AtomicAssets gives publishers the ability to pick precisely the data types they need for whatever metadata attributes need to be stored. Storage space requirements are generally measured and priced in terms of *bytes*. + +To allow efficient use of blockchain resources, AtomicAssets gives publishers the ability to pick precisely the data types they need for whatever metadata attributes need to be stored. Storage space requirements are generally measured and priced in terms of _bytes_. Especially for launching large collections, it becomes important to be aware of how you are storing data to ensure it's not wasteful. The main principle to remember is to pick the smallest data types that work for your needs. ## Numerical Types ### Integer Types (whole numbers, including negatives) -*Integers* are whole numbers and can include negative values. Also referred to as *Signed Integers*, meaning they can include numbers with a "negative sign". + +_Integers_ are whole numbers and can include negative values. Also referred to as _Signed Integers_, meaning they can include numbers with a "negative sign". - `int8` (1 byte): whole numbers `-128` to `127` - `int16` (2 bytes): whole numbers `-32,768` to `32,767` - `int32` (4 bytes): whole numbers `-2,147,483,648` to `2,147,483,647` - `int64` (8 bytes): whole numbers `-9,223,372,036,854,775,808` to `9,223,372,036,854,775,807` -*Technical note: Integers are stored as [zig-zag encoded](https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba#file-zigzag-encoding-readme) varints.* +_Technical note: Integers are stored as [zig-zag encoded](https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba#file-zigzag-encoding-readme) varints._ ### Unsigned Integer Types (whole numbers, no negative values) -*Unsigned Integers* are whole numbers greater or equal to zero (no negative numbers). + +_Unsigned Integers_ are whole numbers greater or equal to zero (no negative numbers). - `uint8` (1 byte): whole numbers `0` to `255` - `uint16` (2 bytes): whole numbers `0` to `65,535` - `uint32` (4 bytes): whole numbers `0` to `4,294,967,295` - `uint64` (8 bytes): whole numbers `0` to `18,446,744,073,709,551,615` -*Technical note: Unsigned Integers are stored as varints.* +_Technical note: Unsigned Integers are stored as varints._ ### Fixed Types -For advanced users only. The `fixed` type is an alias for `uint`, but not stored as `varints` and instead as a *fixed size in little endian order* ([source](https://github.com/pinknetworkx/atomicassets-contract/wiki/Serialization)). + +For advanced users only. The `fixed` type is an alias for `uint`, but not stored as `varints` and instead as a _fixed size in little endian order_ ([source](https://github.com/pinknetworkx/atomicassets-contract/wiki/Serialization)). + - `fixed8` (1 byte): `0` to `255` - `fixed16` (2 bytes): `0` to `65,535` - `fixed32` (4 bytes): `0` to `4,294,967,295` @@ -34,18 +39,21 @@ For advanced users only. The `fixed` type is an alias for `uint`, but not stored - `byte` (1 byte): an alias for `fixed8`: `0` to `255` ### Float/Double -*Floats* and *Doubles* are generally used whenever you need numbers with a decimal component or in cases where the values are very large. While they are imprecise (for instance, setting a float value of `0.3` might internally be represented as `0.299999999`), they allow you to store very large numbers such as `1e28` or very small numbers such as `1e-30`. + +_Floats_ and _Doubles_ are generally used whenever you need numbers with a decimal component or in cases where the values are very large. While they are imprecise (for instance, setting a float value of `0.3` might internally be represented as `0.299999999`), they allow you to store very large numbers such as `1e28` or very small numbers such as `1e-30`. - `float` (4 bytes): numbers as high as `3.4e38` or as small as `1.7e-38`, with about 7 digits of precision - `double` (8 bytes): numbers as high as `1.7e308` or as small as `1.7e-308`, with about 15 digits of precision ## Other Data Types + - `string` (~1 byte per character): Stores any length of text. - `ipfs` (~32 bytes): stores a Base58 IPFS address. -- `bool` (1 byte): *boolean* has two possible values: `1` (`true`) and `0` (`false`). (e.g. `is_burnable`) +- `bool` (1 byte): _boolean_ has two possible values: `1` (`true`) and `0` (`false`). (e.g. `is_burnable`) ## Vectors -While the `collection-manager` app does not currently allow it, the AtomicAssets contract allows any type to be turned into a *vector* by appending `[]` to the type name (e.g. `int32[]`). Nested vectors (e.g. `int32[][]`) are not allowed. + +While the `collection-manager` app does not currently allow it, the AtomicAssets contract allows any type to be turned into a _vector_ by appending `[]` to the type name (e.g. `int32[]`). Nested vectors (e.g. `int32[][]`) are not allowed. For example, a user-defined schema field like `tags` can contain multiple values if the data type is `string[]`. @@ -56,7 +64,7 @@ For example, a user-defined schema field like `tags` can contain multiple values 3. To store a number that can range from `0..50000`, use a `uint16`. 4. To store an IPFS hash, use `ipfs` type. 5. To store a number which is very large, very small, or has many decimal points, use a `float` or `double`. `float` provides about 7 digits of precision, while `double` about 15. -6. To store a number with many decimal places *precisely*, use one of the `uint` or `int` types and multiply/divide by some factor of 10 when writing or reading the value. The only way to get precise decimal places is to use an *Integer* type (which only allows whole numbers) and shift the decimal place as needed. +6. To store a number with many decimal places _precisely_, use one of the `uint` or `int` types and multiply/divide by some factor of 10 when writing or reading the value. The only way to get precise decimal places is to use an _Integer_ type (which only allows whole numbers) and shift the decimal place as needed. ## References diff --git a/docs/plugin-airdrop.md b/docs/plugin-airdrop.md index 8e7b7f7..d957f70 100644 --- a/docs/plugin-airdrop.md +++ b/docs/plugin-airdrop.md @@ -15,6 +15,7 @@ holders. ### Querying Accounts There are three options for querying holders: + 1. Search by template - enter 1 or more template ids to retrieve a list of holders. 2. Search by collection - enter 1 or more collection names to retrieve a list of holders. 3. Search by not holding template - enter a collection and template id to @@ -26,6 +27,7 @@ There are three options for querying holders: When "Unique accounts only" is selected, the query will not return duplicate accounts. If this toggle is not selected: + 1. When searching by a template, accounts holding multiple ("X") copies will be added "X" times. 2. When searching by multiple collections, accounts holding multiple collections will be added multiple times. @@ -39,7 +41,6 @@ accounts here, separated by commas. The dice button on the right side of this field will shuffle the listed accounts using a seed obtained from random.org. Press it as many times as you like. - ## NFT Selection ### Mint NFTs from template diff --git a/docs/plugin-import.md b/docs/plugin-import.md index 988da23..ce5335d 100644 --- a/docs/plugin-import.md +++ b/docs/plugin-import.md @@ -18,7 +18,6 @@ The benefits of this approach: catch mistakes such as empty or non-unique entries before the schema and templates are created. - ## Step 1: Preparing the Spreadsheet To start, we assume you have a spreadsheet where the first row contains @@ -33,39 +32,34 @@ This spreadsheet consists of 4 columns and 5 rows, from which 1 schema and 4 templates will be created. | name | img | points | description | -|--------|-------------------------------------------------------------|--------|---------------------| +| ------ | ----------------------------------------------------------- | ------ | ------------------- | | Apple | QmY2yFqDmSPWiFbzsMeREHXyCkY5LdY62WWyYUhe5ogpmT | 5 | Crunchy round fruit | | Banana | bafkreidullbimqjiob2apauxv3tj23kupb5em3c4i5soks2svsbqrvasce | 2 | Mushy yellow fruit | | Kiwi | bafkreichwf6uollkwe55bmrvnt3744rnmn5g3boekoh62krgu2umv4qpgu | 3 | Hairy green fruit | | Cherry | QmeciM9AtHdCycNAwtWh6bAsLCaKnWjgFAg95pFuP63AGW | 8 | Red stone fruit | - - ## Step 2: Add system columns There are 4 columns that **must be present** when importing a CSV file: 1. `max_supply`: (whole number) the maximum number of NFTs which can be minted - from the template. (set to `0` for *unlimited*) + from the template. (set to `0` for _unlimited_) 2. `burnable`: (`TRUE` or `FALSE`) recommended set to `TRUE` to allow users the option to burn their NFTs. 3. `transferable`: (`TRUE` or `FALSE`) recommended set to `TRUE` to allow users to trade their NFTs. 4. `sysflag`: leave this column blank for now. - ##### Example: Note: ❌ indicates cells which are to be left blank. | name | img | points | description | **max_supply** | **burnable** | **transferable** | **sysflag** | -|--------|-------------------------------------------------------------|--------|---------------------|:--------------:|:------------:|:----------------:|:-----------:| -| Apple | QmY2yFqDmSPWiFbzsMeREHXyCkY5LdY62WWyYUhe5ogpmT | 5 | Crunchy round fruit | **100** | **TRUE** | **TRUE** | ❌ | -| Banana | bafkreidullbimqjiob2apauxv3tj23kupb5em3c4i5soks2svsbqrvasce | 2 | Mushy yellow fruit | **100** | **TRUE** | **TRUE** | ❌ | -| Kiwi | bafkreichwf6uollkwe55bmrvnt3744rnmn5g3boekoh62krgu2umv4qpgu | 3 | Hairy green fruit | **100** | **TRUE** | **TRUE** | ❌ | -| Cherry | QmeciM9AtHdCycNAwtWh6bAsLCaKnWjgFAg95pFuP63AGW | 8 | Red stone fruit | **100** | **TRUE** | **TRUE** | ❌ | - - +| ------ | ----------------------------------------------------------- | ------ | ------------------- | :------------: | :----------: | :--------------: | :---------: | +| Apple | QmY2yFqDmSPWiFbzsMeREHXyCkY5LdY62WWyYUhe5ogpmT | 5 | Crunchy round fruit | **100** | **TRUE** | **TRUE** | ❌ | +| Banana | bafkreidullbimqjiob2apauxv3tj23kupb5em3c4i5soks2svsbqrvasce | 2 | Mushy yellow fruit | **100** | **TRUE** | **TRUE** | ❌ | +| Kiwi | bafkreichwf6uollkwe55bmrvnt3744rnmn5g3boekoh62krgu2umv4qpgu | 3 | Hairy green fruit | **100** | **TRUE** | **TRUE** | ❌ | +| Cherry | QmeciM9AtHdCycNAwtWh6bAsLCaKnWjgFAg95pFuP63AGW | 8 | Red stone fruit | **100** | **TRUE** | **TRUE** | ❌ | ## Step 3: Specify data types @@ -74,6 +68,7 @@ Note: ❌ indicates cells which are to be left blank. 3. For each of your original columns, enter the desired datatype in this row (ignore `max_supply`, `burnable`, and `transferable`). Example data types: + 1. `string`: any text content 2. `uint8`: whole numbers from `0` to `255` 3. `int16`: whole numbers from `-32,768` to `32,767` @@ -84,19 +79,17 @@ Example data types: For a full list of allowed types, see [Data Types](data-types.md). - ##### Example Note: ❌ indicates cells which are to be left blank. -| name | img | points | description | max_supply | burnable | transferable | sysflag | -|------------|-------------------------------------------------------------|-----------|---------------------|:----------:|:--------:|:------------:|:------------:| -| **string** | **image** | **uint8** | **string** | ❌ | ❌ | ❌ | **datatype** | -| Apple | QmY2yFqDmSPWiFbzsMeREHXyCkY5LdY62WWyYUhe5ogpmT | 5 | Crunchy round fruit | 100 | TRUE | TRUE | ❌ | -| Banana | bafkreidullbimqjiob2apauxv3tj23kupb5em3c4i5soks2svsbqrvasce | 2 | Mushy yellow fruit | 100 | TRUE | TRUE | ❌ | -| Kiwi | bafkreichwf6uollkwe55bmrvnt3744rnmn5g3boekoh62krgu2umv4qpgu | 3 | Hairy green fruit | 100 | TRUE | TRUE | ❌ | -| Cherry | QmeciM9AtHdCycNAwtWh6bAsLCaKnWjgFAg95pFuP63AGW | 8 | Red stone fruit | 100 | TRUE | TRUE | ❌ | - +| name | img | points | description | max_supply | burnable | transferable | sysflag | +| ---------- | ----------------------------------------------------------- | --------- | ------------------- | :--------: | :------: | :----------: | :----------: | +| **string** | **image** | **uint8** | **string** | ❌ | ❌ | ❌ | **datatype** | +| Apple | QmY2yFqDmSPWiFbzsMeREHXyCkY5LdY62WWyYUhe5ogpmT | 5 | Crunchy round fruit | 100 | TRUE | TRUE | ❌ | +| Banana | bafkreidullbimqjiob2apauxv3tj23kupb5em3c4i5soks2svsbqrvasce | 2 | Mushy yellow fruit | 100 | TRUE | TRUE | ❌ | +| Kiwi | bafkreichwf6uollkwe55bmrvnt3744rnmn5g3boekoh62krgu2umv4qpgu | 3 | Hairy green fruit | 100 | TRUE | TRUE | ❌ | +| Cherry | QmeciM9AtHdCycNAwtWh6bAsLCaKnWjgFAg95pFuP63AGW | 8 | Red stone fruit | 100 | TRUE | TRUE | ❌ | ## Step 4: (Optional) Additional properties and validators @@ -111,7 +104,7 @@ Note: ❌ indicates cells which are to be left blank. Validators are optional but recommended as they will help you capture any potential issues or mistakes in your data (e.g. missing or repeated values). -Any failed validations will be reported by the Importer *before* anything is +Any failed validations will be reported by the Importer _before_ anything is written to the blockchain. Futute validators may include features like "spellcheck". @@ -120,10 +113,11 @@ Futute validators may include features like "spellcheck". - `unique`: `TRUE` or `FALSE` - if set to `TRUE`, the data import will verify that all templates to be created have no duplicate values for this field. -### How to add a *Property* or *Validator* +### How to add a _Property_ or _Validator_ + +The process of adding _properties_ or _validators_ is similar to adding the +required `datatype` _sysflag_: -The process of adding *properties* or *validators* is similar to adding the -required `datatype` *sysflag*: 1. Insert a new header row (e.g. after the `datatype` row but before the templates start. in the example below, two validators were added). 2. Enter the desired values, ignoring `max_supply`, `burnable`, and `transferable`. @@ -134,16 +128,15 @@ At this point, the spreadsheet will look something like this: Note: ❌ indicates cells which are to be left blank. -| name | img | points | description | max_supply | burnable | transferable | sysflag | -|----------|-------------------------------------------------------------|-----------|---------------------|:----------:|:--------:|:------------:|:------------:| -| string | image | uint8 | string | ❌ | ❌ | ❌ | datatype | -| **TRUE** | **TRUE** | **TRUE** | **TRUE** | ❌ | ❌ | ❌ | **required** | -| **TRUE** | **TRUE** | **FALSE** | **TRUE** | ❌ | ❌ | ❌ | **unique** | -| Apple | QmY2yFqDmSPWiFbzsMeREHXyCkY5LdY62WWyYUhe5ogpmT | 5 | Crunchy round fruit | 100 | TRUE | TRUE | ❌ | -| Banana | bafkreidullbimqjiob2apauxv3tj23kupb5em3c4i5soks2svsbqrvasce | 2 | Mushy yellow fruit | 100 | TRUE | TRUE | ❌ | -| Kiwi | bafkreichwf6uollkwe55bmrvnt3744rnmn5g3boekoh62krgu2umv4qpgu | 3 | Hairy green fruit | 100 | TRUE | TRUE | ❌ | -| Cherry | QmeciM9AtHdCycNAwtWh6bAsLCaKnWjgFAg95pFuP63AGW | 8 | Red stone fruit | 100 | TRUE | TRUE | ❌ | - +| name | img | points | description | max_supply | burnable | transferable | sysflag | +| -------- | ----------------------------------------------------------- | --------- | ------------------- | :--------: | :------: | :----------: | :----------: | +| string | image | uint8 | string | ❌ | ❌ | ❌ | datatype | +| **TRUE** | **TRUE** | **TRUE** | **TRUE** | ❌ | ❌ | ❌ | **required** | +| **TRUE** | **TRUE** | **FALSE** | **TRUE** | ❌ | ❌ | ❌ | **unique** | +| Apple | QmY2yFqDmSPWiFbzsMeREHXyCkY5LdY62WWyYUhe5ogpmT | 5 | Crunchy round fruit | 100 | TRUE | TRUE | ❌ | +| Banana | bafkreidullbimqjiob2apauxv3tj23kupb5em3c4i5soks2svsbqrvasce | 2 | Mushy yellow fruit | 100 | TRUE | TRUE | ❌ | +| Kiwi | bafkreichwf6uollkwe55bmrvnt3744rnmn5g3boekoh62krgu2umv4qpgu | 3 | Hairy green fruit | 100 | TRUE | TRUE | ❌ | +| Cherry | QmeciM9AtHdCycNAwtWh6bAsLCaKnWjgFAg95pFuP63AGW | 8 | Red stone fruit | 100 | TRUE | TRUE | ❌ | ## Step 5: Export to CSV diff --git a/docs/plugins.md b/docs/plugins.md index 25812a1..c281299 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -6,7 +6,6 @@ functionality to your deployment. Currently, all plugins are collection-level and, when enabled, are exposed on the "Plugins" tab of the Collection page. - ## Configuring Plugins The following directories are scanned for plugins: @@ -16,7 +15,6 @@ The following directories are scanned for plugins: All plugins found in either folder will be enabled on the UI. - ## Default Plugins ### `import`: Data import @@ -25,7 +23,6 @@ This plugin enables users to upload a CSV file from which to create a schema and any number of templates. See [`plugin-import.md`](plugin-import.md) for additional details. - ## External Plugins To add external plugins, simply add them to the `src/plugins/external` folder. @@ -35,7 +32,6 @@ They will be detected automatically upon restarting the app. It is recommended to use remote git repos, but the only requirement is that the plugins follow the rules outlined in the "Creating a Plugin" section below. - ### Installing the "Hello World" plugin `hello` is an example of a minimal plugin which can be cloned and adapted for @@ -50,9 +46,6 @@ git clone https://github.com/FACINGS/hello-plugin.git After restarting the app, you will see "Hello" in the Collection Plugins tab. - - - ## Creating a Plugin All plugins are required to have an `index.tsx` file located at @@ -86,7 +79,6 @@ export default function Hello() { } ``` - ## Contributing Plugins We welcome all merge requests for plugins which extend the functionality of the diff --git a/docs/testing-guide.md b/docs/testing-guide.md index a65ac0d..66b26ef 100644 --- a/docs/testing-guide.md +++ b/docs/testing-guide.md @@ -88,7 +88,6 @@ Note that many published collections may not have any published shemas or templa 2. Click on "Edit NFT", then "Burn NFT" tab. 3. Click on the "Burn NFT" button to sign and broadcast the transaction. - ## 4. Testing Stage 2, Milestone 1 deliverables ### Deliverable 1: TypeScript Implementation @@ -129,8 +128,6 @@ Documentation of the plugin system and instructions on how to install a sample p https://github.com/FACINGS/collection-manager/blob/main/docs/plugins.md - - ## 5. Testing Stage 2, Milestone 2 deliverables ### Deliverable 1: Import Function @@ -158,7 +155,6 @@ You may view the documentation of the standard here: [Data Import Plugin](plugin > Ability to submit transactions in batches - 1. Download sample file: [batch.test.csv](plugin-import-sample/batch.test.csv) 2. Start on the "Import" page (Collection > Plugins > Import) 3. Select the `batch.test.csv` file you downloaded in step 1 @@ -166,13 +162,12 @@ You may view the documentation of the standard here: [Data Import Plugin](plugin and you will be able to adjust the batch size from the default of 25, if desired. 5. Submitting the form will result in the user being prompted to sign a series of transactions. - ### Deliverable 2: Advanced Validation > Data validation heuristics upon import: -> (a) Uniqueness; -> (b) Completeness; -> (c) Datatype optimization +> (a) Uniqueness; +> (b) Completeness; +> (c) Datatype optimization 1. Download sample file: [validationtest.csv](plugin-import-sample/validationtest.csv) 2. Start on the "Import" page (Collection > Plugins > Import) @@ -183,10 +178,8 @@ You may view the documentation of the standard here: [Data Import Plugin](plugin 3. Required field error: "Kiwi" is missing a value for the `points` field, which was marked as 'required' 4. IPFS field validation error: "Orange" has an invalid entry for the `img` field, which expects an IPFS address - ## 6. Testing Stage 2, Milestone 3 deliverables - ### Deliverable 1: Airdrop Plugin Core > - Specify single & multiple recipients @@ -205,15 +198,16 @@ You may view the documentation of the standard here: [Data Import Plugin](plugin 2. Transfer from NFT IDs - specify (or query) a list of NFT (asset) IDs; submission transfers to the specified accounts. 4. Batch Size - at the bottom of the form, the user has control over batch size. This can be verified by inspecting the generated transactions. - ### Deliverable 2: Airdrop API Queries > Look-up Queries: +> > - everyone who has specific template > - everyone who doesn’t have specific templates within a collection > - everyone who has any item from a collection > > Unique Queries: +> > - option to send single or multiple assets to matched accounts 1. From the Airdrop page, these account queries are visible at the very top diff --git a/docs/user-guide.md b/docs/user-guide.md index 58e1cf5..dca3fa4 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -1,5 +1,4 @@ - -The FACINGS NFT Creator allows you to publish, manage, and distribute AtomicAssets NFTs. This guide assumes you have general knowledge of the EOS ecosystem, an Antelope account (such as EOS, WAX, or Proton) with sufficient resources to publish transactions, and a supported wallet such as *Anchor* or *Cloud Wallet*. +The FACINGS NFT Creator allows you to publish, manage, and distribute AtomicAssets NFTs. This guide assumes you have general knowledge of the EOS ecosystem, an Antelope account (such as EOS, WAX, or Proton) with sufficient resources to publish transactions, and a supported wallet such as _Anchor_ or _Cloud Wallet_. If you need more help getting started with account setup and familiarity with Antelope blockchains, we recommend starting here: https://academy.anyo.io/ If it's your first time publishing an NFT collection or you are simply testing the app, you should create a free testnet account before following this guide. Currently this app supports both [EOS Jungle4 Testnet](https://eosinabox.com/) and [WAX Testnet](https://waxsweden.org/create-testnet-account/). (Click links to create a free testing account.) @@ -9,39 +8,38 @@ If it's your first time publishing an NFT collection or you are simply testing t Important concepts in the AtomicAssets standard (using alphakittens on EOS as an example): 1. **"Collection"** - 1. the top-level record for your project: it can represent a Dapp or art collection. - 2. has a special field *Collection Name* which must be unique; and once set, it is not editable. This field adheres to the Antelope naming standard for accounts. You can make a short name (less than 12 characters) only if the author account creating the collection has the corresponding short name. - 3. has editable fields such as *Display Name*, *Description*, *Website*, and *Market Fee*. - 4. is the container for *Schemas*, *Templates*, and *NFTs*. - 5. editing permissions are managed throught the `authorized_accounts` field. - 6. example: the `alphakittens` collection has a *Display Name* of "Alpha Kittens" and a *Market Fee* of 6%. + 1. the top-level record for your project: it can represent a Dapp or art collection. + 2. has a special field _Collection Name_ which must be unique; and once set, it is not editable. This field adheres to the Antelope naming standard for accounts. You can make a short name (less than 12 characters) only if the author account creating the collection has the corresponding short name. + 3. has editable fields such as _Display Name_, _Description_, _Website_, and _Market Fee_. + 4. is the container for _Schemas_, _Templates_, and _NFTs_. + 5. editing permissions are managed throught the `authorized_accounts` field. + 6. example: the `alphakittens` collection has a _Display Name_ of "Alpha Kittens" and a _Market Fee_ of 6%. 2. **"Schema"** - 1. a collection of attributes ("fields") and their data type, e.g. `name (text)`, `age (number)`, `author (text)`. - 2. belongs to a collection. there can be any number of *Schemas* per *Collection*. - 3. must have a unique name within the collection (e.g. `kittens` or `artwork`). - 4. cannot be renamed or edited after creation, but can have attributes added to it. - 5. example: the `alphakittens` collection has one schema named `kittens` with two attributes: `name (text)` and `age (number)`. + 1. a collection of attributes ("fields") and their data type, e.g. `name (text)`, `age (number)`, `author (text)`. + 2. belongs to a collection. there can be any number of _Schemas_ per _Collection_. + 3. must have a unique name within the collection (e.g. `kittens` or `artwork`). + 4. cannot be renamed or edited after creation, but can have attributes added to it. + 5. example: the `alphakittens` collection has one schema named `kittens` with two attributes: `name (text)` and `age (number)`. 3. **"Template"** - 1. a specific configuration from which NFTs are minted. - 2. must be created from a specific schema (e.g. `kittens`). - 3. determines the total supply of NFTs that can be created from this - specific Template and its values. - 4. provides values for the attributes in that schema. - 5. designed to make easy work of minting multiple NFTs that are nearly identical. - 6. templates are not tradable, but templates are used to mint NFTs. - 7. cannot be modified after creation, with the exception of limiting supply of unminted NFTs. - 8. determines if attributes of the NFT are *mutable* (can be modified later) - or *immutable* (permanent). If an attribute/value are in a template, they - are *always* immutable. Mutable fields are fields which are defined on - the *schema* but *not* the *template*, and are set upon minting the NFT. - 9. example: I'm making a new template from the `kittens` schema, the `name` will be "Tom" and the `age` will be "4". + 1. a specific configuration from which NFTs are minted. + 2. must be created from a specific schema (e.g. `kittens`). + 3. determines the total supply of NFTs that can be created from this + specific Template and its values. + 4. provides values for the attributes in that schema. + 5. designed to make easy work of minting multiple NFTs that are nearly identical. + 6. templates are not tradable, but templates are used to mint NFTs. + 7. cannot be modified after creation, with the exception of limiting supply of unminted NFTs. + 8. determines if attributes of the NFT are _mutable_ (can be modified later) + or _immutable_ (permanent). If an attribute/value are in a template, they + are _always_ immutable. Mutable fields are fields which are defined on + the _schema_ but _not_ the _template_, and are set upon minting the NFT. + 9. example: I'm making a new template from the `kittens` schema, the `name` will be "Tom" and the `age` will be "4". 4. **"NFTs"** (also known as "Assets") - 1. *minted* (created) from a specific *Template*. - 2. can be minted individually or all at once. - 3. can be minted directly to your account or to any combination of accounts. - 4. may include *mutable* data that the collection creator can modify. - 5. tradable and burnable (assuming the creator left these "on" when creating the *Template*). - + 1. _minted_ (created) from a specific _Template_. + 2. can be minted individually or all at once. + 3. can be minted directly to your account or to any combination of accounts. + 4. may include _mutable_ data that the collection creator can modify. + 5. tradable and burnable (assuming the creator left these "on" when creating the _Template_). ### Additional Resources @@ -49,8 +47,6 @@ Important concepts in the AtomicAssets standard (using alphakittens on EOS as an - The [WAX Developer Portal](https://developer.wax.io/en/tutorials/howto_atomicassets/collection_struct.html) offers additional technical background and resources. ![](https://developer.wax.io/assets/img/tutorials/howto_atomicassets/atomicassets_scheme.jpg) - - # User Guide ## 1. Connect your Wallet @@ -64,25 +60,24 @@ Important concepts in the AtomicAssets standard (using alphakittens on EOS as an Click the "Create Collection" box. The "New Collection" page will open. Upload an image into the "Add Collection Image" box to the left. Fill out the other required text fields: -1. **Collection Name:** a *permanent* and *unique* on-chain name. (e.g. `alphakittens`) - 1. It must be exactly 12 characters (following the same rules as on-chain account names), unless you are specifically creating a collection from a premium account or a sub-account. - 2. Collection names are unique, so you cannot use any name which already belongs to a collection; and once you register your collection, nobody else will be able to use your collection name. - 3. This is generally the name which will be used in URLs for your collection (e.g. `https://creator.facings.io/jungle4/collection/`**`alphakittens`**) +1. **Collection Name:** a _permanent_ and _unique_ on-chain name. (e.g. `alphakittens`) + 1. It must be exactly 12 characters (following the same rules as on-chain account names), unless you are specifically creating a collection from a premium account or a sub-account. + 2. Collection names are unique, so you cannot use any name which already belongs to a collection; and once you register your collection, nobody else will be able to use your collection name. + 3. This is generally the name which will be used in URLs for your collection (e.g. `https://creator.facings.io/jungle4/collection/`**`alphakittens`**) 2. **Display Name:** e.g. `Alpha Kittens`. This can be edited in the future. 3. **Website:** e.g. `https://alphakittens.example.com`. This can be edited in the future. 4. **Market fee:** a number between `0` and `15`. This can be edited in the future. - 1. This is the percent which will be charged as a fee (and paid to you) when your NFTs are bought and sold. - 2. Keep in mind that there are additional fees paid to chain resources and - marketplaces than cannot be changed. Market data suggests a 6% fee is the - most commonly accepted collection fee. + 1. This is the percent which will be charged as a fee (and paid to you) when your NFTs are bought and sold. + 2. Keep in mind that there are additional fees paid to chain resources and + marketplaces than cannot be changed. Market data suggests a 6% fee is the + most commonly accepted collection fee. 5. **Description:** a short blurb describing the project, to be shown publicly on collection explorers. This can be edited in the future. 6. **Social Media (optional):** insert none, some or all of the social media you want associated with the collection. This can be edited in the future. 7. **Company Details (optional):** insert none, some or all of the social media you want associated with the collection. This can be edited in the future. - Once you submit the form, you will be asked to sign the transactions. If you receive an error the most common issues are lack of chain resources and duplicative name choice. -Congratulations, your *Collection* is registered on-chain! +Congratulations, your _Collection_ is registered on-chain! ## 3. Create Schema @@ -92,20 +87,21 @@ Now that you have registered a Collection, the next step is to define the schema ### Naming the Schema -1. The *Name* must consist of letters `a-z`, digits `1-5`, and/or period (`.`). +1. The _Name_ must consist of letters `a-z`, digits `1-5`, and/or period (`.`). It must be 12 characters or less. 2. Consider using a descriptive name such as `kittens` or `cards`. ### Defining Attributes 1. The default attributes are: - 1. `name` (`Text`) - 2. `img` (`Image`) - 3. `video` (`Text`) + 1. `name` (`Text`) + 2. `img` (`Image`) + 3. `video` (`Text`) 2. `name` is required but `img` or `video` can be removed by clicking on the trash can on the far right of the line. 3. Click the "Add Attribute" button to add any more desired attributes as needed. #### Schema: Basic Data Types + 1. Text - any text, including alphanumeric values and emojis 2. Integer Number - any whole number 3. Floating Point Number - any whole or decimal number @@ -127,15 +123,15 @@ Now that you have created a schema, you can use it to create a template. **"NFTs can be transferred"**: leave this On if you wish to allow users to trade their NFTs (recommended). If turned off the NFT will forever live in the account it is created into when you mint it. **"NFTs can be burned"**: leave this On if you wish to allow users to burn their NFTs (recommended). Turning this off will not allow the NFT to be destroyed at a later date. -**"Max Supply"**: this will determine the maximum number of NFTs which can be minted from this template. Setting a *Max Supply* of 0 means the template is unbounded (unlimited potential mints). The template can be locked later to prevent additional minting if needed. +**"Max Supply"**: this will determine the maximum number of NFTs which can be minted from this template. Setting a _Max Supply_ of 0 means the template is unbounded (unlimited potential mints). The template can be locked later to prevent additional minting if needed. #### Immutable Attributes Here you will see all the attributes you defined in the schema. -Templates contain solely *Immutable Attributes*. Any values you set here will be permanently on the NFTs you mint from this template. If you need your NFTs to have any attributes which can change later, leave them blank here. +Templates contain solely _Immutable Attributes_. Any values you set here will be permanently on the NFTs you mint from this template. If you need your NFTs to have any attributes which can change later, leave them blank here. -When you fill out an attribute (such as "*name*"), you will see the toggle switch to "Immutable" - this means the attribute will not be editable on any of the NFTs you mint from this template. Again, if you want to be able to edit an attribute such as "*name*" on individual NFTs, leave this blank and ensure the toggle switch reads "Mutable". +When you fill out an attribute (such as "_name_"), you will see the toggle switch to "Immutable" - this means the attribute will not be editable on any of the NFTs you mint from this template. Again, if you want to be able to edit an attribute such as "_name_" on individual NFTs, leave this blank and ensure the toggle switch reads "Mutable". Once you're done, click "Create Template" to sign the transaction. @@ -145,7 +141,7 @@ Once you're done, click "Create Template" to sign the transaction. 2. Ensure that the correct Schema and Template are selected. 3. **Recipients:** By default, one NFT will be minted to the account you are logged in as. You may add any number of recipients and set any number of copies to mint to each. 4. **Immutable Attributes**: These are the attributes which were set on the template. These cannot be changed, and they will be part of the NFT you mint. -5. **Mutable Attributes**: Any attributes you added to the schema but *not* the template are considered mutable. You can set those values here or leave them blank to be adjusted later. +5. **Mutable Attributes**: Any attributes you added to the schema but _not_ the template are considered mutable. You can set those values here or leave them blank to be adjusted later. 6. Click "Create NFT" to sign the transactions. 7. Your selected recipients will now have your minted NFTs! diff --git a/next.config.js b/next.config.js index b69c6f4..575f769 100644 --- a/next.config.js +++ b/next.config.js @@ -7,13 +7,13 @@ const nextConfig = { distDir: '.next', images: { domains: [ - 'wax.bloks.io', - 'bloks.io', - 'facings.mypinata.cloud', 'robohash.org', - 'ipfs.ledgerwise.io', + 'ipfs-gateway.soon.market', + 'media.soon.market', + 'soon.market', ], }, + output: 'standalone', }; module.exports = nextConfig; diff --git a/package.json b/package.json index 3fc5901..f06aecc 100644 --- a/package.json +++ b/package.json @@ -10,41 +10,40 @@ "prettier": "prettier --write ./" }, "dependencies": { - "@eosdacio/ual-wax": "^1.9.0", - "@headlessui/react": "^1.7.4", - "@headlessui/tailwindcss": "^0.1.1", - "@hookform/resolvers": "^2.9.10", - "@types/react-beautiful-dnd": "^13.1.3", - "@waxio/waxjs": "^1.1.0", - "axios": "^1.1.3", - "eosjs": "^22.1.0", - "framer-motion": "^9.0.3", - "is-ipfs": "^7.0.3", + "@headlessui/react": "1.7.4", + "@headlessui/tailwindcss": "0.1.1", + "@hookform/resolvers": "2.9.10", + "@phosphor-icons/react": "2.0.10", + "@proton/ual-webauth": "0.1.19", + "@types/react-beautiful-dnd": "13.1.4", + "axios": "1.1.3", + "eosjs": "22.1.0", + "framer-motion": "9.0.3", + "is-ipfs": "7.0.3", "next": "13.0.3", - "papaparse": "^5.4.1", - "phosphor-react": "^1.4.1", + "papaparse": "5.4.1", "react": "18.2.0", - "react-beautiful-dnd": "^13.1.1", + "react-beautiful-dnd": "13.1.1", "react-dom": "18.2.0", - "react-hook-form": "^7.39.4", - "react-swipeable": "^7.0.0", - "seed-random": "^2.2.0", - "sharp": "^0.31.3", - "ual-anchor": "^1.3.0", - "ual-reactjs-renderer": "^0.3.1", - "yup": "^0.32.11" + "react-hook-form": "7.39.4", + "react-swipeable": "7.0.0", + "seed-random": "2.2.0", + "sharp": "0.32.5", + "ual-anchor": "1.3.0", + "ual-reactjs-renderer": "0.3.1", + "yup": "0.32.11" }, "devDependencies": { "@types/node": "18.11.18", "@types/react": "18.0.26", - "autoprefixer": "^10.4.13", + "autoprefixer": "10.4.13", "eslint": "8.27.0", "eslint-config-next": "13.0.3", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.2.1", - "postcss": "^8.4.19", - "prettier": "^2.7.1", - "tailwindcss": "^3.2.4", + "eslint-config-prettier": "8.5.0", + "eslint-plugin-prettier": "4.2.1", + "postcss": "8.4.19", + "prettier": "2.7.1", + "tailwindcss": "3.2.4", "typescript": "4.9.4" } } diff --git a/public/xprnetwork.png b/public/xprnetwork.png new file mode 100644 index 0000000000000000000000000000000000000000..e2b82eccccc2b3f7a63aa3830b0d55c24e4a58c3 GIT binary patch literal 36145 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 literal 0 HcmV?d00001 diff --git a/src/assets/styles/globals.css b/src/assets/styles/globals.css index 2270cb0..7f91412 100644 --- a/src/assets/styles/globals.css +++ b/src/assets/styles/globals.css @@ -51,10 +51,10 @@ @apply border-transparent; } .btn-small.btn-square { - @apply px-[calc(0.5rem-1px)] + @apply px-[calc(0.5rem-1px)]; } .btn-square { - @apply px-[calc(1rem-1px)] + @apply px-[calc(1rem-1px)]; } .badge-small { @@ -78,8 +78,8 @@ @apply relative; } .tooltip .tooltip-text { - @apply hidden absolute p-4 -top-16 right-0 bg-neutral-700 whitespace-nowrap rounded - } + @apply hidden absolute p-4 -top-16 right-0 bg-neutral-700 whitespace-nowrap rounded; + } .tooltip:hover .tooltip-text { @apply block; } diff --git a/src/components/BaseField.tsx b/src/components/BaseField.tsx index 87817d2..803db42 100644 --- a/src/components/BaseField.tsx +++ b/src/components/BaseField.tsx @@ -1,5 +1,5 @@ import { ReactNode } from 'react'; -import { WarningCircle } from 'phosphor-react'; +import { WarningCircle } from '@phosphor-icons/react'; interface BaseFieldProps { icon?: ReactNode; diff --git a/src/components/Card/components/CardContent.tsx b/src/components/Card/components/CardContent.tsx index 832df0b..16c1208 100644 --- a/src/components/Card/components/CardContent.tsx +++ b/src/components/Card/components/CardContent.tsx @@ -1,5 +1,5 @@ import Image from 'next/image'; -import { ImageSquare } from 'phosphor-react'; +import { ImageSquare } from '@phosphor-icons/react'; import Link from 'next/link'; interface CardContentProps { @@ -8,6 +8,7 @@ interface CardContentProps { video: string; title: string; subtitle: string; + saleInfo?: any; viewLink?: string; withThumbnail: boolean; } @@ -18,12 +19,13 @@ export function CardContent({ video, title, subtitle, + saleInfo, viewLink, withThumbnail, }: CardContentProps) { return ( <> - {id && ( + {(id && saleInfo) && (

#{id}

@@ -64,6 +66,9 @@ export function CardContent({ {subtitle && (

{subtitle}

)} + {saleInfo && ( +

{`${(Number(saleInfo.listingPrice) / Math.pow(10, saleInfo.tokenPrecision))} ${saleInfo.token}`}

+ )} {viewLink && ( event.stopPropagation()} > - View NFT + View {saleInfo ? 'Sale' : 'NFT'} )} diff --git a/src/components/Card/index.tsx b/src/components/Card/index.tsx index de0982f..9a67eb1 100644 --- a/src/components/Card/index.tsx +++ b/src/components/Card/index.tsx @@ -4,22 +4,26 @@ import { HTMLAttributes } from 'react'; interface CardProps extends HTMLAttributes { href?: string; + target?: string; id?: string; image?: string; video?: string; title?: string; subtitle?: string; + saleInfo?: any; viewLink?: string; withThumbnail?: boolean; } export function Card({ href, + target, id, image, video, title, subtitle, + saleInfo, viewLink, withThumbnail = true, ...props @@ -32,6 +36,7 @@ export function Card({ className={`bg-neutral-800 rounded-xl overflow-hidden cursor-pointer hover:scale-105 duration-300 ${ !id && 'flex flex-col justify-end' }`} + target={target} > diff --git a/src/components/Carousel.tsx b/src/components/Carousel.tsx index e118071..8532073 100644 --- a/src/components/Carousel.tsx +++ b/src/components/Carousel.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useRef } from 'react'; import Image from 'next/image'; import { AnimatePresence, motion, MotionConfig } from 'framer-motion'; -import { ImageSquare } from 'phosphor-react'; +import { ImageSquare } from '@phosphor-icons/react'; import { ipfsEndpoint } from '@configs/globalsConfig'; import { CarouselPreview } from './CarouselPreview'; diff --git a/src/components/CarouselPreview.tsx b/src/components/CarouselPreview.tsx index b41825c..04470fb 100644 --- a/src/components/CarouselPreview.tsx +++ b/src/components/CarouselPreview.tsx @@ -6,7 +6,7 @@ import { Ref, } from 'react'; import Image from 'next/image'; -import { X, CaretLeft, CaretRight } from 'phosphor-react'; +import { X, CaretLeft, CaretRight } from '@phosphor-icons/react'; import { useSwipeable } from 'react-swipeable'; import { Dialog, Transition } from '@headlessui/react'; import { AnimatePresence, motion, MotionConfig } from 'framer-motion'; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 2ea9d73..437e5b9 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -3,7 +3,7 @@ import { DiscordLogo, GlobeSimple, GithubLogo, -} from 'phosphor-react'; +} from '@phosphor-icons/react'; export function Footer() { return ( @@ -11,69 +11,51 @@ export function Footer() {
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index d3cccce..ccb056b 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,5 +1,5 @@ import Link from 'next/link'; -import { CaretRight } from 'phosphor-react'; +import { CaretRight } from '@phosphor-icons/react'; import { ReactNode } from 'react'; import { Carousel } from '@components/Carousel'; diff --git a/src/components/InputPreview.tsx b/src/components/InputPreview.tsx index 577e6b4..21b885b 100644 --- a/src/components/InputPreview.tsx +++ b/src/components/InputPreview.tsx @@ -7,7 +7,13 @@ import { InputHTMLAttributes, } from 'react'; import Image from 'next/image'; -import { UploadSimple, File, FileCsv, FilePdf, FileX } from 'phosphor-react'; +import { + UploadSimple, + File, + FileCsv, + FilePdf, + FileX, +} from '@phosphor-icons/react'; import { ipfsEndpoint } from '@configs/globalsConfig'; import { convertToBase64 } from '@utils/convertToBase64'; diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx index 9b68a0f..488aac8 100644 --- a/src/components/Loading.tsx +++ b/src/components/Loading.tsx @@ -1,4 +1,4 @@ -import { CircleNotch } from 'phosphor-react'; +import { CircleNotch } from '@phosphor-icons/react'; export function Loading() { return ( diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 051087f..55b6c35 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -7,7 +7,7 @@ import { Ref, } from 'react'; import { Dialog, Transition } from '@headlessui/react'; -import { X } from 'phosphor-react'; +import { X } from '@phosphor-icons/react'; interface ModalProps { title: string; diff --git a/src/components/SeeMoreButton.tsx b/src/components/SeeMoreButton.tsx index 7df9e8a..c7069e3 100644 --- a/src/components/SeeMoreButton.tsx +++ b/src/components/SeeMoreButton.tsx @@ -1,4 +1,4 @@ -import { CircleNotch } from 'phosphor-react'; +import { CircleNotch } from '@phosphor-icons/react'; interface SeeMoreButtonProps { isLoading: boolean; diff --git a/src/components/Select.tsx b/src/components/Select.tsx index 868ec64..efdb699 100644 --- a/src/components/Select.tsx +++ b/src/components/Select.tsx @@ -1,7 +1,7 @@ import { useState, Fragment, ReactNode } from 'react'; import { BaseField } from '@components/BaseField'; import { Listbox, Transition } from '@headlessui/react'; -import { CaretDown } from 'phosphor-react'; +import { CaretDown } from '@phosphor-icons/react'; interface SelectProps { icon?: ReactNode; diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx index 1a66cfe..02ba34e 100644 --- a/src/components/Switch.tsx +++ b/src/components/Switch.tsx @@ -1,4 +1,4 @@ -import { Check, X, Minus } from 'phosphor-react'; +import { Check, X, Minus } from '@phosphor-icons/react'; import { Switch as SwitchComponent, Transition } from '@headlessui/react'; interface SwitchProps { diff --git a/src/components/Table.tsx b/src/components/Table.tsx index dbe49cf..7c96842 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -1,4 +1,4 @@ -import { TrashSimple } from 'phosphor-react'; +import { TrashSimple } from '@phosphor-icons/react'; interface TableProps { list: string[]; diff --git a/src/components/PluginsContainer.tsx b/src/components/ToolsContainer.tsx similarity index 62% rename from src/components/PluginsContainer.tsx rename to src/components/ToolsContainer.tsx index bc3b8e4..43f2c71 100644 --- a/src/components/PluginsContainer.tsx +++ b/src/components/ToolsContainer.tsx @@ -1,12 +1,17 @@ import Link from 'next/link'; -import { Parachute, PuzzlePiece } from 'phosphor-react'; +import { Fire, Parachute, Prohibit, PuzzlePiece, ShareFat } from '@phosphor-icons/react'; -export function PluginsContainer({ plugins, chainKey }) { - const handleIcons = (plugin) => { - switch (plugin) { +export function ToolsContainer({ tools, chainKey }) { + const handleIcons = (tool) => { + switch (tool) { case 'airdrop': return ; - + case 'burn': + return ; + case 'cancel-sales': + return ; + case 'transfer': + return ; default: return ; } @@ -14,19 +19,19 @@ export function PluginsContainer({ plugins, chainKey }) { return ( <> - {plugins.map((item) => ( -
- {item.page === 'plugins' && ( + {tools.map((item) => ( +
+ {item.page === 'tools' && (
- {handleIcons(item.plugin)} + {handleIcons(item.tool)}
{item.label} diff --git a/src/components/TopAppBar/components/Chain.tsx b/src/components/TopAppBar/components/Chain.tsx index 71d0eed..823d092 100644 --- a/src/components/TopAppBar/components/Chain.tsx +++ b/src/components/TopAppBar/components/Chain.tsx @@ -2,9 +2,9 @@ import Image from 'next/image'; import Link from 'next/link'; import { withUAL } from 'ual-reactjs-renderer'; import { Menu } from '@headlessui/react'; -import { CaretDown } from 'phosphor-react'; +import { CaretDown } from '@phosphor-icons/react'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { ChainLink } from './ChainLink'; diff --git a/src/components/TopAppBar/components/Login.tsx b/src/components/TopAppBar/components/Login.tsx index 5de4ea5..270d3b8 100644 --- a/src/components/TopAppBar/components/Login.tsx +++ b/src/components/TopAppBar/components/Login.tsx @@ -2,12 +2,12 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import { Menu } from '@headlessui/react'; import { withUAL } from 'ual-reactjs-renderer'; -import { CaretDown } from 'phosphor-react'; +import { CaretDown } from '@phosphor-icons/react'; import { getAccount } from '@services/account/getAccount'; import { getChainKeyByChainId } from '@utils/getChainKeyByChainId'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; interface LoginComponentProps { chainKey: string; diff --git a/src/components/TopAppBar/index.tsx b/src/components/TopAppBar/index.tsx index 925f533..6f00e70 100644 --- a/src/components/TopAppBar/index.tsx +++ b/src/components/TopAppBar/index.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/router'; import { Chain } from './components/Chain'; import { NavItem } from './components/NavItem'; import { Login } from './components/Login'; -import { List, X } from 'phosphor-react'; +import { List, X } from '@phosphor-icons/react'; import { chainKeyDefault } from '@configs/globalsConfig'; @@ -25,27 +25,29 @@ export function TopAppBar() { setOpen(false)}> My Collections + setOpen(false)}> + Tools + setOpen(false)} + target="_blank" > - Explorer + Explore setOpen(false)} + target="_blank" > - Transfer - - setOpen(false)}> - Plugins + Sell setOpen(false)} target="_blank" > - Docs + Promote setOpen(false)}> About diff --git a/src/components/WarningCard.tsx b/src/components/WarningCard.tsx index a28fcdd..949bb1f 100644 --- a/src/components/WarningCard.tsx +++ b/src/components/WarningCard.tsx @@ -1,4 +1,4 @@ -import { WarningCircle } from 'phosphor-react'; +import { WarningCircle } from '@phosphor-icons/react'; interface WarningCardProps { title: string; diff --git a/src/components/collection/CollectionAccountsList.tsx b/src/components/collection/CollectionAccountsList.tsx index e65e24b..89084c6 100644 --- a/src/components/collection/CollectionAccountsList.tsx +++ b/src/components/collection/CollectionAccountsList.tsx @@ -59,6 +59,10 @@ export function CollectionAccountsList({ {accounts.map((account) => ( { + if(chainKey == "xpr") { + return `https://soon.market/nft/templates/${asset.template.template_id}/${asset.asset_id}?utm_medium=nfts&utm_source=nft-manager`; + } + return `/${chainKey}/collection/${collectionName}/template/${asset.template.template_id}`; + } + async function handleSeeMoreAssets() { setIsLoading(true); @@ -103,9 +110,13 @@ export function CollectionAssetsList({ ; - break; case 'facebook': return ; - break; case 'medium': return ; - break; case 'github': return ; - break; case 'discord': return ; - break; case 'youtube': return ; - break; case 'telegram': return ; - break; - default: break; } diff --git a/src/components/collection/CollectionTemplatesList.tsx b/src/components/collection/CollectionTemplatesList.tsx index e1254ee..0f38add 100644 --- a/src/components/collection/CollectionTemplatesList.tsx +++ b/src/components/collection/CollectionTemplatesList.tsx @@ -36,6 +36,13 @@ export function CollectionTemplatesList({ const offset = (currentPage - 1) * limit; const isEndOfList = Number(totalTemplates) === templates.length; + const getTemplateLink = (templateId) => { + if(chainKey == "xpr") { + return `https://soon.market/nft/templates/${templateId}?utm_medium=template&utm_source=nft-manager`; + } + return `/${chainKey}/collection/${collectionName}/template/${templateId}`; + } + async function handleSeeMoreTemplates() { setIsLoading(true); @@ -74,9 +81,13 @@ export function CollectionTemplatesList({ ([]); - const [externalPluginsList, setExternalPluginsList] = useState< - PluginsListProps[] - >([]); +}: CollectionToolsProps) { + const [toolsList, setToolsList] = useState([]); + const [externalToolsList, setExternalToolsList] = useState( + [] + ); useEffect(() => { - async function fetchPlugins() { + async function fetchTools() { try { - const response = await fetch('/api/plugins?path=default'); - const pluginNames = await response.json(); - setPluginsList(pluginNames); + const response = await fetch('/api/tools?path=default'); + const toolNames = await response.json(); + setToolsList(toolNames); - const res = await fetch('/api/plugins?path=external'); - const externalPluginNames = await res.json(); - setExternalPluginsList(externalPluginNames); + const res = await fetch('/api/tools?path=external'); + const externalToolNames = await res.json(); + setExternalToolsList(externalToolNames); } catch (error) { console.log(error); } } - fetchPlugins(); + fetchTools(); }, []); return ( @@ -44,16 +44,16 @@ export function CollectionPlugins({

{collectionTabs[5].name}

- {(pluginsList.length > 0 || externalPluginsList.length > 0) && ( + {(toolsList.length > 0 || externalToolsList.length > 0) && (
- {pluginsList.map((item) => { + {toolsList.map((item) => { if (item.page === 'collection') { return ( { + {externalToolsList.map((item) => { if (item.page === 'collection') { return (
-

Explore and manage NFT Collections

+

NFT Manager

- Connect your wallet to see and manage your collections + Connect your wallet to get access to the collection manager and other + useful tools!

- - Explorer - + <> + {'xpr' == chainKey ? ( + + Explore on Soon.Market + + ) : ( + + Explorer + + )} +
diff --git a/src/components/schema/Attributes.tsx b/src/components/schema/Attributes.tsx index 14a9287..41096b2 100644 --- a/src/components/schema/Attributes.tsx +++ b/src/components/schema/Attributes.tsx @@ -1,4 +1,4 @@ -import { TrashSimple, HandGrabbing } from 'phosphor-react'; +import { TrashSimple, HandGrabbing } from '@phosphor-icons/react'; import { useFormContext, useFieldArray, Controller } from 'react-hook-form'; import * as yup from 'yup'; import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; diff --git a/src/configs/chainsConfig.ts b/src/configs/chainsConfig.ts index 7939e93..92073d9 100644 --- a/src/configs/chainsConfig.ts +++ b/src/configs/chainsConfig.ts @@ -1,48 +1,25 @@ +import { WebAuth } from '@proton/ual-webauth'; import { Anchor } from 'ual-anchor'; -import { Wax } from '@eosdacio/ual-wax'; -module.exports = { - eos: { - name: 'EOS', - imageUrl: 'https://bloks.io/img/chains/eos.png', - authenticators: [Anchor], - aaEndpoint: process.env.NEXT_PUBLIC_EOS_MAINNET_AA_ENDPOINT, - chainId: process.env.NEXT_PUBLIC_EOS_MAINNET_CHAIN_ID, - protocol: process.env.NEXT_PUBLIC_EOS_MAINNET_PROTOCOL, - host: process.env.NEXT_PUBLIC_EOS_MAINNET_HOST, - port: process.env.NEXT_PUBLIC_EOS_MAINNET_PORT, +export default { + xpr: { + name: 'XPR Network', + imageUrl: '/xprnetwork.png', + authenticators: [WebAuth, Anchor], + aaEndpoint: process.env.NEXT_PUBLIC_XPR_NETWORK_MAINNET_AA_ENDPOINT, + chainId: process.env.NEXT_PUBLIC_XPR_NETWORK_MAINNET_CHAIN_ID, + protocol: process.env.NEXT_PUBLIC_XPR_NETWORK_MAINNET_PROTOCOL, + host: process.env.NEXT_PUBLIC_XPR_NETWORK_MAINNET_HOST, + port: process.env.NEXT_PUBLIC_XPR_NETWORK_MAINNET_PORT, }, - - wax: { - name: 'WAX', - imageUrl: 'https://wax.bloks.io/img/chains/wax.png', - authenticators: [Anchor, Wax], - aaEndpoint: process.env.NEXT_PUBLIC_WAX_MAINNET_AA_ENDPOINT, - chainId: process.env.NEXT_PUBLIC_WAX_MAINNET_CHAIN_ID, - protocol: process.env.NEXT_PUBLIC_WAX_MAINNET_PROTOCOL, - host: process.env.NEXT_PUBLIC_WAX_MAINNET_HOST, - port: process.env.NEXT_PUBLIC_WAX_MAINNET_PORT, - }, - - 'wax-test': { - name: 'WAX (Testnet)', - imageUrl: 'https://wax.bloks.io/img/chains/wax.png', - authenticators: [Anchor], - aaEndpoint: process.env.NEXT_PUBLIC_WAX_TESTNET_AA_ENDPOINT, - chainId: process.env.NEXT_PUBLIC_WAX_TESTNET_CHAIN_ID, - protocol: process.env.NEXT_PUBLIC_WAX_TESTNET_PROTOCOL, - host: process.env.NEXT_PUBLIC_WAX_TESTNET_HOST, - port: process.env.NEXT_PUBLIC_WAX_TESTNET_PORT, - }, - - jungle4: { - name: 'Jungle4 (EOS Testnet)', - imageUrl: 'https://bloks.io/img/chains/jungle.png', - authenticators: [Anchor], - aaEndpoint: process.env.NEXT_PUBLIC_EOS_JUNGLE4_AA_ENDPOINT, - chainId: process.env.NEXT_PUBLIC_EOS_JUNGLE4_CHAIN_ID, - protocol: process.env.NEXT_PUBLIC_EOS_JUNGLE4_PROTOCOL, - host: process.env.NEXT_PUBLIC_EOS_JUNGLE4_HOST, - port: process.env.NEXT_PUBLIC_EOS_JUNGLE4_PORT, + 'xpr-test': { + name: 'XPR Network (Testnet)', + imageUrl: '/xprnetwork.png', + authenticators: [WebAuth, Anchor], + aaEndpoint: process.env.NEXT_PUBLIC_XPR_NETWORK_TESTNET_AA_ENDPOINT, + chainId: process.env.NEXT_PUBLIC_XPR_NETWORK_TESTNET_CHAIN_ID, + protocol: process.env.NEXT_PUBLIC_XPR_NETWORK_TESTNET_PROTOCOL, + host: process.env.NEXT_PUBLIC_XPR_NETWORK_TESTNET_HOST, + port: process.env.NEXT_PUBLIC_XPR_NETWORK_TESTNET_PORT, }, }; diff --git a/src/configs/globalsConfig.ts b/src/configs/globalsConfig.ts index 15d1965..297d51c 100644 --- a/src/configs/globalsConfig.ts +++ b/src/configs/globalsConfig.ts @@ -6,3 +6,4 @@ export const appDescription = process.env.NEXT_PUBLIC_APP_DESCRIPTION; export const chainKeyDefault = process.env.NEXT_PUBLIC_CHAIN_KEY_DEFAULT; export const metaIcon = process.env.NEXT_PUBLIC_META_ICON; export const appUrl = process.env.NEXT_PUBLIC_APP_URL; +export const requestAccount = process.env.NEXT_PUBLIC_REQUEST_ACCOUNT; diff --git a/src/libs/authenticators.ts b/src/libs/authenticators.ts index ac17007..67cac28 100644 --- a/src/libs/authenticators.ts +++ b/src/libs/authenticators.ts @@ -1,5 +1,5 @@ -import { appName } from '@configs/globalsConfig'; -import * as chainsConfig from '@configs/chainsConfig'; +import { appName, requestAccount } from '@configs/globalsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { blockchains } from '@utils/blockchains'; export const authenticators = Object.keys(chainsConfig).reduce( @@ -16,6 +16,13 @@ export const authenticators = Object.keys(chainsConfig).reduce( new Authenticator([blockchain], { appName, disableGreymassFuel: true, + transportOptions: { + requestAccount, + }, + selectorOptions: { + appName, + dialogRootNode: '#__next', + }, }) ), }; diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 0893802..bc850db 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,5 +1,5 @@ import Head from 'next/head'; -import { FlyingSaucer } from 'phosphor-react'; +import { FlyingSaucer } from '@phosphor-icons/react'; export default function PageNotFound() { return ( diff --git a/src/pages/[chainKey]/about.tsx b/src/pages/[chainKey]/about.tsx index 06bdb14..679877f 100644 --- a/src/pages/[chainKey]/about.tsx +++ b/src/pages/[chainKey]/about.tsx @@ -17,22 +17,47 @@ export default function About() {

Purpose

- Collection Manager is a reference UI implementation showcasing - AtomicAssets functionality, built by FACINGS and funded by the EOS - Network Foundation. -

-

- It's meant to work as both a stand-alone tool for collection - owners, as well as a starting point for NFT developers making - their own apps on Antelope. Collection Manager can be forked and - customized to suit the needs of your own project. + NFT Manager is a fork of the reference UI implementation that has + originally been built by FACINGS and funded by the EOS Network + Foundation. +
+
+ + Soon.Market + {' '} + is the leading NFT marketplace on XPR Network and currently does not allow to manage NFT collections. + Therefore, this application was forked in order to provide creators + the possibility to easily create and manage their NFT collections. +
+
+ Creators and regular users can also benefit from the NFT Manager + by making use of other provided tools: +

    +
  1. Airdrop NFTs
  2. +
  3. Burn NFTs
  4. +
  5. Cancel Sales
  6. +
  7. Transfer NFTs
  8. +

Source Code

- Github:{' '} + https://github.com/kryptokrauts/nft-manager + {' '} + forked from{' '} +

- -

Design principles

-
    -
  1. - Keep the core simple and secure with minimal dependencies -
  2. -
  3. - Help publishers, developers, and businesses ship faster -
  4. -
  5. - Grow open-source community around core EOS/AtomicAssets needs -
  6. -
- -

Core features

-
    -
  1. Login and view resource usage
  2. -
  3. - View/explore collections (schemas, templates, and NFTs) -
  4. -
  5. Create and edit collections
  6. -

About AtomicAssets

- AtomicAssets is a Non-Fungible Token (NFT) standard for Antelope + + AtomicAssets + {' '} + is a Non-Fungible Token (NFT) standard for{' '} + + Antelope + {' '} blockchains. AtomicAsset NFTs are resource-efficient yet full-featured. All metadata is stored on-chain, and templates are used to efficiently store redundant data. Additional features @@ -77,25 +94,30 @@ export default function About() {

-

About FACINGS

+

About kryptokrauts

- FACINGS aims to unlock the value of web3 for the masses by making - distribution of engaging NFTs easy, affordable, and scalable. We - serve those who aspire to launch NFTs, saving them time and money - by providing flexible tools to model and publish high-quality, - feature-rich NFT collections. Using FACINGS, NFT publishers can - take their concept to market quickly and reach their audience, - wherever they are. + We have been around in the crypto community since 2017 and we are + winners of several hackathons. +
+
+ Currently, we are focusing on building{' '} + + Soon.Market + {' '} + - the leading NFT marketplace on XPR Network!

- Social links:{' '} - https://linktr.ee/FACINGSOfficial + https://kryptokrauts.com

diff --git a/src/pages/[chainKey]/collection/[collectionName].tsx b/src/pages/[chainKey]/collection/[collectionName].tsx index dec7cc3..4e5b382 100644 --- a/src/pages/[chainKey]/collection/[collectionName].tsx +++ b/src/pages/[chainKey]/collection/[collectionName].tsx @@ -35,7 +35,7 @@ import { CollectionTemplatesList } from '@components/collection/CollectionTempla import { CollectionAccountsList } from '@components/collection/CollectionAccountsList'; import { CollectionSchemasList } from '@components/collection/CollectionSchemasList'; import { CollectionAssetsList } from '@components/collection/CollectionAssetsList'; -import { CollectionPlugins } from '@components/collection/CollectionPlugins'; +import { CollectionTools } from '@components/collection/CollectionTools'; import { CollectionStats } from '@components/collection/CollectionStats'; import { CollectionHints } from '@components/collection/CollectionHints'; @@ -221,7 +221,7 @@ function Collection({ /> - diff --git a/src/pages/[chainKey]/collection/[collectionName]/asset/[assetId]/edit.tsx b/src/pages/[chainKey]/collection/[collectionName]/asset/[assetId]/edit.tsx index 8db1d40..dbc00d5 100644 --- a/src/pages/[chainKey]/collection/[collectionName]/asset/[assetId]/edit.tsx +++ b/src/pages/[chainKey]/collection/[collectionName]/asset/[assetId]/edit.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react'; import { useRouter } from 'next/router'; import { withUAL } from 'ual-reactjs-renderer'; import { Tab, Disclosure } from '@headlessui/react'; -import { CircleNotch } from 'phosphor-react'; +import { CircleNotch } from '@phosphor-icons/react'; import Head from 'next/head'; import { useForm, Controller } from 'react-hook-form'; diff --git a/src/pages/[chainKey]/collection/[collectionName]/asset/new.tsx b/src/pages/[chainKey]/collection/[collectionName]/asset/new.tsx index 3295b9a..5a27cb0 100644 --- a/src/pages/[chainKey]/collection/[collectionName]/asset/new.tsx +++ b/src/pages/[chainKey]/collection/[collectionName]/asset/new.tsx @@ -4,7 +4,12 @@ import { useRouter } from 'next/router'; import { withUAL } from 'ual-reactjs-renderer'; import { Disclosure } from '@headlessui/react'; import { Listbox, Transition } from '@headlessui/react'; -import { CaretDown, Check, CircleNotch, TrashSimple } from 'phosphor-react'; +import { + CaretDown, + Check, + CircleNotch, + TrashSimple, +} from '@phosphor-icons/react'; import { GetServerSideProps } from 'next'; import { useForm, Controller, useFieldArray } from 'react-hook-form'; diff --git a/src/pages/[chainKey]/collection/[collectionName]/edit.tsx b/src/pages/[chainKey]/collection/[collectionName]/edit.tsx index b2ec727..495fa08 100644 --- a/src/pages/[chainKey]/collection/[collectionName]/edit.tsx +++ b/src/pages/[chainKey]/collection/[collectionName]/edit.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, FormEvent, useRef } from 'react'; -import { UploadSimple, CircleNotch } from 'phosphor-react'; +import { UploadSimple, CircleNotch } from '@phosphor-icons/react'; import { withUAL } from 'ual-reactjs-renderer'; import { GetServerSideProps } from 'next'; import { Tab, Disclosure } from '@headlessui/react'; diff --git a/src/pages/[chainKey]/collection/[collectionName]/schema/[schemaName]/edit.tsx b/src/pages/[chainKey]/collection/[collectionName]/schema/[schemaName]/edit.tsx index cd3d9ba..b6aafab 100644 --- a/src/pages/[chainKey]/collection/[collectionName]/schema/[schemaName]/edit.tsx +++ b/src/pages/[chainKey]/collection/[collectionName]/schema/[schemaName]/edit.tsx @@ -1,6 +1,6 @@ import { useState, useRef } from 'react'; import { withUAL } from 'ual-reactjs-renderer'; -import { CircleNotch } from 'phosphor-react'; +import { CircleNotch } from '@phosphor-icons/react'; import { useRouter } from 'next/router'; import { Disclosure } from '@headlessui/react'; import Head from 'next/head'; diff --git a/src/pages/[chainKey]/collection/[collectionName]/schema/new.tsx b/src/pages/[chainKey]/collection/[collectionName]/schema/new.tsx index 8f9fdc5..8187851 100644 --- a/src/pages/[chainKey]/collection/[collectionName]/schema/new.tsx +++ b/src/pages/[chainKey]/collection/[collectionName]/schema/new.tsx @@ -1,6 +1,6 @@ import { useState, useRef } from 'react'; import { withUAL } from 'ual-reactjs-renderer'; -import { CircleNotch } from 'phosphor-react'; +import { CircleNotch } from '@phosphor-icons/react'; import { useRouter } from 'next/router'; import { Disclosure } from '@headlessui/react'; import Head from 'next/head'; diff --git a/src/pages/[chainKey]/collection/[collectionName]/template/[templateId]/edit.tsx b/src/pages/[chainKey]/collection/[collectionName]/template/[templateId]/edit.tsx index 31f3906..4471635 100644 --- a/src/pages/[chainKey]/collection/[collectionName]/template/[templateId]/edit.tsx +++ b/src/pages/[chainKey]/collection/[collectionName]/template/[templateId]/edit.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/router'; import { GetServerSideProps } from 'next'; import { withUAL } from 'ual-reactjs-renderer'; import { Disclosure } from '@headlessui/react'; -import { CircleNotch } from 'phosphor-react'; +import { CircleNotch } from '@phosphor-icons/react'; import { getTemplateService, diff --git a/src/pages/[chainKey]/collection/[collectionName]/template/new.tsx b/src/pages/[chainKey]/collection/[collectionName]/template/new.tsx index 605b3bd..e52a752 100644 --- a/src/pages/[chainKey]/collection/[collectionName]/template/new.tsx +++ b/src/pages/[chainKey]/collection/[collectionName]/template/new.tsx @@ -4,7 +4,7 @@ import Head from 'next/head'; import { useRouter } from 'next/router'; import { GetServerSideProps } from 'next'; import { withUAL } from 'ual-reactjs-renderer'; -import { CircleNotch, Info } from 'phosphor-react'; +import { CircleNotch, Info } from '@phosphor-icons/react'; import { Disclosure, Popover } from '@headlessui/react'; import { useForm, Controller } from 'react-hook-form'; diff --git a/src/pages/[chainKey]/collection/new.tsx b/src/pages/[chainKey]/collection/new.tsx index 15efb92..3aa36bb 100644 --- a/src/pages/[chainKey]/collection/new.tsx +++ b/src/pages/[chainKey]/collection/new.tsx @@ -8,7 +8,7 @@ import { useForm, Controller } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; -import { UploadSimple, CircleNotch, CaretDown } from 'phosphor-react'; +import { UploadSimple, CircleNotch, CaretDown } from '@phosphor-icons/react'; import { Disclosure } from '@headlessui/react'; import { createCollectionService } from '@services/collection/createCollectionService'; @@ -22,12 +22,17 @@ import { Select } from '@components/Select'; import { countriesList } from '@utils/countriesList'; -import { appName } from '@configs/globalsConfig'; +import { appName, ipfsEndpoint } from '@configs/globalsConfig'; const schema = yup.object().shape({ - image: yup.mixed().test('image', 'Image is required', (value) => { - return value.length > 0; - }), + // image: yup.mixed().test('image', 'Image is required', (value) => { + // return value.length > 0; + // }), + imageIpfsHash: yup + .mixed() + .test('imageIpfsHash', 'Image IPFS hash is required', (value) => { + return value.startsWith('Qm') || value.startsWith('bafy'); + }), collectionName: yup.string().matches(/^[a-z1-5.]+$/, { message: 'Only lowercase letters (a-z) and numbers 1-5 are allowed.', excludeEmptyString: false, @@ -64,27 +69,36 @@ function CreateNewCollection({ ual }) { watch, control, formState: { errors }, + setValue, } = useForm({ resolver: yupResolver(schema), }); - const image = watch('image'); + // const image = watch('image'); + const imageIpfsHash = watch('imageIpfsHash'); const collectionName = watch('collectionName'); useEffect(() => { - if (image && image.length > 0) { - const [img] = image; + // if (image && image.length > 0) { + // const [img] = image; - const fileReader = new FileReader(); - fileReader.onload = () => { - setPreviewImageSrc(fileReader.result); - }; + // const fileReader = new FileReader(); + // fileReader.onload = () => { + // setPreviewImageSrc(fileReader.result); + // }; - fileReader.readAsDataURL(img); + // fileReader.readAsDataURL(img); + // } + if ( + imageIpfsHash && + (imageIpfsHash.startsWith('Qm') || imageIpfsHash.startsWith('bafy')) + ) { + setPreviewImageSrc(`${ipfsEndpoint}/${imageIpfsHash}`); } else { setPreviewImageSrc(null); } - }, [image]); + // }, [image]); + }, [imageIpfsHash]); useEffect(() => { const timer = setTimeout(() => { @@ -112,8 +126,6 @@ function CreateNewCollection({ ual }) { return false; } - console.log(collectionName.startsWith(`.`)); - if ( (collectionName.length <= 12 && (collectionName === userAccount || @@ -127,8 +139,20 @@ function CreateNewCollection({ ual }) { return false; } + function generateCollectionName() { + const validNumbers = '12345'; + let result = ''; + + for (let i = 0; i < 12; i++) { + const randomIndex = Math.floor(Math.random() * validNumbers.length); + result += validNumbers.charAt(randomIndex); + } + setValue('collectionName', result); + } + async function onSubmit({ - image, + // image, + imageIpfsHash, collectionName, displayName, website, @@ -152,7 +176,7 @@ function CreateNewCollection({ ual }) { setIsLoading(true); try { - const data = await uploadImageToIpfsService(image[0]); + // const data = await uploadImageToIpfsService(image[0]); if (collectionNameError) { setIsLoading(false); @@ -182,7 +206,7 @@ function CreateNewCollection({ ual }) { }, { key: 'img', - value: ['string', data['IpfsHash']], + value: ['string', imageIpfsHash], }, { key: 'socials', @@ -329,24 +353,38 @@ function CreateNewCollection({ ual }) { />
) : ( + //
+ // + //

Add Collection Image

+ //

+ // Transparent backgrounds are recommended + //

+ //
- -

Add Collection Image

+

Collection Image Preview

- Transparent backgrounds are recommended + Will be shown if you provide a valid IPFS hash in the form. +
+ Transparent backgrounds are recommended.

)} - + /> */}
@@ -354,17 +392,30 @@ function CreateNewCollection({ ual }) { + + ([]); - const [externalPluginsList, setExternalPluginsList] = useState< - PluginsListProps[] - >([]); - - useEffect(() => { - async function fetchPlugins() { - try { - const response = await fetch('/api/plugins?path=default'); - const pluginNames = await response.json(); - setPluginsList(pluginNames); - - const res = await fetch('/api/plugins?path=external'); - const externalPluginNames = await res.json(); - setExternalPluginsList(externalPluginNames); - } catch (error) { - console.log(error); - } - } - fetchPlugins(); - }, []); - - return ( - <> - - {`Plugins - ${appName}`} - - - - - - -
- {(pluginsList.length > 0 || externalPluginsList.length > 0) && ( -
-
- - -
-
- )} -
- - ); -} - -export const getServerSideProps: GetServerSideProps = async ({ query }) => { - const chainKey = query.chainKey as string; - - try { - return { - props: { - chainKey, - }, - }; - } catch (error) { - return { - notFound: true, - }; - } -}; diff --git a/src/pages/[chainKey]/plugins/[plugin].tsx b/src/pages/[chainKey]/tools/[tool].tsx similarity index 86% rename from src/pages/[chainKey]/plugins/[plugin].tsx rename to src/pages/[chainKey]/tools/[tool].tsx index 1a9f29f..13a2b12 100644 --- a/src/pages/[chainKey]/plugins/[plugin].tsx +++ b/src/pages/[chainKey]/tools/[tool].tsx @@ -12,17 +12,17 @@ import { CollectionProps, } from '@services/collection/getCollectionService'; -interface PluginProps { +interface ToolProps { ual: any; type: string; - plugin: string; + tool: string; chainKey: string; collection: CollectionProps; } -function Plugin({ ual, plugin, type, collection, chainKey }: PluginProps) { +function Tool({ ual, tool, type, collection, chainKey }: ToolProps) { const DynamicComponent = dynamic(() => - import(`../../../plugins/${type}/${plugin}`).then((mod) => mod) + import(`../../../tools/${type}/${tool}`).then((mod) => mod) ); const hasAuthorization = isAuthorizedAccount(ual, collection) as boolean; @@ -44,7 +44,7 @@ function Plugin({ ual, plugin, type, collection, chainKey }: PluginProps) { collectionTabs[5].name, `/${chainKey}/collection/${collection.collection_name}?tab=${collectionTabs[5].key}`, ], - [plugin], + [tool], ]} > )} @@ -55,7 +55,7 @@ function Plugin({ ual, plugin, type, collection, chainKey }: PluginProps) { } export const getServerSideProps: GetServerSideProps = async ({ query }) => { - const plugin = query.plugin as string; + const tool = query.tool as string; const type = query.type as string; const chainKey = query.chainKey as string; const collectionName = query.collection as string; @@ -74,7 +74,7 @@ export const getServerSideProps: GetServerSideProps = async ({ query }) => { return { props: { type, - plugin, + tool, chainKey, collection: collectionData || null, }, @@ -86,4 +86,4 @@ export const getServerSideProps: GetServerSideProps = async ({ query }) => { } }; -export default withUAL(Plugin); +export default withUAL(Tool); diff --git a/src/pages/[chainKey]/tools/index.tsx b/src/pages/[chainKey]/tools/index.tsx new file mode 100644 index 0000000..87284f8 --- /dev/null +++ b/src/pages/[chainKey]/tools/index.tsx @@ -0,0 +1,78 @@ +import { useState, useEffect } from 'react'; +import { GetServerSideProps } from 'next'; +import Head from 'next/head'; + +import { Header } from '@components/Header'; +import { ToolsContainer } from '@components/ToolsContainer'; + +import { appName } from '@configs/globalsConfig'; + +interface ToolsListProps { + tool: string; + label: string; + page: string; + description: string; +} + +export default function Tools({ chainKey }) { + const [toolsList, setToolsList] = useState([]); + const [externalToolsList, setexternalToolsList] = useState( + [] + ); + + useEffect(() => { + async function fetchTools() { + try { + const response = await fetch('/api/tools?path=default'); + const toolNames = await response.json(); + setToolsList(toolNames); + + const res = await fetch('/api/tools?path=external'); + const externalToolNames = await res.json(); + setexternalToolsList(externalToolNames); + } catch (error) { + console.log(error); + } + } + fetchTools(); + }, []); + + return ( + <> + + {`Tools - ${appName}`} + + + + + + +
+ {(toolsList.length > 0 || externalToolsList.length > 0) && ( +
+
+ + +
+
+ )} +
+ + ); +} + +export const getServerSideProps: GetServerSideProps = async ({ query }) => { + const chainKey = query.chainKey as string; + + try { + return { + props: { + chainKey, + }, + }; + } catch (error) { + return { + notFound: true, + }; + } +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 22ca301..52dbb98 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -13,7 +13,7 @@ import { authenticators } from '@libs/authenticators'; import { blockchains } from '@utils/blockchains'; import '@utils/yupMethods'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import '@styles/globals.css'; diff --git a/src/pages/api/plugins.ts b/src/pages/api/tools.ts similarity index 85% rename from src/pages/api/plugins.ts rename to src/pages/api/tools.ts index 7f73e5d..e65a22d 100644 --- a/src/pages/api/plugins.ts +++ b/src/pages/api/tools.ts @@ -3,10 +3,10 @@ import { promises as fs } from 'fs'; export default async function handler(req, res) { try { const { path } = req.query; - const dirPath = `src/plugins/${path}`; + const dirPath = `src/tools/${path}`; const files = await fs.readdir(dirPath, { withFileTypes: true }); - const pluginInfo = []; + const toolInfo = []; for (const file of files) { if (file.isDirectory()) { @@ -19,8 +19,8 @@ export default async function handler(req, res) { /description: '([^']+)'/s )?.[1]; if (name) { - pluginInfo.push({ - plugin: file.name, + toolInfo.push({ + tool: file.name, label: name, page, description, @@ -32,7 +32,7 @@ export default async function handler(req, res) { } } - res.status(200).json(pluginInfo); + res.status(200).json(toolInfo); } catch (error) { console.error(error); res.status(500).send('Internal Server Error'); diff --git a/src/services/account/getAccount.ts b/src/services/account/getAccount.ts index bcfd740..8bf1c24 100644 --- a/src/services/account/getAccount.ts +++ b/src/services/account/getAccount.ts @@ -1,5 +1,5 @@ import { JsonRpc } from 'eosjs'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; interface AccountNameProps { accountName: string; diff --git a/src/services/account/getAccountStatsService.ts b/src/services/account/getAccountStatsService.ts index d65dd9d..1ca4369 100644 --- a/src/services/account/getAccountStatsService.ts +++ b/src/services/account/getAccountStatsService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; export interface AccountStatsProps { diff --git a/src/services/asset/getAssetService.ts b/src/services/asset/getAssetService.ts index 5848203..70c509c 100644 --- a/src/services/asset/getAssetService.ts +++ b/src/services/asset/getAssetService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; export interface AssetProps { diff --git a/src/services/asset/massburnAssetService.ts b/src/services/asset/massburnAssetService.ts new file mode 100644 index 0000000..e6daa3e --- /dev/null +++ b/src/services/asset/massburnAssetService.ts @@ -0,0 +1,33 @@ +interface ActionProps { + account: string; + name: string; + authorization: { + actor: string; + permission: string; + }[]; + data: { + asset_owner: string; + asset_id: string; + }; +} +interface burnAssetProps { + activeUser: any; + actions: ActionProps[]; +} + +export async function burnAssetService({ + activeUser, + actions, +}: burnAssetProps) { + const response = await activeUser.signTransaction( + { + actions, + }, + { + blocksBehind: 3, + expireSeconds: 30, + } + ); + + return response; +} diff --git a/src/services/asset/masscancelSalesService.ts b/src/services/asset/masscancelSalesService.ts new file mode 100644 index 0000000..1a3c403 --- /dev/null +++ b/src/services/asset/masscancelSalesService.ts @@ -0,0 +1,32 @@ +interface ActionProps { + account: string; + name: string; + authorization: { + actor: string; + permission: string; + }[]; + data: { + sale_id: string; + }; +} +interface CancelSalesAssetProps { + activeUser: any; + actions: ActionProps[]; +} + +export async function CancelSalesAssetService({ + activeUser, + actions, +}: CancelSalesAssetProps) { + const response = await activeUser.signTransaction( + { + actions, + }, + { + blocksBehind: 3, + expireSeconds: 30, + } + ); + + return response; +} diff --git a/src/services/collection/collectionAccountsService.ts b/src/services/collection/collectionAccountsService.ts index b1757b3..5f92c33 100644 --- a/src/services/collection/collectionAccountsService.ts +++ b/src/services/collection/collectionAccountsService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; export interface AccountProps { diff --git a/src/services/collection/collectionAssetsService.ts b/src/services/collection/collectionAssetsService.ts index 97ee068..510ea46 100644 --- a/src/services/collection/collectionAssetsService.ts +++ b/src/services/collection/collectionAssetsService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; export interface AssetProps { diff --git a/src/services/collection/collectionSchemasService.ts b/src/services/collection/collectionSchemasService.ts index 8123d4c..5ea8a66 100644 --- a/src/services/collection/collectionSchemasService.ts +++ b/src/services/collection/collectionSchemasService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; export interface SchemaProps { diff --git a/src/services/collection/collectionStatsService.ts b/src/services/collection/collectionStatsService.ts index c87650e..10e0519 100644 --- a/src/services/collection/collectionStatsService.ts +++ b/src/services/collection/collectionStatsService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; export interface StatsProps { diff --git a/src/services/collection/collectionTemplatesService.ts b/src/services/collection/collectionTemplatesService.ts index 4cb2b5d..ae15282 100644 --- a/src/services/collection/collectionTemplatesService.ts +++ b/src/services/collection/collectionTemplatesService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; export interface TemplateProps { diff --git a/src/services/collection/getCollectionService.ts b/src/services/collection/getCollectionService.ts index 5219457..189848c 100644 --- a/src/services/collection/getCollectionService.ts +++ b/src/services/collection/getCollectionService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; export interface CollectionProps { diff --git a/src/services/collection/listCollectionsService.ts b/src/services/collection/listCollectionsService.ts index e9749a2..a61011b 100644 --- a/src/services/collection/listCollectionsService.ts +++ b/src/services/collection/listCollectionsService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; interface OptionsProps { diff --git a/src/services/inventory/getInventoryService.ts b/src/services/inventory/getInventoryService.ts index 4c8851d..d3b1c6e 100644 --- a/src/services/inventory/getInventoryService.ts +++ b/src/services/inventory/getInventoryService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; export interface AssetProps { @@ -72,6 +72,7 @@ export async function getInventoryService( chainKey, options ): Promise> { + console.log(options); const url = `${chainsConfig[chainKey].aaEndpoint}/atomicassets/v1/assets`; const { @@ -81,6 +82,7 @@ export async function getInventoryService( collection_name, template_id, limit, + match, } = options; const response = await api.get(url, { @@ -90,6 +92,7 @@ export async function getInventoryService( template_id, page, limit: limit || 12, + match, offset, order: 'desc', sort: 'transferred_at_time', diff --git a/src/services/sales/getSalesService.ts b/src/services/sales/getSalesService.ts new file mode 100644 index 0000000..5537a57 --- /dev/null +++ b/src/services/sales/getSalesService.ts @@ -0,0 +1,142 @@ +import { api } from '@libs/api'; +import chainsConfig from '@configs/chainsConfig'; +import { AxiosResponse } from 'axios'; + +export interface SaleProps { + market_contract: string; + assets_contract: string; + sale_id: string; + seller: string; + buyer: string | null; + offer_id: string; + price: { + token_contract: string; + token_symbol: string; + token_precision: number; + median: null; + amount: string; + }; + listing_price: string; + listing_symbol: string; + assets: { + contract: string; + asset_id: string; + owner: string; + is_transferable: boolean; + is_burnable: boolean; + collection: { + collection_name: string; + name: string; + img: string; + author: string; + allow_notify: boolean; + authorized_accounts: string[]; + notify_accounts: string[]; + market_fee: number; + created_at_block: string; + created_at_time: string; + }; + schema: { + schema_name: string; + format: { + name: string; + type: string; + }[]; + created_at_block: string; + created_at_time: string; + }; + template: { + template_id: string; + max_supply: string; + is_transferable: boolean; + is_burnable: boolean; + issued_supply: string; + immutable_data: { + [key: string]: any; + }; + created_at_time: string; + created_at_block: string; + }; + mutable_data: { + [key: string]: any; + }; + immutable_data: { + [key: string]: any; + }; + template_mint: string; + backed_tokens: any[]; + burned_by_account: string | null; + burned_at_block: string | null; + burned_at_time: string | null; + updated_at_block: string; + updated_at_time: string; + transferred_at_block: string; + transferred_at_time: string; + minted_at_block: string; + minted_at_time: string; + data: { + [key: string]: any; + }; + name: string; + }[]; + maker_marketplace: string; + taker_marketplace: string | null; + collection_name: string; + collection: { + collection_name: string; + name: string; + img: string; + author: string; + allow_notify: boolean; + authorized_accounts: string[]; + notify_accounts: string[]; + market_fee: number; + created_at_block: string; + created_at_time: string; + }; + is_seller_contract: boolean; + updated_at_block: string; + updated_at_time: string; + created_at_block: string; + created_at_time: string; + ordinality: string; + state: number; +} + +interface DataProps { + data: SaleProps[]; +} + +export async function getSalesService( + chainKey, + options +): Promise> { + const url = `${chainsConfig[chainKey].aaEndpoint}/atomicmarket/v1/sales`; + + const { + page = 1, + offset = 0, + owner, + seller, + collection_name, + limit = 12, + } = options; + + const params = { + state: 1, + owner, + seller, + collection_name, + page, + limit, + offset, + order: 'desc', + sort: 'created', + }; + + const response = await api.get(url, { + params, + }); + + return response; +} diff --git a/src/services/schema/getSchemaService.ts b/src/services/schema/getSchemaService.ts index 68728d1..8a1b738 100644 --- a/src/services/schema/getSchemaService.ts +++ b/src/services/schema/getSchemaService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; export interface SchemaProps { diff --git a/src/services/template/getTemplateService.ts b/src/services/template/getTemplateService.ts index a949369..64a8ba4 100644 --- a/src/services/template/getTemplateService.ts +++ b/src/services/template/getTemplateService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; export interface TemplateProps { diff --git a/src/plugins/default/airdrop/config.ts b/src/tools/default/airdrop/config.ts similarity index 79% rename from src/plugins/default/airdrop/config.ts rename to src/tools/default/airdrop/config.ts index 19f6bcd..93e258c 100644 --- a/src/plugins/default/airdrop/config.ts +++ b/src/tools/default/airdrop/config.ts @@ -1,8 +1,8 @@ -export const pluginInfo = { - name: 'Airdrop', - page: 'plugins', +export const toolInfo = { + name: 'Airdrop NFTs', + page: 'tools', // prettier-ignore - description: 'Simplify NFT Distribution for Large Batches of NFTs using Our User-friendly NFT Drop Tool.', + description: 'Distribute a batch of NFTs using this user-friendly NFT drop tool.', }; export const searchByOptions = [ diff --git a/src/plugins/default/airdrop/index.tsx b/src/tools/default/airdrop/index.tsx similarity index 99% rename from src/plugins/default/airdrop/index.tsx rename to src/tools/default/airdrop/index.tsx index c4b274e..e9eb66a 100644 --- a/src/plugins/default/airdrop/index.tsx +++ b/src/tools/default/airdrop/index.tsx @@ -3,7 +3,7 @@ import { withUAL } from 'ual-reactjs-renderer'; import { Disclosure } from '@headlessui/react'; import { useRouter } from 'next/router'; import Head from 'next/head'; -import { DiceFive, CircleNotch } from 'phosphor-react'; +import { DiceFive, CircleNotch } from '@phosphor-icons/react'; import { useForm, Controller } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; @@ -17,13 +17,13 @@ import { Switch } from '@components/Switch'; import { Textarea } from '@components/Textarea'; import { WarningCard } from '@components/WarningCard'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { appName } from '@configs/globalsConfig'; import * as utils from './utils/utils'; import { validationSchema } from './utils/validationSchema'; import { - pluginInfo, + toolInfo, batchOptions, searchByOptions, dropAssetsOptions, @@ -408,7 +408,7 @@ function Airdrop({ ual }: AirdropProps) { return (
-

{pluginInfo.name}

+

{toolInfo.name}

Bulk send NFTs with filters.
{hasRemainingTransactions ? ( @@ -445,7 +445,7 @@ function Airdrop({ ual }: AirdropProps) { />
)} @@ -804,7 +804,7 @@ function Airdrop({ ual }: AirdropProps) { loading } > - {pluginInfo.name} + {toolInfo.name} )} diff --git a/src/plugins/default/airdrop/services/getAccountsService.ts b/src/tools/default/airdrop/services/getAccountsService.ts similarity index 93% rename from src/plugins/default/airdrop/services/getAccountsService.ts rename to src/tools/default/airdrop/services/getAccountsService.ts index 2b56d7d..d740ad3 100644 --- a/src/plugins/default/airdrop/services/getAccountsService.ts +++ b/src/tools/default/airdrop/services/getAccountsService.ts @@ -1,5 +1,5 @@ import { api } from '@libs/api'; -import * as chainsConfig from '@configs/chainsConfig'; +import chainsConfig from '@configs/chainsConfig'; import { AxiosResponse } from 'axios'; interface DataProps { diff --git a/src/plugins/default/airdrop/services/getRandomSeedService.ts b/src/tools/default/airdrop/services/getRandomSeedService.ts similarity index 100% rename from src/plugins/default/airdrop/services/getRandomSeedService.ts rename to src/tools/default/airdrop/services/getRandomSeedService.ts diff --git a/src/plugins/default/airdrop/utils/utils.ts b/src/tools/default/airdrop/utils/utils.ts similarity index 100% rename from src/plugins/default/airdrop/utils/utils.ts rename to src/tools/default/airdrop/utils/utils.ts diff --git a/src/plugins/default/airdrop/utils/validationSchema.ts b/src/tools/default/airdrop/utils/validationSchema.ts similarity index 100% rename from src/plugins/default/airdrop/utils/validationSchema.ts rename to src/tools/default/airdrop/utils/validationSchema.ts diff --git a/src/tools/default/burn/config.ts b/src/tools/default/burn/config.ts new file mode 100644 index 0000000..6862faf --- /dev/null +++ b/src/tools/default/burn/config.ts @@ -0,0 +1,6 @@ +export const toolInfo = { + name: 'Burn NFTs', + page: 'tools', + // prettier-ignore + description: 'Burn one or many NFTs.', +}; diff --git a/src/tools/default/burn/index.tsx b/src/tools/default/burn/index.tsx new file mode 100644 index 0000000..d4b66f4 --- /dev/null +++ b/src/tools/default/burn/index.tsx @@ -0,0 +1,516 @@ +import { useState, useEffect, useRef } from 'react'; +import Link from 'next/link'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import { CircleNotch, MagnifyingGlass } from '@phosphor-icons/react'; +import { withUAL } from 'ual-reactjs-renderer'; +import { Disclosure } from '@headlessui/react'; + +import { useForm } from 'react-hook-form'; + +import { Card } from '@components/Card'; +import { Input } from '@components/Input'; +import { Modal } from '@components/Modal'; +import { Select } from '@components/Select'; +import { Loading } from '@components/Loading'; +import { CardContainer } from '@components/CardContainer'; +import { SeeMoreButton } from '@components/SeeMoreButton'; +import { Header } from '@components/Header'; + +import chainsConfig from '@configs/chainsConfig'; +import { ipfsEndpoint, chainKeyDefault, appName } from '@configs/globalsConfig'; + +import { burnAssetService } from '@services/asset/massburnAssetService'; +import { + getInventoryService, + AssetProps, +} from '@services/inventory/getInventoryService'; +import { getAccountStatsService } from '@services/account/getAccountStatsService'; + +interface ModalProps { + title: string; + message?: string; + details?: string; + isError?: boolean; +} + +function Burn({ ual }) { + const router = useRouter(); + const modalRef = useRef(null); + + const { handleSubmit } = useForm({}); + + const [isLoading, setIsLoading] = useState(false); + const [isSaved, setIsSaved] = useState(false); + const [selectedAssets, setSelectedAssets] = useState([]); + const [ownedCollections, setOwnedCollections] = useState([]); + const [collectionsFilterOptions, setCollectionsFilterOptions] = useState([]); + const [filteredAssets, setFilteredAssets] = useState([]); + const [selectedCollection, setSelectedCollection] = useState(''); + const [match, setMatch] = useState(''); + const [waitToSearch, setWaitToSearch] = useState(null); + const [modal, setModal] = useState({ + title: '', + message: '', + details: '', + isError: false, + }); + + const chainKey = (router.query.chainKey ?? chainKeyDefault) as string; + + const chainIdLogged = + ual?.activeUser?.chainId ?? ual?.activeUser?.chain.chainId; + + const chainId = chainsConfig[chainKey].chainId; + + const limit = 12; + const currentPage = Math.ceil(filteredAssets.length / limit); + const offset = (currentPage - 1) * limit; + const isEndOfList = filteredAssets.length % limit > 0; + + const accountName = ual?.activeUser?.accountName; + + const getViewLink = (asset) => { + if (chainKey == 'xpr') { + return `https://soon.market/nft/templates/${asset.template.template_id}/${asset.asset_id}?utm_medium=card&utm_source=nft-manager`; + } + return `/${chainKey}/collection/${asset.collection.collection_name}/asset/${asset.asset_id}`; + }; + + useEffect(() => { + async function getUserInfo() { + try { + const { data: inventory } = await getInventoryService(chainKey, { + owner: accountName, + }); + + const { data: collections } = await getAccountStatsService( + chainKey, + accountName + ); + + setFilteredAssets(inventory.data); + setOwnedCollections(collections.data['collections']); + } catch (e) { + modalRef.current?.openModal(); + const jsonError = JSON.parse(JSON.stringify(e)); + const details = JSON.stringify(e, undefined, 2); + const message = + jsonError?.cause?.json?.error?.details[0]?.message ?? + 'Unable to get user inventory or collections'; + + setModal({ + title: 'Error', + message, + details, + }); + } + } + + if (!!chainId && !!chainIdLogged && chainId === chainIdLogged) { + getUserInfo(); + } + }, [chainKey, chainIdLogged, chainId, accountName]); + + useEffect(() => { + let options = [ + { + label: `All Collections (${ownedCollections.length})`, + value: '', + }, + ]; + + ownedCollections.forEach((item) => + options.push({ + label: `(${item.collection.name}) By ${item.collection.author}`, + value: item.collection.collection_name, + }) + ); + + setCollectionsFilterOptions(options); + }, [ownedCollections]); + + useEffect(() => { + async function getUserInventory() { + try { + const { data: inventory } = await getInventoryService(chainKey, { + owner: accountName, + collection_name: selectedCollection, + }); + + setFilteredAssets(inventory.data); + } catch (e) { + modalRef.current?.openModal(); + const jsonError = JSON.parse(JSON.stringify(e)); + const details = JSON.stringify(e, undefined, 2); + const message = + jsonError?.cause?.json?.error?.details[0]?.message ?? + 'Unable to get user inventory'; + + setModal({ + title: 'Error', + message, + details, + }); + } + } + getUserInventory(); + }, [selectedCollection, accountName, chainKey]); + + async function handleSeeMoreAssets() { + setIsLoading(true); + + try { + const { data } = await getInventoryService(chainKey, { + owner: accountName, + match, + collection_name: selectedCollection, + page: currentPage + 1, + limit, + offset, + }); + + setFilteredAssets((state) => [...state, ...data.data]); + } catch (e) { + modalRef.current?.openModal(); + const jsonError = JSON.parse(JSON.stringify(e)); + const details = JSON.stringify(e, undefined, 2); + const message = + jsonError?.cause?.json?.error?.details[0]?.message ?? + 'Unable to get user inventory'; + + setModal({ + title: 'Error', + message, + details, + }); + } + + setIsLoading(false); + } + + async function onSubmit() { + setIsLoading(true); + + let assetIds = []; + selectedAssets.map((item) => { + assetIds.push(item.asset_id); + }); + try { + const actions = []; + + assetIds.map((assetId) => { + const action = { + account: 'atomicassets', + name: 'burnasset', + authorization: [ + { + actor: ual.activeUser.accountName, + permission: ual.activeUser.requestPermission, + }, + ], + data: { + asset_owner: ual.activeUser.accountName, + asset_id: assetId, + }, + }; + + actions.push(action); + }); + + await burnAssetService({ + activeUser: ual.activeUser, + actions: actions, + }); + + setIsSaved(true); + + modalRef.current?.openModal(); + const title = 'NFTs was successfully burned'; + const message = 'Please wait while we refresh the page.'; + + setModal({ + title, + message, + }); + + setTimeout(() => { + router.reload(); + setIsSaved(false); + }, 3000); + } catch (e) { + modalRef.current?.openModal(); + const jsonError = JSON.parse(JSON.stringify(e)); + const details = JSON.stringify(e, undefined, 2); + const message = + jsonError?.cause?.json?.error?.details[0]?.message ?? + 'Unable to burn NFTs'; + + setModal({ + title: 'Error', + message, + details, + isError: true, + }); + } + setIsLoading(false); + } + + function handleAssetSelection(asset) { + const alreadySelected = + selectedAssets.length > 0 && + selectedAssets.find((item) => item && item.asset_id === asset.asset_id); + + if (!alreadySelected) { + setSelectedAssets((state) => [...state, ...[asset]]); + } + + setSelectedAssets((state) => { + const assetIndex = selectedAssets.findIndex( + (item) => item && item.asset_id === asset.asset_id + ); + + let newState = state.filter((item, index) => index !== assetIndex); + + return [...newState]; + }); + } + + function handleLogin() { + ual?.showModal(); + } + + async function handleSearch(event) { + const { value } = event.target; + clearTimeout(waitToSearch); + + setMatch(value); + const newWaitToSearch = setTimeout(async () => { + const { data: assets } = await getInventoryService(chainKey, { + match: value || '', + owner: accountName, + collection_name: selectedCollection || '', + }); + + setFilteredAssets(assets.data); + }, 500); + + setWaitToSearch(newWaitToSearch); + } + + if (!!chainId && !!chainIdLogged && chainId === chainIdLogged) { + return ( + <> + + {`Burn NFTs - ${appName}`} + + + + + + +
+

Burn one or multiple NFTs

+
    +
  1. + Select the NFTs by clicking on their pictures to the right. +
  2. +
  3. Each selected NFT will be burned.
  4. +
  5. Click the "Burn NFTs" button.
  6. +
+ +

{modal.message}

+ {!modal.isError ? ( + + + Loading... + + ) : ( + + + Details + + +
+                    {modal.details}
+                  
+
+
+ )} +
+ +
+
+ {selectedAssets.length > 0 ? ( +
+

Selected NFTs

+ + {selectedAssets.map((asset, index) => ( +
+ handleAssetSelection(asset)} + image={ + asset.data['img'] + ? `${ipfsEndpoint}/${asset.data['img']}` + : asset.data['image'] + ? `${ipfsEndpoint}/${asset.data['image']}` + : asset.data['glbthumb'] + ? `${ipfsEndpoint}/${asset.data['glbthumb']}` + : '' + } + video={ + asset.data['video'] + ? `${ipfsEndpoint}/${asset.data['video']}` + : '' + } + title={asset.name} + subtitle={`by ${asset.collection.author}`} + viewLink={getViewLink(asset)} + /> +
+ ))} +
+
+ ) : ( +
+

Select NFTs to burn

+
+ )} + {isLoading ? ( + + + Loading... + + ) : ( + + )} +
+
+
+

Select NFTs to burn

+ + {collectionsFilterOptions.length > 0 && ( +
+ } + type="search" + label="Search by name" + placeholder="Search NFT" + onChange={handleSearch} + value={match} + /> + {filteredAssets.length > 0 ? ( + + {filteredAssets.map((asset, index) => { + if (asset.is_burnable) { + return ( +
+
+ handleAssetSelection(asset)} + image={ + asset.data.image + ? `${ipfsEndpoint}/${asset.data.image}` + : asset.data.img + ? `${ipfsEndpoint}/${asset.data.img}` + : asset.data.glbthumb + ? `${ipfsEndpoint}/${asset.data.glbthumb}` + : '' + } + video={ + asset.data.video + ? `${ipfsEndpoint}/${asset.data.video}` + : '' + } + title={asset.name} + subtitle={`by ${asset.collection.author}`} + viewLink={getViewLink(asset)} + /> +
+
+ ); + } + })} +
+ ) : ( + <> + {isLoading ? ( + + ) : ( +
+

NFTs not found

+
+ )} + + )} + {!isEndOfList && ( +
+ +
+ )} +
+
+
+
+ + ); + } + + return ( + <> + + {`Burn NFTs - ${appName}`} + + + {!ual?.activeUser && ( +
+

Connect your wallet

+

+ You need to connect your wallet to burn one or multiple NFTs +

+ +
+ )} + + ); +} + +export default withUAL(Burn); diff --git a/src/tools/default/cancel-sales/config.ts b/src/tools/default/cancel-sales/config.ts new file mode 100644 index 0000000..ca873af --- /dev/null +++ b/src/tools/default/cancel-sales/config.ts @@ -0,0 +1,6 @@ +export const toolInfo = { + name: 'Cancel Sales', + page: 'tools', + // prettier-ignore + description: 'Cancel one or many sales on AtomicMarket.', +}; diff --git a/src/tools/default/cancel-sales/index.tsx b/src/tools/default/cancel-sales/index.tsx new file mode 100644 index 0000000..2dc5ec3 --- /dev/null +++ b/src/tools/default/cancel-sales/index.tsx @@ -0,0 +1,496 @@ +import { useState, useEffect, useRef } from 'react'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import { CircleNotch } from '@phosphor-icons/react'; +import { withUAL } from 'ual-reactjs-renderer'; +import { Disclosure } from '@headlessui/react'; + +import { useForm } from 'react-hook-form'; + +import { Card } from '@components/Card'; +import { Modal } from '@components/Modal'; +import { Select } from '@components/Select'; +import { Loading } from '@components/Loading'; +import { CardContainer } from '@components/CardContainer'; +import { SeeMoreButton } from '@components/SeeMoreButton'; +import { Header } from '@components/Header'; + +import chainsConfig from '@configs/chainsConfig'; +import { ipfsEndpoint, chainKeyDefault, appName } from '@configs/globalsConfig'; + +import { SaleProps, getSalesService } from '@services/sales/getSalesService'; +import { getAccountStatsService } from '@services/account/getAccountStatsService'; +import { CancelSalesAssetService } from '@services/asset/masscancelSalesService'; + +interface ModalProps { + title: string; + message?: string; + details?: string; + isError?: boolean; +} + +function CancelSales({ ual }) { + const router = useRouter(); + const modalRef = useRef(null); + + const { + handleSubmit, + } = useForm({}); + + const [isLoading, setIsLoading] = useState(false); + const [isSaved, setIsSaved] = useState(false); + const [selectedSales, setSelectedSales] = useState([]); + const [ownedCollections, setOwnedCollections] = useState([]); + const [collectionsFilterOptions, setCollectionsFilterOptions] = useState([]); + const [filteredSales, setFilteredSales] = useState([]); + const [selectedCollection, setSelectedCollection] = useState(''); + const [modal, setModal] = useState({ + title: '', + message: '', + details: '', + isError: false, + }); + + const chainKey = (router.query.chainKey ?? chainKeyDefault) as string; + + const chainIdLogged = + ual?.activeUser?.chainId ?? ual?.activeUser?.chain.chainId; + + const chainId = chainsConfig[chainKey].chainId; + + const limit = 12; + const currentPage = Math.ceil(filteredSales.length / limit); + const offset = (currentPage - 1) * limit; + const isEndOfList = filteredSales.length % limit > 0; + + const accountName = ual?.activeUser?.accountName; + + const getViewLink = (sale) => { + if(chainKey == 'xpr') { + return `https://soon.market/sales/${sale.sale_id}`; + } + return undefined; + } + + useEffect(() => { + async function getUserInfo() { + try { + const { data: sales } = await getSalesService(chainKey, { + owner: accountName, + }); + + const { data: collections } = await getAccountStatsService( + chainKey, + accountName + ); + + setFilteredSales(sales.data); + setOwnedCollections(collections.data['collections']); + } catch (e) { + modalRef.current?.openModal(); + const jsonError = JSON.parse(JSON.stringify(e)); + const details = JSON.stringify(e, undefined, 2); + const message = + jsonError?.cause?.json?.error?.details[0]?.message ?? + 'Unable to get user sales or collections'; + + setModal({ + title: 'Error', + message, + details, + }); + } + } + + if (!!chainId && !!chainIdLogged && chainId === chainIdLogged) { + getUserInfo(); + } + }, [chainKey, chainIdLogged, chainId, accountName]); + + useEffect(() => { + let options = [ + { + label: `All Collections (${ownedCollections.length})`, + value: '', + }, + ]; + + ownedCollections.forEach((item) => + options.push({ + label: `(${item.collection.name}) By ${item.collection.author}`, + value: item.collection.collection_name, + }) + ); + + setCollectionsFilterOptions(options); + }, [ownedCollections]); + + useEffect(() => { + async function getUserSales() { + try { + const { data: sales } = await getSalesService(chainKey, { + owner: accountName, + collection_name: selectedCollection, + }); + + setFilteredSales(sales.data); + } catch (e) { + modalRef.current?.openModal(); + const jsonError = JSON.parse(JSON.stringify(e)); + const details = JSON.stringify(e, undefined, 2); + const message = + jsonError?.cause?.json?.error?.details[0]?.message ?? + 'Unable to get user sales'; + + setModal({ + title: 'Error', + message, + details, + }); + } + } + getUserSales(); + }, [selectedCollection, accountName, chainKey]); + + async function handleSeeMoreSales() { + setIsLoading(true); + + try { + const { data } = await getSalesService(chainKey, { + owner: accountName, + collection_name: selectedCollection, + page: currentPage + 1, + limit, + offset, + }); + + setFilteredSales((state) => [...state, ...data.data]); + } catch (e) { + modalRef.current?.openModal(); + const jsonError = JSON.parse(JSON.stringify(e)); + const details = JSON.stringify(e, undefined, 2); + const message = + jsonError?.cause?.json?.error?.details[0]?.message ?? + 'Unable to get user sales'; + + setModal({ + title: 'Error', + message, + details, + }); + } + + setIsLoading(false); + } + + async function onSubmit() { + setIsLoading(true); + + let saleIds = []; + selectedSales.map((sale) => { + saleIds.push(sale.sale_id); + }); + try { + const actions = []; + saleIds.map((id) => { + const action = { + account: 'atomicmarket', + name: 'cancelsale', + authorization: [ + { + actor: ual.activeUser.accountName, + permission: ual.activeUser.requestPermission, + }, + ], + data: { + sale_id: id, + }, + }; + actions.push(action); + }); + + await CancelSalesAssetService({ + activeUser: ual.activeUser, + actions: actions, + }); + + setIsSaved(true); + + modalRef.current?.openModal(); + const title = 'Sales successfully canceled.'; + const message = 'Please wait while we refresh the page.'; + + setModal({ + title, + message, + }); + + setTimeout(() => { + router.reload(); + setIsSaved(false); + }, 3000); + } catch (e) { + modalRef.current?.openModal(); + const jsonError = JSON.parse(JSON.stringify(e)); + const details = JSON.stringify(e, undefined, 2); + const message = + jsonError?.cause?.json?.error?.details[0]?.message ?? + 'Unable to cancel the sale of selected NFTs'; + + setModal({ + title: 'Error', + message, + details, + isError: true, + }); + } + setIsLoading(false); + } + + function handleSaleSelection(sale) { + const alreadySelected = + selectedSales.length > 0 && + selectedSales.some((item) => item.sale_id === sale.sale_id); + if (!alreadySelected) { + setSelectedSales((state) => [...state, ...[sale]]); + } else { + setSelectedSales((state) => { + const saleIndex = state.findIndex( + (item) => item && item.sale_id === sale.sale_id + ); + let newState = state.filter((item, index) => index !== saleIndex); + return [...newState]; + }); + } + } + + function handleLogin() { + ual?.showModal(); + } + + if (!!chainId && !!chainIdLogged && chainId === chainIdLogged) { + return ( + <> + + {`Cancel Sales - ${appName}`} + + + + + + +
+

+ Cancel one or multiple sales +

+
    +
  1. Select the sales by clicking on their pictures to the right.
  2. +
  3. Each selected sale will be cancelled.
  4. +
  5. Click the "Cancel Sales" button.
  6. +
+ +

{modal.message}

+ {!modal.isError ? ( + + + Loading... + + ) : ( + + + Details + + +
+                    {modal.details}
+                  
+
+
+ )} +
+ +
+
+ {selectedSales.length > 0 ? ( +
+

Selected Sales

+ + {selectedSales.map((sale, index) => ( +
+ handleSaleSelection(sale)} + image={ + sale.assets[0].data['img'] + ? `${ipfsEndpoint}/${sale.assets[0].data['img']}` + : sale.assets[0].data['image'] + ? `${ipfsEndpoint}/${sale.assets[0].data['image']}` + : sale.assets[0].data['glbthumb'] + ? `${ipfsEndpoint}/${sale.assets[0].data['glbthumb']}` + : '' + } + video={ + sale.assets[0].data['video'] ? `${ipfsEndpoint}/${sale.assets[0].data['video']}` : '' + } + title={sale.assets[0].name} + subtitle={`by ${sale.collection.author}`} + saleInfo={ + { + isBundle: true, + assetCount: sale.assets.length, + listingPrice: sale.listing_price, + token: sale.listing_symbol, + tokenPrecision: sale.price.token_precision + } + } + viewLink={getViewLink(sale)} + /> +
+ ))} +
+
+ ) : ( +
+

Select sales

+
+ )} + {isLoading ? ( + + + Loading... + + ) : ( + + )} +
+
+
+

Select sales

+ + {collectionsFilterOptions.length > 0 && ( +
+ } type="search" placeholder="Search collection" onChange={handleSearch} /> - + */}
diff --git a/src/pages/[chainKey]/collection/[collectionName].tsx b/src/pages/[chainKey]/collection/[collectionName].tsx index 4e5b382..3f5f1b4 100644 --- a/src/pages/[chainKey]/collection/[collectionName].tsx +++ b/src/pages/[chainKey]/collection/[collectionName].tsx @@ -110,6 +110,26 @@ function Collection({ >
+ {(hasAuthorization && chainKey == 'xpr') && ( + + Create NFT on Soon.Market + + )} + {chainKey == 'xpr' && ( + + Show on Soon.Market + + )} {hasAuthorization ? ( )} - - Website -
{collectionTabs[0].name} - - {collectionTabs[1].name} - {stats.schemas ?? '0'} - + {chainKey != "xpr" && ( + + {collectionTabs[1].name} + {stats.schemas ?? '0'} + + )} {collectionTabs[2].name} {stats.templates ?? '0'} @@ -176,23 +190,25 @@ function Collection({ {stats.assets ?? '0'} {collectionTabs[4].name} - {hasAuthorization && ( - {collectionTabs[5].name} - )} + {/* {hasAuthorization && ( + {collectionTabs[4].name} + )} */} - - - - + + {chainKey != "xpr" && ( + + + + )} - + {/* - + */} diff --git a/src/pages/[chainKey]/collection/[collectionName]/edit.tsx b/src/pages/[chainKey]/collection/[collectionName]/edit.tsx index 495fa08..8d46ce2 100644 --- a/src/pages/[chainKey]/collection/[collectionName]/edit.tsx +++ b/src/pages/[chainKey]/collection/[collectionName]/edit.tsx @@ -14,7 +14,6 @@ import * as yup from 'yup'; import { ipfsEndpoint, appName } from '@configs/globalsConfig'; import { editCollectionService } from '@services/collection/editCollectionService'; -import { uploadImageToIpfsService } from '@services/collection/uploadImageToIpfsService'; import { getCollectionService, CollectionProps, @@ -30,8 +29,12 @@ import { Modal } from '@components/Modal'; import { countriesList } from '@utils/countriesList'; const informationValidations = yup.object().shape({ + imageIpfsHash: yup.mixed() + .test('imageIpfsHash', 'Image IPFS hash is required', (value) => { + return value.startsWith('Qm') || value.startsWith('bafy'); + }), displayName: yup.string().required().label('Display name'), - website: yup.string().url().label('Website'), + // website: yup.string().url().label('Website'), description: yup.string(), }); @@ -53,10 +56,21 @@ const notificationValidations = yup.object().shape({ }); interface InformationProps { - image: File; + imageIpfsHash: string; displayName: string; - website: string; description: string; + socials: SocialProps; +} + +interface SocialProps { + website?: string; + twitter?: string; + telegram?: string; + discord?: string; + instagram?: string; + youtube?: string; + snipverse?: string; + medium?: string; } interface EditCollectionProps { @@ -88,10 +102,10 @@ function EditCollection({ const router = useRouter(); const modalRef = useRef(null); - const [imageSrc, setImageSrc] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isSaved, setIsSaved] = useState(false); const [collection, setCollection] = useState(initialCollection); + const [previewImageSrc, setPreviewImageSrc] = useState(collection.img ? `${ipfsEndpoint}/${collection.img}` : null); const [modal, setModal] = useState({ title: '', message: '', @@ -99,10 +113,25 @@ function EditCollection({ isError: false, }); - const creatorInfo = - collection.data.creator_info && JSON.parse(collection.data.creator_info); - const socials = - collection.data.socials && JSON.parse(collection.data.socials); + const socials = collection.data.url && collection.data.url.includes('\n') ? collection.data.url.split('\n').map((line) => { + if (line.includes('https:') || line.includes('www.')) { + const colonIndex = line.indexOf(':', line.indexOf('https:') > -1 ? 6 : 5); + const platform = line.substring(0, colonIndex); + let url = line.substring(colonIndex + 1); + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://" + url; + } + return { platform, url }; + } + return null; + }).filter((item) => item !== null) : []; + + // this will be used in the future most likely. at this point we stick to the status quo on XPR Network + // + // const creatorInfo = + // collection.data.creator_info && JSON.parse(collection.data.creator_info); + // const socials = + // collection.data.socials && JSON.parse(collection.data.socials); const { register, @@ -138,13 +167,18 @@ function EditCollection({ resolver: yupResolver(notificationValidations), }); - const image = watch('image'); + const imageIpfsHash = watch('imageIpfsHash'); useEffect(() => { - if (typeof image !== 'string' && image && image.length > 0) { - handleImageSource(image[0]); + if ( + imageIpfsHash && + (imageIpfsHash.startsWith('Qm') || imageIpfsHash.startsWith('bafy')) + ) { + setPreviewImageSrc(`${ipfsEndpoint}/${imageIpfsHash}`); + } else { + setPreviewImageSrc(null); } - }, [image]); + }, [imageIpfsHash]); useEffect(() => { const timer = setTimeout(() => { @@ -153,16 +187,6 @@ function EditCollection({ return () => clearTimeout(timer); }, [isSaved]); - const handleImageSource = (img) => { - if (img) { - const fileReader = new FileReader(); - fileReader.onload = () => { - setImageSrc(fileReader.result); - }; - fileReader.readAsDataURL(img); - } - }; - if (!ual?.activeUser || ual?.activeUser?.accountName !== collection?.author) { return ( <> @@ -185,16 +209,38 @@ function EditCollection({ } async function onSubmitInformation({ - image, + imageIpfsHash, displayName, - website, description, - }: InformationProps) { + website, + telegram, + twitter, + instagram, + discord, + youtube, + snipverse, + medium, + }: InformationProps & SocialProps) { setIsLoading(true); try { - const pinataImage = - image[0].length > 0 && (await uploadImageToIpfsService(image[0])); + const socialLinks = [ + { name: 'website', link: website }, + { name: 'twitter', link: twitter }, + { name: 'telegram', link: telegram }, + { name: 'instagram', link: instagram }, + { name: 'youtube', link: youtube }, + { name: 'discord', link: discord }, + { name: 'snipverse', link: snipverse }, + { name: 'medium', link: medium }, + ]; + + const socials = socialLinks + .map((link) => { + const trimmedLink = link.link ? link.link.trim().replace(/\/+$/, '') : ''; + return `${link.name}:${trimmedLink}`; + }) + .join('\n'); const editedInformation = await editCollectionService({ action: 'setcoldata', @@ -211,12 +257,12 @@ function EditCollection({ value: ['string', description], }, { - key: 'url', - value: ['string', website], + key: 'img', + value: ['string', imageIpfsHash], }, { - key: 'img', - value: ['string', pinataImage['IpfsHash'] ?? collection.img], + key: 'url', + value: ['string', `${socials}\n`], }, ], }, @@ -561,56 +607,63 @@ function EditCollection({