├── .commitlintrc.json ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── publish.yml │ ├── semantic_version.yml │ ├── test.yml │ └── typedoc.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── release.config.js ├── src ├── abis │ ├── core-mx-life-bonding-sc.abi.json │ ├── core-mx-liveliness-stake.abi.json │ ├── data-nft-lease.abi.json │ ├── data_market.abi.json │ └── datanftmint.abi.json ├── bond.ts ├── common │ ├── mint-utils.ts │ ├── utils.ts │ └── validator.ts ├── config.ts ├── contract.ts ├── datanft.ts ├── errors.ts ├── index.ts ├── interfaces.ts ├── liveliness-stake.ts ├── marketplace.ts ├── minter.ts ├── nft-minter.ts └── sft-minter.ts ├── tests ├── bond.test.ts ├── datanft.test.ts ├── environment.test.ts ├── marketplace.test.ts ├── nftminter.test.ts ├── sftminter.test.ts ├── stake.test.ts ├── traits-check.test.ts └── validator.test.ts ├── tsconfig.json └── tslint.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | out/ 4 | node_modules/ 5 | .snapshots/ 6 | *.min.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "parser": "@typescript-eslint/parser", 7 | "parserOptions": { 8 | "ecmaVersion": 2021, 9 | "sourceType": "module", 10 | "ecmaFeatures": { 11 | "jsx": true 12 | }, 13 | "project": "./tsconfig.json" 14 | }, 15 | "settings": { 16 | "import/parsers": { 17 | "@typescript-eslint/parser": [".ts", ".tsx"] 18 | }, 19 | "import/resolver": { 20 | "node": { 21 | "extensions": [".js", ".jsx", ".ts", ".tsx"], 22 | "moduleDirectory": ["node_modules", "src/"] 23 | }, 24 | "typescript": { 25 | "alwaysTryTypes": true 26 | } 27 | }, 28 | "react": { 29 | "pragma": "React", 30 | "version": "detect" 31 | } 32 | }, 33 | "extends": [ 34 | "plugin:react/recommended", 35 | "plugin:@typescript-eslint/recommended", 36 | "prettier", 37 | "plugin:prettier/recommended" 38 | ], 39 | "plugins": ["react", "prettier", "import"], 40 | "rules": { 41 | "import/order": [ 42 | "warn", 43 | { 44 | "groups": ["builtin", "external", "internal"], 45 | "pathGroups": [ 46 | { 47 | "pattern": "react", 48 | "group": "external", 49 | "position": "before" 50 | } 51 | ], 52 | "pathGroupsExcludedImportTypes": ["react"], 53 | "newlines-between": "ignore", 54 | "alphabetize": { 55 | "order": "asc", 56 | "caseInsensitive": true 57 | } 58 | } 59 | ], 60 | "prettier/prettier": [ 61 | "error", 62 | { 63 | "endOfLine": "lf" 64 | } 65 | ], 66 | "no-restricted-imports": [ 67 | "error", 68 | { 69 | "patterns": [ 70 | "@mui/*/*/*", 71 | "!@mui/material/test-utils/*", 72 | "!@mui/material/styles/*", 73 | "!@mui/styles/*" 74 | ] 75 | } 76 | ], 77 | "@typescript-eslint/indent": "off", 78 | "@typescript-eslint/explicit-module-boundary-types": "off", 79 | "@typescript-eslint/no-use-before-define": [ 80 | "error", 81 | { "functions": false, "classes": false } 82 | ], 83 | "@typescript-eslint/no-explicit-any": "off", 84 | "@typescript-eslint/no-var-requires": "off", 85 | "@typescript-eslint/explicit-function-return-type": "off", 86 | "react/jsx-one-expression-per-line": "off", 87 | "react/prop-types": "off", 88 | "linebreak-style": ["error", "unix"], 89 | "quotes": ["error", "single"], 90 | "semi": ["error", "always"], 91 | "object-curly-newline": "off", 92 | "arrow-body-style": "off", 93 | "react/jsx-props-no-spreading": "off", 94 | "implicit-arrow-linebreak": "off", 95 | "func-names": "off", 96 | "operator-linebreak": "off", 97 | "function-paren-newline": "off", 98 | "react/require-default-props": "off", 99 | "react/display-name": "off", 100 | "react/jsx-curly-newline": "off", 101 | "react/jsx-wrap-multilines": "off", 102 | "react/destructuring-assignment": "off", 103 | "no-shadow": "off", 104 | "@typescript-eslint/no-shadow": "off", 105 | "react/no-array-index-key": "off" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | publish-npm: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: '18.x' 20 | registry-url: https://registry.npmjs.org/ 21 | - name: Install dependencies 22 | run: npx ci 23 | - name: Lint 24 | run: npm run lint:fix 25 | - name: Test 26 | run: npm run test --if-present 27 | - name: Create release 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: | 31 | RELEASE_TAG=v$(node -p "require('./package.json').version") 32 | gh release create $RELEASE_TAG --target=$GITHUB_SHA --title="$RELEASE_TAG" --generate-notes 33 | - name: Publish to npmjs 34 | env: 35 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 36 | run: npm publish --access=public 37 | -------------------------------------------------------------------------------- /.github/workflows/semantic_version.yml: -------------------------------------------------------------------------------- 1 | name: Determine Next Release Version and bump version 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | get-next-version: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: '20.x' 19 | 20 | - name: Install dependencies 21 | run: npm install 22 | 23 | - name: Run semantic release dry-run 24 | id: semantic 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | run: | 29 | OUTPUT=$(unset GITHUB_ACTIONS && npx semantic-release --dry-run --no-ci) 30 | VERSION=$(echo "$OUTPUT" | grep -o "The next release version is [0-9]*\.[0-9]*\.[0-9]*" | awk '{print $6}') 31 | echo "::set-output name=version::$VERSION" 32 | 33 | - name: Use the version 34 | run: echo "The version is ${{ steps.semantic.outputs.version }}" 35 | 36 | - name: Check if version needs to be updated 37 | id: check_version 38 | run: | 39 | current_version=$(jq -r '.version' package.json) 40 | new_version="${{ steps.semantic.outputs.version }}" 41 | if [ "$current_version" != "$new_version" ]; then 42 | echo "::set-output name=version_updated::true" 43 | else 44 | echo "::set-output name=version_updated::false" 45 | fi 46 | 47 | - name: Update package.json with new version 48 | if: steps.check_version.outputs.version_updated == 'true' 49 | run: | 50 | sed -i 's/"version": "[^"]*"/"version": "${{ steps.semantic.outputs.version }}"/' package.json 51 | 52 | - name: Commit version update 53 | if: steps.check_version.outputs.version_updated == 'true' 54 | run: | 55 | git config user.name 'github-actions[bot]' 56 | git config user.email 'github-actions[bot]@users.noreply.github.com' 57 | git add package.json 58 | git commit -m "chore: update package.json version to ${{ steps.semantic.outputs.version }}" 59 | git push -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - develop 7 | pull_request: 8 | branches: 9 | - main 10 | - develop 11 | 12 | jobs: 13 | test-job: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | 19 | - name: Use Node 18.x 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: '18.x' 23 | 24 | - name: Install dependencies 25 | run: npm ci 26 | 27 | - name: Test 28 | run: npm test 29 | -------------------------------------------------------------------------------- /.github/workflows/typedoc.yml: -------------------------------------------------------------------------------- 1 | name: Create documentation and deploy to GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | permissions: 7 | contents: write 8 | jobs: 9 | build-and-deploy: 10 | concurrency: ci-${{ github.ref }} 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Create docs 17 | run: | 18 | npm install 19 | npx typedoc src/index.ts 20 | 21 | - name: Deploy to GitHub Pages 22 | uses: JamesIves/github-pages-deploy-action@v4 23 | with: 24 | folder: docs 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | .cache 7 | *.log 8 | .DS_Store 9 | 10 | # builds 11 | build 12 | dist 13 | .idea 14 | .idea/workspace.xml 15 | .rpt2_cache 16 | .yalc 17 | out 18 | 19 | # misc 20 | .DS_Store 21 | .env 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | coverage 33 | 34 | docs -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm test 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | 3 | .travis.yml 4 | .idea 5 | .eslintrc 6 | .eslintignore 7 | .prettierrc 8 | 9 | tsconfig.json 10 | tsconfig.test.json 11 | jest.config.ts 12 | babel.config.js 13 | esbuild.js 14 | jest.config.js 15 | 16 | 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": true, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none", 10 | "printWidth": 80 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.6.2](https://github.com/Itheum/sdk-mx-data-nft/compare/v2.6.1...v2.6.2) (2024-02-05) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * remove husky from package scripts ([462e3e7](https://github.com/Itheum/sdk-mx-data-nft/commit/462e3e703d6caab1217691b75925f4cb427da343)) 7 | 8 | ## [2.6.1](https://github.com/Itheum/sdk-mx-data-nft/compare/v2.6.0...v2.6.1) (2024-02-05) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * add limit to max 50 items per call ([d3bbed8](https://github.com/Itheum/sdk-mx-data-nft/commit/d3bbed8f31e1ea3e4eed40e5445d15f0091c7faf)) 14 | * add linx:fix script ([0644626](https://github.com/Itheum/sdk-mx-data-nft/commit/0644626342c02e897da363b2591bed3621d5928c)) 15 | * add override attributes and viewData url change based on override ([a1441f1](https://github.com/Itheum/sdk-mx-data-nft/commit/a1441f1788fb420fedc19a91b609042e7d19a68f)) 16 | * assign the dataMarshal to originalDataMarshal before override ([f3b7957](https://github.com/Itheum/sdk-mx-data-nft/commit/f3b7957c969a4db5e5200c9fc3ec0a8b1620d897)) 17 | * axios vulnerabilities ([0bffa10](https://github.com/Itheum/sdk-mx-data-nft/commit/0bffa10c3b02e8e8d7bcdad587a27cdc089212bd)) 18 | * override dataMarshal based on overrideList ([bc6220d](https://github.com/Itheum/sdk-mx-data-nft/commit/bc6220de68990b12ca96507c132f92b18851a93f)) 19 | 20 | # Change Log 21 | 22 | All notable changes will be documented in this file. 23 | 24 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 25 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SDK MX - Data NFT 2 | 3 | [![npm (scoped)](https://img.shields.io/npm/v/@itheum/sdk-mx-data-nft?style=for-the-badge)](https://www.npmjs.com/package/@itheum/sdk-mx-data-nft) 4 | 5 | This SDK is currently focused on interacting with the Itheum's Data NFT technology that's deployed to the MultiversX blockchain. 6 | 7 | ## Contributing 8 | 9 | - requires `node@19.7X` 10 | - `npm install` 11 | - work on typescript code in the `/src` folder 12 | - handy tip: when developing locally, you can do integration tests by running `npm run prepare` and the `npm install --save ../sdk-mx-data-nft` in the host project 13 | 14 | ### Dev Environment 15 | 16 | - create a '.husky' folder in the root of the project 17 | Inside the folder: 18 | - add a 'commit-msg' file with the following content: 19 | ```bash 20 | #!/usr/bin/env sh 21 | . "$(dirname -- "$0")/_/husky.sh" 22 | npx --no-install commitlint --edit $1 23 | ``` 24 | - add a 'pre-commit' file with the following content: 25 | ```bash 26 | #!/usr/bin/env sh 27 | . "$(dirname -- "$0")/_/husky.sh" 28 | npm test 29 | ``` 30 | 31 | ### Dev Testing 32 | 33 | - Only simple dev testing added. First **Build** as below and then run `npm run test` and work on the test.mjs file for live reload 34 | 35 | ### Build 36 | 37 | - `npm run build` 38 | - New build is sent to `dist` folder 39 | 40 | ## Usage in 3rd party dApps 41 | 42 | - Install this SDK via `npm i @itheum/sdk-mx-data-nft` 43 | - Methods supported are given below is `SDK Docs` 44 | 45 | ## SDK DOCS 46 | 47 | Note that all param requirements and method docs are marked up in the typescript code, so if you use typescript in your project your development tool (e.g. Visual Studio Code) will provide intellisense for all methods and functions. 48 | 49 | ### 1. Interacting with Data NFTs 50 | 51 | ```typescript 52 | import { DataNft } from '@itheum/sdk-mx-data-nft'; 53 | 54 | DataNft.setNetworkConfig('devnet' | 'testnet' | 'mainnet'); 55 | 56 | // Can build a new DataNft object partially 57 | const dataNft = new DataNft({ 58 | tokenIdentifier: 'tokenIdentifier', 59 | tokenName: 'tokenName' 60 | }); 61 | 62 | // Create a new DataNft object from API 63 | const nonce = 1; 64 | const nft = await DataNft.createFromApi({ nonce }); 65 | 66 | // Create a new DataNft object from API Response 67 | const response = await fetch('https://devnet-api.multiversx.com/address/nfts'); 68 | const dataNfts = []; 69 | 70 | response.forEach(async (nft) => { 71 | const data = await DataNft.createFromApiResponse(nft); 72 | dataNfts.push(data); 73 | }); 74 | 75 | // Retrieves the DataNfts owned by a address 76 | const address = 'address'; 77 | const dataNfts = []; 78 | dataNfts = await DataNft.ownedByAddress(address); 79 | 80 | // Retrieves the specific DataNft 81 | const dataNft = DataNft.createFromApi({ nonce }); 82 | 83 | // (A) Get a message from the Data Marshal node for your to sign to prove ownership 84 | const message = await dataNft.messageToSign(); 85 | 86 | // (B) Sign the message with a wallet and obtain a signature 87 | const signature = 'signature'; 88 | 89 | // There are 2 methods to open a data NFT and view the content --> 90 | 91 | // Method 1) Unlock the data inside the Data NFT via signature verification 92 | dataNft.viewData({ 93 | message, 94 | signature 95 | }); // optional params "stream" (stream out data instead of downloading file), "fwdAllHeaders"/"fwdHeaderKeys", "fwdHeaderMapLookup" can be used to pass headers like Authorization to origin Data Stream servers 96 | 97 | 98 | // Method 2) OR, you can use a MultiversX Native Auth access token to unlock the data inside the Data NFT without the need for the the signature steps above (A)(B). This has a much better UX 99 | dataNft.viewDataViaMVXNativeAuth({ 100 | mvxNativeAuthOrigins: "http://localhost:3000", "https://mycoolsite.com"], // same whitelist domains your client app used when generating native auth token 101 | mvxNativeAuthMaxExpirySeconds: 300, // same expiry seconds your client app used when generating native auth token 102 | fwdHeaderMapLookup: { 103 | authorization : "Bearer myNativeAuthToken" 104 | } 105 | }); // optional params "stream" (stream out data instead of downloading file), "fwdAllHeaders"/"fwdHeaderKeys" can be used to pass on the headers like Authorization to origin Data Stream servers 106 | ``` 107 | 108 | ### 2. Interacting with Data NFT Minter 109 | 110 | ```typescript 111 | import { SftMinter } from '@itheum/sdk-mx-data-nft'; 112 | 113 | const dataNftMinter = new SftMinter('devnet' | 'testnet' | 'mainnet'); 114 | 115 | // View minter smart contract requirements 116 | const requirements = await dataNftMinter.viewMinterRequirements( 117 | new Address('erd1...') 118 | ); 119 | 120 | // View contract pause state 121 | const result = await dataNftMinter.viewContractPauseState(); 122 | ``` 123 | 124 | #### Create a mint transaction 125 | 126 | Method 1: Mint a new Data NFT with Ithuem generated image and traits. 127 | Currently only supports [nft.storage](https://nft.storage/docs/quickstart/#get-an-api-token). 128 | 129 | ```typescript 130 | const transaction = await nftMinter.mint( 131 | new Address('erd1...'), 132 | 'TEST-TOKEN', 133 | 'https://marshal.com', 134 | 'https://streamdata.com', 135 | 'https://previewdata', 136 | 1000, 137 | 'Test Title', 138 | 'Test Description', 139 | { 140 | nftStorageToken: 'API TOKEN' 141 | } 142 | ); 143 | ``` 144 | 145 | Method 2: Mint a new Data NFT with custom image and traits. 146 | Traits should be compliant with the Itheum [traits structure](#traits-structure). 147 | 148 | ```typescript 149 | const transaction = await nftMinter.mint( 150 | new Address('erd1'), 151 | 'TEST-TOKEN', 152 | 'https://marshal.com', 153 | 'https://streamdata.com', 154 | 'https://previewdata', 155 | 1000, 156 | 'Test Title', 157 | 'Test Description', 158 | { 159 | imageUrl: 'https://imageurl.com', 160 | traitsUrl: 'https://traitsurl.com' 161 | } 162 | ); 163 | ``` 164 | 165 | #### Create a burn transaction 166 | 167 | ```typescript 168 | const transaction = await dataNftMarket.burn( 169 | new Address('erd1'), 170 | dataNftNonce, 171 | quantityToBurn 172 | ); 173 | ``` 174 | 175 | ### 3. Interacting with Data NFT Marketplace 176 | 177 | ```typescript 178 | import { DataNftMarket } from '@itheum/sdk-mx-data-nft'; 179 | 180 | const dataNftMarket = new DataNftMarket('devnet' | 'testnet' | 'mainnet'); 181 | 182 | // View requirements 183 | const result = await dataNftMarket.viewRequirements(); 184 | 185 | // View address listed offers 186 | const result = await dataNftMarket.viewAddressListedOffers(new Address('')); 187 | 188 | // View address paged offers 189 | const result = await dataNftMarket.viewAddressPagedOffers( 190 | 1, 191 | 10, 192 | new Address('') 193 | ); 194 | 195 | // View address total offers 196 | const result = await dataNftMarket.viewAddressTotalOffers(new Address('')); 197 | 198 | // View address cancelled offers 199 | const result = await dataNftMarket.viewAddressCancelledOffers(new Address('')); 200 | 201 | // View offers paged 202 | const result = await dataNftMarket.viewPagedOffers(1, 10); 203 | 204 | // View offers 205 | const result = await dataNftMarket.viewOffers(); 206 | 207 | // View number of offers listed 208 | const result = await dataNftMarket.viewNumberOfOffers(); 209 | 210 | // View contract pause state 211 | const result = await dataNftMarket.viewContractPauseState(); 212 | 213 | // View last valid offer id 214 | const result = await dataNftMarket.viewLastValidOfferId(); 215 | 216 | // Create addOffer transaction 217 | const result = dataNftMarket.addOffer(new Address(''), '', 0, 0, '', 0, 0, 0); 218 | 219 | // Create acceptOffer transaction 220 | const result = dataNftMarket.acceptOffer(new Address(''), 0, 0, 0); 221 | 222 | // Create cancelOffer transaction 223 | const result = dataNftMarket.cancelOffer(new Address(''), 0); 224 | 225 | // Create cancelOffer transaction without sending the funds back to the owner 226 | const result = dataNftMarket.cancelOffer(new Address(''), 0, false); 227 | 228 | // Create withdrawFromCancelledOffer transaction 229 | const result = dataNftMarket.withdrawCancelledOffer(new Address(''), 0); 230 | 231 | // Create changeOfferPrice transaction 232 | const result = dataNftMarket.changeOfferPrice(new Address(''), 0, 0); 233 | ``` 234 | 235 | ### Traits structure 236 | 237 | Items below marked "required" are the "minimum" required for it to be compatible with the Itheum protocol. You can add any additional traits you may need for your own reasons. 238 | 239 | ```json 240 | { 241 | "description": "Data NFT description", // required 242 | "data_preview_url": "https://previewdata.com", 243 | "attributes": [ 244 | { 245 | "trait_type": "Creator", // required 246 | "value": "creator address" 247 | }, 248 | { 249 | "trait_type": "extra trait", 250 | "value": "extra trait value" 251 | }, 252 | ... 253 | ] 254 | } 255 | ``` 256 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { '^.+\\.ts?$': 'ts-jest' }, 3 | testEnvironment: 'node', 4 | testRegex: '/tests/.*\\.(test|spec)?\\.(ts|tsx)$', 5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 6 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@itheum/sdk-mx-data-nft", 3 | "version": "3.6.1", 4 | "description": "SDK for Itheum's Data NFT Technology on MultiversX Blockchain", 5 | "main": "out/index.js", 6 | "types": "out/index.d.js", 7 | "files": [ 8 | "out/**/*" 9 | ], 10 | "scripts": { 11 | "test": "jest --runInBand --forceExit", 12 | "lint": "tslint --project .", 13 | "lint:fix": "tslint --project . --fix", 14 | "build": "tsc -p tsconfig.json", 15 | "prepare": "npm run build && husky" 16 | }, 17 | "author": "Itheum Protocol", 18 | "license": "GPL-3.0-only", 19 | "dependencies": { 20 | "@multiversx/sdk-core": "13.2.2", 21 | "@multiversx/sdk-network-providers": "2.4.3", 22 | "bignumber.js": "9.1.2", 23 | "nft.storage": "7.2.0" 24 | }, 25 | "devDependencies": { 26 | "@commitlint/config-conventional": "19.2.2", 27 | "@semantic-release/changelog": "6.0.3", 28 | "@semantic-release/commit-analyzer": "13.0.0", 29 | "@semantic-release/git": "10.0.1", 30 | "@semantic-release/github": "10.1.3", 31 | "@semantic-release/npm": "12.0.1", 32 | "@semantic-release/release-notes-generator": "14.0.1", 33 | "@types/jest": "29.5.12", 34 | "commitlint": "19.3.0", 35 | "husky": "9.1.4", 36 | "jest": "29.7.0", 37 | "semantic-release": "24.0.0", 38 | "ts-jest": "29.2.3", 39 | "tslint": "6.1.3", 40 | "typedoc": "0.26.5", 41 | "typescript": "5.5.4" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/Itheum/sdk-mx-data-nft" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ['main', 'develop'], 3 | plugins: [ 4 | '@semantic-release/commit-analyzer', 5 | '@semantic-release/release-notes-generator', 6 | [ 7 | '@semantic-release/changelog', 8 | { 9 | changelogFile: 'CHANGELOG.md' 10 | } 11 | ], 12 | '@semantic-release/npm', 13 | '@semantic-release/github', 14 | [ 15 | '@semantic-release/git', 16 | { 17 | assets: ['CHANGELOG.md'], 18 | message: 'chore(release): set `package.json` to ${nextRelease.version}' 19 | } 20 | ] 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /src/abis/core-mx-liveliness-stake.abi.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildInfo": { 3 | "rustc": { 4 | "version": "1.78.0", 5 | "commitHash": "9b00956e56009bab2aa15d7bff10916599e3d6d6", 6 | "commitDate": "2024-04-29", 7 | "channel": "Stable", 8 | "short": "rustc 1.78.0 (9b00956e5 2024-04-29)" 9 | }, 10 | "contractCrate": { 11 | "name": "core-mx-liveliness-stake", 12 | "version": "0.0.0" 13 | }, 14 | "framework": { 15 | "name": "multiversx-sc", 16 | "version": "0.51.1" 17 | } 18 | }, 19 | "name": "CoreMxLivelinessStake", 20 | "constructor": { 21 | "inputs": [], 22 | "outputs": [] 23 | }, 24 | "upgradeConstructor": { 25 | "inputs": [], 26 | "outputs": [] 27 | }, 28 | "endpoints": [ 29 | { 30 | "name": "claimRewards", 31 | "mutability": "mutable", 32 | "inputs": [ 33 | { 34 | "name": "address", 35 | "type": "optional
", 36 | "multi_arg": true 37 | } 38 | ], 39 | "outputs": [] 40 | }, 41 | { 42 | "name": "setAddressRewardsPerShare", 43 | "mutability": "mutable", 44 | "inputs": [ 45 | { 46 | "name": "address", 47 | "type": "Address" 48 | } 49 | ], 50 | "outputs": [] 51 | }, 52 | { 53 | "name": "stakeRewards", 54 | "mutability": "mutable", 55 | "inputs": [ 56 | { 57 | "name": "token_identifier", 58 | "type": "TokenIdentifier" 59 | } 60 | ], 61 | "outputs": [] 62 | }, 63 | { 64 | "name": "setContractStateActive", 65 | "mutability": "mutable", 66 | "inputs": [], 67 | "outputs": [] 68 | }, 69 | { 70 | "name": "setContractStateInactive", 71 | "mutability": "mutable", 72 | "inputs": [], 73 | "outputs": [] 74 | }, 75 | { 76 | "name": "setMaxApr", 77 | "mutability": "mutable", 78 | "inputs": [ 79 | { 80 | "name": "max_apr", 81 | "type": "BigUint" 82 | } 83 | ], 84 | "outputs": [] 85 | }, 86 | { 87 | "name": "setRewardsTokenIdentifier", 88 | "mutability": "mutable", 89 | "inputs": [ 90 | { 91 | "name": "token_identifier", 92 | "type": "TokenIdentifier" 93 | } 94 | ], 95 | "outputs": [] 96 | }, 97 | { 98 | "name": "setPerBlockRewardAmount", 99 | "mutability": "mutable", 100 | "inputs": [ 101 | { 102 | "name": "per_block_amount", 103 | "type": "BigUint" 104 | } 105 | ], 106 | "outputs": [] 107 | }, 108 | { 109 | "name": "topUpRewards", 110 | "mutability": "mutable", 111 | "payableInTokens": [ 112 | "*" 113 | ], 114 | "inputs": [], 115 | "outputs": [] 116 | }, 117 | { 118 | "name": "withdrawRewards", 119 | "mutability": "mutable", 120 | "inputs": [ 121 | { 122 | "name": "amount", 123 | "type": "BigUint" 124 | } 125 | ], 126 | "outputs": [] 127 | }, 128 | { 129 | "name": "startProduceRewards", 130 | "mutability": "mutable", 131 | "inputs": [], 132 | "outputs": [] 133 | }, 134 | { 135 | "name": "endProduceRewards", 136 | "mutability": "mutable", 137 | "inputs": [], 138 | "outputs": [] 139 | }, 140 | { 141 | "name": "setBondContractAddress", 142 | "mutability": "mutable", 143 | "inputs": [ 144 | { 145 | "name": "bond_contract_address", 146 | "type": "Address" 147 | } 148 | ], 149 | "outputs": [] 150 | }, 151 | { 152 | "name": "setAdministrator", 153 | "onlyOwner": true, 154 | "mutability": "mutable", 155 | "inputs": [ 156 | { 157 | "name": "administrator", 158 | "type": "Address" 159 | } 160 | ], 161 | "outputs": [] 162 | }, 163 | { 164 | "name": "getContractState", 165 | "mutability": "readonly", 166 | "inputs": [], 167 | "outputs": [ 168 | { 169 | "type": "State" 170 | } 171 | ] 172 | }, 173 | { 174 | "name": "rewardsState", 175 | "mutability": "readonly", 176 | "inputs": [], 177 | "outputs": [ 178 | { 179 | "type": "State" 180 | } 181 | ] 182 | }, 183 | { 184 | "name": "getAdministrator", 185 | "mutability": "readonly", 186 | "inputs": [], 187 | "outputs": [ 188 | { 189 | "type": "Address" 190 | } 191 | ] 192 | }, 193 | { 194 | "name": "bondContractAddress", 195 | "mutability": "readonly", 196 | "inputs": [], 197 | "outputs": [ 198 | { 199 | "type": "Address" 200 | } 201 | ] 202 | }, 203 | { 204 | "name": "generateAggregatedRewards", 205 | "mutability": "mutable", 206 | "inputs": [], 207 | "outputs": [] 208 | }, 209 | { 210 | "name": "rewardsReserve", 211 | "mutability": "readonly", 212 | "inputs": [], 213 | "outputs": [ 214 | { 215 | "type": "BigUint" 216 | } 217 | ] 218 | }, 219 | { 220 | "name": "accumulatedRewards", 221 | "mutability": "readonly", 222 | "inputs": [], 223 | "outputs": [ 224 | { 225 | "type": "BigUint" 226 | } 227 | ] 228 | }, 229 | { 230 | "name": "rewardsTokenIdentifier", 231 | "mutability": "readonly", 232 | "inputs": [], 233 | "outputs": [ 234 | { 235 | "type": "TokenIdentifier" 236 | } 237 | ] 238 | }, 239 | { 240 | "name": "rewardsPerBlock", 241 | "mutability": "readonly", 242 | "inputs": [], 243 | "outputs": [ 244 | { 245 | "type": "BigUint" 246 | } 247 | ] 248 | }, 249 | { 250 | "name": "lastRewardBlockNonce", 251 | "mutability": "readonly", 252 | "inputs": [], 253 | "outputs": [ 254 | { 255 | "type": "u64" 256 | } 257 | ] 258 | }, 259 | { 260 | "name": "rewardsPerShare", 261 | "mutability": "readonly", 262 | "inputs": [], 263 | "outputs": [ 264 | { 265 | "type": "BigUint" 266 | } 267 | ] 268 | }, 269 | { 270 | "name": "addressLastRewardPerShare", 271 | "mutability": "readonly", 272 | "inputs": [ 273 | { 274 | "name": "address", 275 | "type": "Address" 276 | } 277 | ], 278 | "outputs": [ 279 | { 280 | "type": "BigUint" 281 | } 282 | ] 283 | }, 284 | { 285 | "name": "maxApr", 286 | "mutability": "readonly", 287 | "inputs": [], 288 | "outputs": [ 289 | { 290 | "type": "BigUint" 291 | } 292 | ] 293 | }, 294 | { 295 | "name": "claimableRewards", 296 | "mutability": "readonly", 297 | "inputs": [ 298 | { 299 | "name": "caller", 300 | "type": "Address" 301 | }, 302 | { 303 | "name": "opt_bypass_liveliness", 304 | "type": "Option" 305 | } 306 | ], 307 | "outputs": [ 308 | { 309 | "type": "BigUint" 310 | } 311 | ] 312 | }, 313 | { 314 | "name": "contractDetails", 315 | "mutability": "readonly", 316 | "inputs": [], 317 | "outputs": [ 318 | { 319 | "type": "ContractDetails" 320 | } 321 | ] 322 | }, 323 | { 324 | "name": "userDataOut", 325 | "mutability": "readonly", 326 | "inputs": [ 327 | { 328 | "name": "address", 329 | "type": "Address" 330 | }, 331 | { 332 | "name": "token_identifier", 333 | "type": "TokenIdentifier" 334 | } 335 | ], 336 | "outputs": [ 337 | { 338 | "type": "tuple" 339 | } 340 | ] 341 | } 342 | ], 343 | "events": [ 344 | { 345 | "identifier": "set_administrator_event", 346 | "inputs": [ 347 | { 348 | "name": "administrator", 349 | "type": "Address", 350 | "indexed": true 351 | } 352 | ] 353 | }, 354 | { 355 | "identifier": "contract_state_event", 356 | "inputs": [ 357 | { 358 | "name": "state", 359 | "type": "State", 360 | "indexed": true 361 | } 362 | ] 363 | }, 364 | { 365 | "identifier": "max_apr", 366 | "inputs": [ 367 | { 368 | "name": "max_apr", 369 | "type": "BigUint", 370 | "indexed": true 371 | } 372 | ] 373 | }, 374 | { 375 | "identifier": "rewards_token_identifier", 376 | "inputs": [ 377 | { 378 | "name": "token_identifier", 379 | "type": "TokenIdentifier", 380 | "indexed": true 381 | } 382 | ] 383 | }, 384 | { 385 | "identifier": "per_block_reward_amount", 386 | "inputs": [ 387 | { 388 | "name": "per_block_amount", 389 | "type": "BigUint", 390 | "indexed": true 391 | } 392 | ] 393 | }, 394 | { 395 | "identifier": "top_up_rewards_event", 396 | "inputs": [ 397 | { 398 | "name": "amount", 399 | "type": "BigUint", 400 | "indexed": true 401 | } 402 | ] 403 | }, 404 | { 405 | "identifier": "withdraw_rewards_event", 406 | "inputs": [ 407 | { 408 | "name": "amount", 409 | "type": "BigUint", 410 | "indexed": true 411 | } 412 | ] 413 | }, 414 | { 415 | "identifier": "rewards_state_event", 416 | "inputs": [ 417 | { 418 | "name": "state", 419 | "type": "State", 420 | "indexed": true 421 | } 422 | ] 423 | }, 424 | { 425 | "identifier": "bond_contract_address", 426 | "inputs": [ 427 | { 428 | "name": "bond_contract_address", 429 | "type": "Address", 430 | "indexed": true 431 | } 432 | ] 433 | }, 434 | { 435 | "identifier": "claim_rewards", 436 | "inputs": [ 437 | { 438 | "name": "caller", 439 | "type": "Address", 440 | "indexed": true 441 | }, 442 | { 443 | "name": "rewards_amount", 444 | "type": "BigUint", 445 | "indexed": true 446 | }, 447 | { 448 | "name": "timestamp", 449 | "type": "u64", 450 | "indexed": true 451 | }, 452 | { 453 | "name": "block_nonce", 454 | "type": "u64", 455 | "indexed": true 456 | }, 457 | { 458 | "name": "rewards_reserve", 459 | "type": "BigUint", 460 | "indexed": true 461 | }, 462 | { 463 | "name": "accumulated_rewards", 464 | "type": "BigUint", 465 | "indexed": true 466 | }, 467 | { 468 | "name": "current_rewards_per_share", 469 | "type": "BigUint", 470 | "indexed": true 471 | }, 472 | { 473 | "name": "user_last_rewards_per_share", 474 | "type": "BigUint", 475 | "indexed": true 476 | }, 477 | { 478 | "name": "rewards_per_block", 479 | "type": "BigUint", 480 | "indexed": true 481 | } 482 | ] 483 | }, 484 | { 485 | "identifier": "address_rewards_per_share_event", 486 | "inputs": [ 487 | { 488 | "name": "address", 489 | "type": "Address", 490 | "indexed": true 491 | }, 492 | { 493 | "name": "rewards_per_share", 494 | "type": "BigUint", 495 | "indexed": true 496 | } 497 | ] 498 | } 499 | ], 500 | "esdtAttributes": [], 501 | "hasCallback": false, 502 | "types": { 503 | "ContractDetails": { 504 | "type": "struct", 505 | "fields": [ 506 | { 507 | "name": "rewards_reserve", 508 | "type": "BigUint" 509 | }, 510 | { 511 | "name": "accumulated_rewards", 512 | "type": "BigUint" 513 | }, 514 | { 515 | "name": "rewards_token_identifier", 516 | "type": "TokenIdentifier" 517 | }, 518 | { 519 | "name": "rewards_per_block", 520 | "type": "BigUint" 521 | }, 522 | { 523 | "name": "rewards_per_share", 524 | "type": "BigUint" 525 | }, 526 | { 527 | "name": "administrator", 528 | "type": "Address" 529 | }, 530 | { 531 | "name": "bond_contract_address", 532 | "type": "Address" 533 | }, 534 | { 535 | "name": "last_reward_block_nonce", 536 | "type": "u64" 537 | }, 538 | { 539 | "name": "max_apr", 540 | "type": "BigUint" 541 | } 542 | ] 543 | }, 544 | "State": { 545 | "type": "enum", 546 | "variants": [ 547 | { 548 | "name": "Inactive", 549 | "discriminant": 0 550 | }, 551 | { 552 | "name": "Active", 553 | "discriminant": 1 554 | } 555 | ] 556 | }, 557 | "UserData": { 558 | "type": "struct", 559 | "fields": [ 560 | { 561 | "name": "total_staked_amount", 562 | "type": "BigUint" 563 | }, 564 | { 565 | "name": "user_staked_amount", 566 | "type": "BigUint" 567 | }, 568 | { 569 | "name": "liveliness_score", 570 | "type": "BigUint" 571 | }, 572 | { 573 | "name": "accumulated_rewards", 574 | "type": "BigUint" 575 | }, 576 | { 577 | "name": "accumulated_rewards_bypass", 578 | "type": "BigUint" 579 | }, 580 | { 581 | "name": "vault_nonce", 582 | "type": "u64" 583 | } 584 | ] 585 | } 586 | } 587 | } 588 | -------------------------------------------------------------------------------- /src/common/mint-utils.ts: -------------------------------------------------------------------------------- 1 | import { File, NFTStorage } from 'nft.storage'; 2 | 3 | export async function dataNFTDataStreamAdvertise( 4 | dataNFTStreamUrl: string, 5 | dataMarshalUrl: string, 6 | dataCreatorAddress: string 7 | ): Promise<{ dataNftHash: string; dataNftStreamUrlEncrypted: string }> { 8 | const myHeaders = new Headers(); 9 | myHeaders.append('cache-control', 'no-cache'); 10 | myHeaders.append('Content-Type', 'application/json'); 11 | 12 | const requestOptions = { 13 | method: 'POST', 14 | headers: myHeaders, 15 | body: JSON.stringify({ 16 | dataNFTStreamUrl, 17 | dataCreatorERDAddress: dataCreatorAddress 18 | }) 19 | }; 20 | 21 | try { 22 | const res = await fetch(`${dataMarshalUrl}/generate_V2`, requestOptions); 23 | const data = await res.json(); 24 | 25 | if (data && data.encryptedMessage && data.messageHash) { 26 | return { 27 | dataNftHash: data.messageHash, 28 | dataNftStreamUrlEncrypted: data.encryptedMessage 29 | }; 30 | } else { 31 | throw new Error('Issue with data marshal generating payload'); 32 | } 33 | } catch (error) { 34 | throw error; 35 | } 36 | } 37 | 38 | export async function storeToIpfs( 39 | storageToken: string, 40 | traits: object, 41 | image: Blob 42 | ): Promise<{ imageOnIpfsUrl: string; metadataOnIpfsUrl: string }> { 43 | try { 44 | const imageHash = await storeImageToIpfs(image, storageToken); 45 | const traitsHash = await storeTraitsToIpfs(traits, storageToken); 46 | return { 47 | imageOnIpfsUrl: `https://ipfs.io/ipfs/${imageHash}`, 48 | metadataOnIpfsUrl: `https://ipfs.io/ipfs/${traitsHash}` 49 | }; 50 | } catch (error) { 51 | throw error; 52 | } 53 | } 54 | 55 | async function storeImageToIpfs(image: Blob, storageToken: string) { 56 | const form = new FormData(); 57 | form.append('file', image); 58 | form.append('pinataMetadata', '{\n "name": "image"\n}'); 59 | form.append('pinataOptions', '{\n "cidVersion": 0\n}'); 60 | 61 | const options = { 62 | method: 'POST', 63 | headers: { 64 | Authorization: `Bearer ${storageToken}` 65 | }, 66 | body: form 67 | }; 68 | const response = await fetch( 69 | 'https://api.pinata.cloud/pinning/pinFileToIPFS', 70 | options 71 | ); 72 | const res = await response.json(); 73 | const imageHash = res.IpfsHash; 74 | return imageHash; 75 | } 76 | 77 | async function storeTraitsToIpfs(traits: object, storageToken: string) { 78 | const options = { 79 | method: 'POST', 80 | headers: { 81 | Authorization: `Bearer ${storageToken}`, 82 | 'Content-Type': 'application/json' 83 | }, 84 | body: JSON.stringify({ 85 | pinataOptions: { cidVersion: 0 }, 86 | pinataMetadata: { name: 'metadata' }, 87 | pinataContent: traits 88 | }) 89 | }; 90 | 91 | const response = await fetch( 92 | 'https://api.pinata.cloud/pinning/pinJSONToIPFS', 93 | options 94 | ); 95 | const res = await response.json(); 96 | const traitsHash = res.IpfsHash; 97 | return traitsHash; 98 | } 99 | 100 | export function createIpfsMetadata( 101 | traits: string, 102 | datasetTitle: string, 103 | datasetDescription: string, 104 | dataNFTStreamPreviewUrl: string, 105 | address: string, 106 | extraAssets: string[] 107 | ) { 108 | const metadata: Record = { 109 | description: `${datasetTitle} : ${datasetDescription}`, 110 | data_preview_url: dataNFTStreamPreviewUrl, 111 | attributes: [] as object[] 112 | }; 113 | if (extraAssets && extraAssets.length > 0) { 114 | metadata.extra_assets = extraAssets; 115 | } 116 | const attributes = traits 117 | .split(',') 118 | .filter((element) => element.trim() !== ''); 119 | const metadataAttributes = []; 120 | for (const attribute of attributes) { 121 | const [key, value] = attribute.split(':'); 122 | const trait = { trait_type: key.trim(), value: value.trim() }; 123 | metadataAttributes.push(trait); 124 | } 125 | metadataAttributes.push({ trait_type: 'Creator', value: address }); 126 | metadata.attributes = metadataAttributes; 127 | return metadata; 128 | } 129 | 130 | export async function createFileFromUrl( 131 | url: string, 132 | datasetTitle: string, 133 | datasetDescription: string, 134 | dataNFTStreamPreviewUrl: string, 135 | address: string, 136 | extraAssets: string[] 137 | ) { 138 | let res: any = ''; 139 | let data: any = ''; 140 | let _imageFile: Blob = new Blob(); 141 | if (url) { 142 | res = await fetch(url); 143 | data = await res.blob(); 144 | _imageFile = data; 145 | } 146 | const traits = createIpfsMetadata( 147 | res.headers.get('x-nft-traits') || '', 148 | datasetTitle, 149 | datasetDescription, 150 | dataNFTStreamPreviewUrl, 151 | address, 152 | extraAssets 153 | ); 154 | const _traitsFile = traits; 155 | return { image: _imageFile, traits: _traitsFile }; 156 | } 157 | -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { DataNft } from '../datanft'; 3 | import { 4 | ErrFetch, 5 | ErrInvalidTokenIdentifier, 6 | ErrMissingTrait, 7 | ErrMissingValueForTrait, 8 | ErrParseNft 9 | } from '../errors'; 10 | import { 11 | Bond, 12 | BondConfiguration, 13 | Compensation, 14 | ContractConfiguration, 15 | LivelinessStakeConfiguration, 16 | NftEnumType, 17 | NftType, 18 | Offer, 19 | Refund, 20 | State, 21 | UserData 22 | } from '../interfaces'; 23 | import { EnvironmentsEnum, dataMarshalUrlOverride } from '../config'; 24 | 25 | export function numberToPaddedHex(value: BigNumber.Value) { 26 | let hex = new BigNumber(value).toString(16); 27 | return zeroPadStringIfOddLength(hex); 28 | } 29 | 30 | /** 31 | * Creates a token identifier from a collection and a nonce 32 | * @param collection The collection of the token 33 | * @param nonce the nonce of the token 34 | * @returns The token identifier in the format of ticker-randomString-nonce 35 | */ 36 | export function createTokenIdentifier( 37 | collection: string, 38 | nonce: BigNumber.Value 39 | ) { 40 | return `${collection}-${numberToPaddedHex(nonce)}`; 41 | } 42 | 43 | /** 44 | * Creates the collection and nonce from a token identifier 45 | * @param tokenIdentifier The token identifier in the format of ticker-randomString-nonce 46 | * @returns The collection and nonce of the token 47 | */ 48 | export function parseTokenIdentifier(tokenIdentifier: string): { 49 | collection: string; 50 | nonce: BigNumber.Value; 51 | } { 52 | const splitTokenIdentifier: string[] = tokenIdentifier.split('-'); 53 | 54 | if (splitTokenIdentifier.length !== 3) { 55 | throw new ErrInvalidTokenIdentifier(); 56 | } 57 | return { 58 | collection: `${splitTokenIdentifier[0]}-${splitTokenIdentifier[1]}`, 59 | nonce: parseInt(splitTokenIdentifier[2], 16) 60 | }; 61 | } 62 | 63 | export function isPaddedHex(input: string) { 64 | input = input || ''; 65 | let decodedThenEncoded = Buffer.from(input, 'hex').toString('hex'); 66 | return input.toUpperCase() == decodedThenEncoded.toUpperCase(); 67 | } 68 | 69 | export function zeroPadStringIfOddLength(input: string): string { 70 | input = input || ''; 71 | 72 | if (input.length % 2 == 1) { 73 | return '0' + input; 74 | } 75 | 76 | return input; 77 | } 78 | 79 | export function parseOffer(value: any): Offer { 80 | return { 81 | index: value.offer_id.toNumber(), 82 | owner: value.owner.toString(), 83 | offeredTokenIdentifier: value.offered_token_identifier.toString(), 84 | offeredTokenNonce: value.offered_token_nonce.toString(), 85 | offeredTokenAmount: value.offered_token_amount.toFixed(0), 86 | wantedTokenIdentifier: value.wanted_token_identifier.toString(), 87 | wantedTokenNonce: value.wanted_token_nonce.toString(), 88 | wantedTokenAmount: value.wanted_token_amount.toFixed(0), 89 | quantity: value.quantity.toNumber(), 90 | maxQuantityPerAddress: value.max_quantity_per_address.toNumber() 91 | }; 92 | } 93 | 94 | export function parseBond(value: any): Bond { 95 | return { 96 | bondId: value.bond_id.toNumber(), 97 | address: value.address.toString(), 98 | tokenIdentifier: value.token_identifier.toString(), 99 | nonce: value.nonce.toNumber(), 100 | lockPeriod: value.lock_period.toNumber(), 101 | bondTimestamp: value.bond_timestamp.toNumber(), 102 | unbondTimestamp: value.unbond_timestamp.toNumber(), 103 | bondAmount: value.bond_amount.toFixed(0), 104 | remainingAmount: value.remaining_amount.toFixed(0) 105 | }; 106 | } 107 | 108 | export function parseBondConfiguration(value: any): BondConfiguration { 109 | const lockPeriods: BigNumber[] = value.lock_periods; 110 | const bondAmounts: BigNumber[] = value.bond_amounts; 111 | 112 | // Construct array of objects containing lock period and bond amount 113 | const result: { lockPeriod: number; amount: BigNumber.Value }[] = []; 114 | for (let i = 0; i < lockPeriods.length; i++) { 115 | const lockPeriod = lockPeriods[i].toNumber(); 116 | const bondAmount = bondAmounts[i].toFixed(0); 117 | result.push({ lockPeriod: lockPeriod, amount: bondAmount }); 118 | } 119 | return { 120 | contractState: value.contract_state.name as State, 121 | bondPaymentTokenIdentifier: value.bond_payment_token_identifier.toString(), 122 | lockPeriodsWithBonds: result, 123 | minimumPenalty: value.minimum_penalty.toNumber(), 124 | maximumPenalty: value.maximum_penalty.toNumber(), 125 | withdrawPenalty: value.withdraw_penalty.toNumber(), 126 | acceptedCallers: value.accepted_callers.map((address: any) => 127 | address.toString() 128 | ) 129 | }; 130 | } 131 | 132 | export function parseLivelinessStakeConfiguration( 133 | value: any 134 | ): LivelinessStakeConfiguration { 135 | return { 136 | rewardsPerBlock: BigNumber(value.rewards_per_block), 137 | rewardsReserve: BigNumber(value.rewards_reserve), 138 | accumulatedRewards: BigNumber(value.accumulated_rewards), 139 | rewardsTokenIdentifier: value.rewards_token_identifier.toString(), 140 | lastRewardBlockNonce: value.last_reward_block_nonce.toNumber(), 141 | maxApr: BigNumber(value.max_apr).div(100).toNumber(), 142 | administrator: value.administrator.toString(), 143 | bondContractAddress: value.bond_contract_address.toString() 144 | }; 145 | } 146 | 147 | export function parseUserData(value: any): UserData { 148 | return { 149 | totalStakedAmount: BigNumber(value.total_staked_amount), 150 | userStakedAmount: BigNumber(value.user_staked_amount), 151 | livelinessScore: BigNumber(value.liveliness_score).div(100).toNumber(), 152 | accumulatedRewards: BigNumber(value.accumulated_rewards), 153 | accumulatedRewardsBypass: BigNumber(value.accumulated_rewards_bypass), 154 | vaultNonce: value.vault_nonce.toNumber() 155 | }; 156 | } 157 | 158 | export function parseCompensation(value: any): Compensation { 159 | return { 160 | compensationId: value.compensation_id.toNumber(), 161 | tokenIdentifier: value.token_identifier.toString(), 162 | nonce: value.nonce.toNumber(), 163 | accumulatedAmount: value.accumulated_amount.toFixed(0), 164 | proofAmount: value.proof_amount.toFixed(0), 165 | endDate: value.end_date.toNumber() 166 | }; 167 | } 168 | 169 | export function parseRefund(value: any): Refund { 170 | return { 171 | compensationId: value.compensation_id.toNumber(), 172 | address: value.address.toString(), 173 | proofOfRefund: { 174 | tokenIdentifier: value.proof_of_refund.token_identifier.toString(), 175 | nonce: value.proof_of_refund.token_nonce.toNumber(), 176 | amount: value.proof_of_refund.amount.toFixed(0) 177 | } 178 | }; 179 | } 180 | 181 | export function parseDataNft(value: NftType): DataNft { 182 | let attributes; 183 | let metadataFile; 184 | 185 | try { 186 | attributes = DataNft.decodeAttributes(value.attributes); // normal attributes 187 | 188 | // get the metadata file, assume for now its the 2nd item. (1 = img, 2 = json, 3.... extra assets) 189 | metadataFile = value.uris?.[1]; 190 | 191 | if (metadataFile) { 192 | metadataFile = Buffer.from(metadataFile, 'base64').toString('ascii'); 193 | } 194 | } catch (error: any) { 195 | try { 196 | attributes = { 197 | dataPreview: value.metadata?.itheum_data_preview_url ?? '', 198 | dataStream: value.metadata?.itheum_data_stream_url ?? '', 199 | dataMarshal: value.metadata?.itheum_data_marshal_url ?? '', 200 | creator: value.metadata?.itheum_creator ?? '', 201 | creationTime: new Date(value.timestamp * 1000), 202 | description: value.metadata?.description ?? '', 203 | isDataNFTPH: true, 204 | title: value.name 205 | }; 206 | } catch (error: any) { 207 | throw new ErrParseNft(error.message); 208 | } 209 | } 210 | 211 | const returnValue = { 212 | tokenIdentifier: value.identifier, 213 | nftImgUrl: value.url ?? '', 214 | tokenName: value.name, 215 | supply: value.supply 216 | ? Number(value.supply) 217 | : value.type === NftEnumType.NonFungibleESDT 218 | ? 1 219 | : 0, 220 | type: value.type, 221 | royalties: value.royalties !== null ? value.royalties / 100 : 0, 222 | nonce: value.nonce, 223 | collection: value.collection, 224 | balance: value.balance ? Number(value.balance) : 0, 225 | owner: value.owner ? value.owner : '', 226 | extraAssets: 227 | value.uris 228 | ?.slice(2) 229 | .map((uri) => Buffer.from(uri, 'base64').toString('ascii')) ?? [], 230 | media: value.media, 231 | metadataFile, 232 | ...attributes 233 | }; 234 | 235 | return new DataNft(returnValue); 236 | } 237 | 238 | export async function checkTraitsUrl(traitsUrl: string) { 239 | const traitsResponse = await fetch(traitsUrl); 240 | const traits = await traitsResponse.json(); 241 | 242 | checkStatus(traitsResponse); 243 | 244 | if (!traits.description) { 245 | throw new ErrMissingTrait(traits.description); 246 | } 247 | 248 | if (!Array.isArray(traits.attributes)) { 249 | throw new ErrMissingTrait(traits.attributes); 250 | } 251 | 252 | const requiredTraits = ['Creator']; 253 | const traitsAttributes = traits.attributes; 254 | 255 | for (const requiredTrait of requiredTraits) { 256 | if ( 257 | !traitsAttributes.some( 258 | (attribute: any) => attribute.trait_type === requiredTrait 259 | ) 260 | ) { 261 | throw new ErrMissingTrait(requiredTrait); 262 | } 263 | } 264 | 265 | for (const attribute of traitsAttributes) { 266 | if (!attribute.value) { 267 | throw new ErrMissingValueForTrait(attribute.trait_type); 268 | } 269 | } 270 | } 271 | 272 | export function overrideMarshalUrl( 273 | env: string, 274 | tokenIdentifier: string, 275 | nonce: number 276 | ): { tokenIdentifier: string; nonce: number; url: string; chainId: string } { 277 | const overridUrlList: { 278 | tokenIdentifier: string; 279 | nonce: number; 280 | url: string; 281 | chainId: string; 282 | }[] = dataMarshalUrlOverride[env as EnvironmentsEnum]; 283 | 284 | if (overridUrlList) { 285 | const override = overridUrlList.find( 286 | (item) => item.tokenIdentifier === tokenIdentifier && item.nonce === nonce 287 | ); 288 | if (override) { 289 | return override; 290 | } else { 291 | return { tokenIdentifier: '', nonce: 0, url: '', chainId: '' }; 292 | } 293 | } else { 294 | return { tokenIdentifier: '', nonce: 0, url: '', chainId: '' }; 295 | } 296 | } 297 | 298 | export function validateSpecificParamsViewData(params: { 299 | signedMessage?: string | undefined; 300 | signableMessage?: any; 301 | stream?: boolean | undefined; 302 | fwdAllHeaders?: boolean | undefined; 303 | fwdHeaderKeys?: string | undefined; 304 | mvxNativeAuthEnable?: number | undefined; 305 | mvxNativeAuthAccessToken?: string | undefined; 306 | mvxNativeAuthMaxExpirySeconds?: number | undefined; 307 | mvxNativeAuthOrigins?: string[] | undefined; 308 | fwdHeaderMapLookup?: any; 309 | nestedIdxToStream?: number | undefined; 310 | _fwdHeaderMapLookupMustContainBearerAuthHeader?: boolean | undefined; 311 | asDeputyOnAppointerAddr?: string | undefined; 312 | _mandatoryParamsList: string[]; // a pure JS fallback way to validate mandatory params, as typescript rules for mandatory can be bypassed by client app 313 | }): { 314 | allPassed: boolean; 315 | validationMessages: string; 316 | } { 317 | let allPassed = true; 318 | let validationMessages = ''; 319 | 320 | try { 321 | // signedMessage test 322 | let signedMessageValid = true; 323 | 324 | if ( 325 | params.signedMessage !== undefined || 326 | params._mandatoryParamsList.includes('signedMessage') 327 | ) { 328 | signedMessageValid = false; // it exists or needs to exist, so we need to validate 329 | 330 | if ( 331 | params.signedMessage !== undefined && 332 | typeof params.signedMessage === 'string' && 333 | params.signedMessage.trim() !== '' && 334 | params.signedMessage.trim().length > 5 335 | ) { 336 | signedMessageValid = true; 337 | } else { 338 | validationMessages += 339 | '[signedMessage needs to be a valid signature type string]'; 340 | } 341 | } 342 | 343 | // signableMessage test 344 | let signableMessageValid = true; 345 | 346 | if ( 347 | params.signableMessage !== undefined || 348 | params._mandatoryParamsList.includes('signableMessage') 349 | ) { 350 | signableMessageValid = false; 351 | 352 | if (params.signableMessage !== undefined) { 353 | signableMessageValid = true; 354 | } else { 355 | validationMessages += '[signableMessage needs to be a valid type]'; 356 | } 357 | } 358 | 359 | // stream test 360 | let streamValid = true; 361 | 362 | if ( 363 | params.stream !== undefined || 364 | params._mandatoryParamsList.includes('stream') 365 | ) { 366 | streamValid = false; 367 | 368 | if ( 369 | params.stream !== undefined && 370 | (params.stream === true || params.stream === false) 371 | ) { 372 | streamValid = true; 373 | } else { 374 | validationMessages += '[stream needs to be true or false]'; 375 | } 376 | } 377 | 378 | // fwdAllHeaders test 379 | let fwdAllHeadersValid = true; 380 | 381 | if ( 382 | params.fwdAllHeaders !== undefined || 383 | params._mandatoryParamsList.includes('fwdAllHeaders') 384 | ) { 385 | fwdAllHeadersValid = false; 386 | 387 | if ( 388 | params.fwdAllHeaders !== undefined && 389 | (params.fwdAllHeaders === true || params.fwdAllHeaders === false) 390 | ) { 391 | fwdAllHeadersValid = true; 392 | } else { 393 | validationMessages += '[fwdAllHeaders needs to be true or false]'; 394 | } 395 | } 396 | 397 | // fwdHeaderKeys test 398 | let fwdHeaderKeysIsValid = true; 399 | 400 | if ( 401 | params.fwdHeaderKeys !== undefined || 402 | params._mandatoryParamsList.includes('fwdHeaderKeys') 403 | ) { 404 | fwdHeaderKeysIsValid = false; 405 | 406 | if ( 407 | params.fwdHeaderKeys !== undefined && 408 | typeof params.fwdHeaderKeys === 'string' && 409 | params.fwdHeaderKeys.trim() !== '' && 410 | params.fwdHeaderKeys.split(',').length > 0 && 411 | params.fwdHeaderKeys.split(',').length < 10 412 | ) { 413 | fwdHeaderKeysIsValid = true; 414 | } else { 415 | validationMessages += 416 | '[fwdHeaderKeys needs to be a comma separated lowercase string with less than 10 items]'; 417 | } 418 | } 419 | 420 | // fwdHeaderMapLookup test 421 | let fwdHeaderMapLookupIsValid = true; 422 | 423 | if ( 424 | params.fwdHeaderMapLookup !== undefined || 425 | params._mandatoryParamsList.includes('fwdHeaderMapLookup') 426 | ) { 427 | fwdHeaderMapLookupIsValid = false; 428 | 429 | if ( 430 | params.fwdHeaderMapLookup !== undefined && 431 | Object.prototype.toString 432 | .call(params.fwdHeaderMapLookup) 433 | .includes('Object') && 434 | Object.keys(params.fwdHeaderMapLookup).length > 0 && 435 | Object.keys(params.fwdHeaderMapLookup).length < 10 436 | ) { 437 | if (!params._fwdHeaderMapLookupMustContainBearerAuthHeader) { 438 | fwdHeaderMapLookupIsValid = true; 439 | } else { 440 | const bearerKeyValEntryFound = Object.keys( 441 | params.fwdHeaderMapLookup 442 | ).find( 443 | (key) => 444 | key === 'authorization' && 445 | params.fwdHeaderMapLookup[key].includes('Bearer ') 446 | ); 447 | 448 | if (bearerKeyValEntryFound) { 449 | fwdHeaderMapLookupIsValid = true; 450 | } else { 451 | validationMessages += 452 | '[fwdHeaderMapLookup in a native auth use case you must to have an case-sensitive entry for `authorization: Bearer XXX` - Where XXX is your native auth token]'; 453 | } 454 | } 455 | } else { 456 | validationMessages += 457 | '[fwdHeaderMapLookup needs to be a object map with maximum 10 items]'; 458 | } 459 | } 460 | 461 | // mvxNativeAuthMaxExpirySeconds test 462 | let mvxNativeAuthMaxExpirySecondsValid = true; 463 | 464 | if ( 465 | params.mvxNativeAuthMaxExpirySeconds !== undefined || 466 | params._mandatoryParamsList.includes('mvxNativeAuthMaxExpirySeconds') 467 | ) { 468 | mvxNativeAuthMaxExpirySecondsValid = false; 469 | 470 | const maxExpirySeconds = 471 | params.mvxNativeAuthMaxExpirySeconds !== undefined 472 | ? parseInt(params.mvxNativeAuthMaxExpirySeconds.toString(), 10) 473 | : null; 474 | 475 | if ( 476 | maxExpirySeconds !== null && 477 | !isNaN(maxExpirySeconds) && 478 | maxExpirySeconds >= 300 && 479 | maxExpirySeconds <= 259200 480 | ) { 481 | mvxNativeAuthMaxExpirySecondsValid = true; 482 | } else { 483 | validationMessages += 484 | '[mvxNativeAuthMaxExpirySeconds needs to between min 5 mins (300) and max 3 days (259200)]'; 485 | } 486 | } 487 | 488 | // mvxNativeAuthOrigins test 489 | let mvxNativeAuthOriginsIsValid = true; 490 | 491 | if ( 492 | params.mvxNativeAuthOrigins !== undefined || 493 | params._mandatoryParamsList.includes('mvxNativeAuthOrigins') 494 | ) { 495 | mvxNativeAuthOriginsIsValid = false; 496 | 497 | if ( 498 | params.mvxNativeAuthOrigins !== undefined && 499 | Array.isArray(params.mvxNativeAuthOrigins) && 500 | params.mvxNativeAuthOrigins.length > 0 && 501 | params.mvxNativeAuthOrigins.length < 10 502 | ) { 503 | mvxNativeAuthOriginsIsValid = true; 504 | } else { 505 | validationMessages += 506 | '[mvxNativeAuthOrigins needs to be a string array of domains with less than 5 items]'; 507 | } 508 | } 509 | 510 | // nestedIdxToStream test 511 | let nestedIdxToStreamValid = true; 512 | 513 | if ( 514 | params.nestedIdxToStream !== undefined || 515 | params._mandatoryParamsList.includes('nestedIdxToStream') 516 | ) { 517 | nestedIdxToStreamValid = false; 518 | 519 | const nestedIdxToStreamToInt = 520 | params.nestedIdxToStream !== undefined 521 | ? parseInt(params.nestedIdxToStream.toString(), 10) 522 | : null; 523 | 524 | if ( 525 | nestedIdxToStreamToInt !== null && 526 | !isNaN(nestedIdxToStreamToInt) && 527 | nestedIdxToStreamToInt >= 0 528 | ) { 529 | nestedIdxToStreamValid = true; 530 | } else { 531 | validationMessages += 532 | '[nestedIdxToStream needs to be a number more than 0]'; 533 | } 534 | } 535 | 536 | // asDeputyOnAppointerAddr test 537 | let asDeputyOnAppointerAddrIsValid = true; 538 | 539 | if ( 540 | params.asDeputyOnAppointerAddr !== undefined || 541 | params._mandatoryParamsList.includes('asDeputyOnAppointerAddr') 542 | ) { 543 | asDeputyOnAppointerAddrIsValid = false; 544 | 545 | if ( 546 | params.asDeputyOnAppointerAddr !== undefined && 547 | typeof params.asDeputyOnAppointerAddr === 'string' && 548 | params.asDeputyOnAppointerAddr.trim() !== '' && 549 | params.asDeputyOnAppointerAddr.length > 10 550 | ) { 551 | asDeputyOnAppointerAddrIsValid = true; 552 | } else { 553 | validationMessages += 554 | '[asDeputyOnAppointerAddr needs to be a multiversx smart contract address in an string. e.g. erd1qqqqqqqqqqqqqpgqd2y9zvaehkn4arsjwxp8vs3rjmdwyffafsxsgjkdw8]'; 555 | } 556 | } 557 | 558 | if ( 559 | !signedMessageValid || 560 | !signableMessageValid || 561 | !streamValid || 562 | !fwdAllHeadersValid || 563 | !fwdHeaderKeysIsValid || 564 | !fwdHeaderMapLookupIsValid || 565 | !mvxNativeAuthMaxExpirySecondsValid || 566 | !mvxNativeAuthOriginsIsValid || 567 | !nestedIdxToStreamValid || 568 | !asDeputyOnAppointerAddrIsValid 569 | ) { 570 | allPassed = false; 571 | } 572 | } catch (e: any) { 573 | allPassed = false; 574 | validationMessages = e.toString(); 575 | } 576 | 577 | return { 578 | allPassed, 579 | validationMessages 580 | }; 581 | } 582 | 583 | export async function checkUrlIsUp(url: string, expectedHttpCodes: number[]) { 584 | // also do an https check as well 585 | if (!url.trim().toLowerCase().includes('https://')) { 586 | throw new Error( 587 | `URLs need to be served via a 'https://' secure protocol : ${url}` 588 | ); 589 | } 590 | 591 | const response = await fetch(url); 592 | 593 | if (!expectedHttpCodes.includes(response.status)) { 594 | throw new ErrFetch(response.status, response.statusText); 595 | } 596 | } 597 | 598 | export function checkStatus(response: Response) { 599 | if (!(response.status >= 200 && response.status <= 299)) { 600 | throw new ErrFetch(response.status, response.statusText); 601 | } 602 | } 603 | -------------------------------------------------------------------------------- /src/common/validator.ts: -------------------------------------------------------------------------------- 1 | export type Result = { ok: true; value: T } | { ok: false; message: string }; 2 | 3 | interface IValidator { 4 | validate(value: unknown): Result; 5 | } 6 | export function validateResults( 7 | results: (Result | Result)[] 8 | ): void { 9 | const errors: string[] = []; 10 | 11 | results.forEach((result, index) => { 12 | if (!result.ok) { 13 | errors.push(`Result at index ${index}: ${result.message}`); 14 | } 15 | }); 16 | 17 | if (errors.length > 0) { 18 | throw new Error(`Validation Error: ${errors.join('\n')}`); 19 | } 20 | } 21 | 22 | type StringRule = 23 | | { type: 'equal'; value: string } 24 | | { type: 'notEqual'; value: string } 25 | | { type: 'minLength'; min: number } 26 | | { type: 'maxLength'; max: number } 27 | | { type: 'alphanumeric' }; 28 | 29 | type NumericRule = 30 | | { type: 'minValue'; value: number } 31 | | { type: 'maxValue'; value: number } 32 | | { type: 'integer' }; 33 | 34 | export class StringValidator implements IValidator { 35 | private rules: StringRule[]; 36 | 37 | constructor(rules: StringRule[] = []) { 38 | this.rules = rules; 39 | } 40 | 41 | private addRule(rule: StringRule): void { 42 | this.rules.push(rule); 43 | } 44 | 45 | equals(value: string): StringValidator { 46 | this.addRule({ type: 'equal', value }); 47 | return this; 48 | } 49 | 50 | notEquals(value: string): StringValidator { 51 | this.addRule({ type: 'notEqual', value }); 52 | return this; 53 | } 54 | 55 | minLength(min: number): StringValidator { 56 | this.addRule({ type: 'minLength', min }); 57 | return this; 58 | } 59 | 60 | maxLength(max: number): StringValidator { 61 | this.addRule({ type: 'maxLength', max }); 62 | return this; 63 | } 64 | 65 | alphanumeric(): StringValidator { 66 | this.addRule({ type: 'alphanumeric' }); 67 | return this; 68 | } 69 | 70 | notEmpty(): StringValidator { 71 | this.addRule({ type: 'minLength', min: 1 }); 72 | return this; 73 | } 74 | 75 | validate(value: unknown): Result { 76 | if (typeof value !== 'string') { 77 | return { 78 | ok: false, 79 | message: `Validator expected a string but received ${typeof value}.` 80 | }; 81 | } 82 | 83 | let result: Result = { ok: true, value }; 84 | 85 | for (const rule of this.rules) { 86 | result = this.checkStringRule(rule, value); 87 | if (!result.ok) { 88 | break; 89 | } 90 | } 91 | 92 | return result; 93 | } 94 | 95 | private checkStringRule(rule: StringRule, value: string): Result { 96 | switch (rule.type) { 97 | case 'equal': 98 | return rule.value !== value 99 | ? { 100 | ok: false, 101 | message: `Value was expected to be '${rule.value}' but was '${value}'.` 102 | } 103 | : { ok: true, value }; 104 | 105 | case 'notEqual': 106 | return rule.value === value 107 | ? { ok: false, message: `Value must not be '${rule.value}'.` } 108 | : { ok: true, value }; 109 | 110 | case 'minLength': 111 | return value.length < rule.min 112 | ? { 113 | ok: false, 114 | message: `String length must be greater than or equal to ${rule.min} but was ${value.length}.` 115 | } 116 | : { ok: true, value }; 117 | 118 | case 'maxLength': 119 | return value.length > rule.max 120 | ? { 121 | ok: false, 122 | message: `String length must be less than or equal to ${rule.max} but was ${value.length}.` 123 | } 124 | : { ok: true, value }; 125 | 126 | case 'alphanumeric': 127 | return /^[a-zA-Z0-9]+$/.test(value) 128 | ? { ok: true, value } 129 | : { 130 | ok: false, 131 | message: 'Value must contain only alphanumeric characters.' 132 | }; 133 | 134 | default: 135 | return { ok: true, value }; 136 | } 137 | } 138 | } 139 | 140 | export class NumericValidator implements IValidator { 141 | private rules: NumericRule[]; 142 | 143 | constructor(rules: NumericRule[] = []) { 144 | this.rules = rules; 145 | } 146 | 147 | private addRule(rule: NumericRule): void { 148 | this.rules.push(rule); 149 | } 150 | 151 | minValue(value: number): NumericValidator { 152 | this.addRule({ type: 'minValue', value }); 153 | return this; 154 | } 155 | 156 | maxValue(value: number): NumericValidator { 157 | this.addRule({ type: 'maxValue', value }); 158 | return this; 159 | } 160 | 161 | integer(): NumericValidator { 162 | this.addRule({ type: 'integer' }); 163 | return this; 164 | } 165 | 166 | validate(value: unknown): Result { 167 | if (typeof value !== 'number' || isNaN(value)) { 168 | return { 169 | ok: false, 170 | message: `Validator expected a number but received ${typeof value}.` 171 | }; 172 | } 173 | 174 | let result: Result = { ok: true, value }; 175 | 176 | for (const rule of this.rules) { 177 | result = this.checkNumericRule(rule, value); 178 | if (!result.ok) { 179 | break; 180 | } 181 | } 182 | 183 | return result; 184 | } 185 | 186 | private checkNumericRule(rule: NumericRule, value: number): Result { 187 | switch (rule.type) { 188 | case 'minValue': 189 | return value < rule.value 190 | ? { 191 | ok: false, 192 | message: `Value must be greater than or equal to ${rule.value}.` 193 | } 194 | : { ok: true, value }; 195 | 196 | case 'maxValue': 197 | return value > rule.value 198 | ? { 199 | ok: false, 200 | message: `Value must be less than or equal to ${rule.value}.` 201 | } 202 | : { ok: true, value }; 203 | case 'integer': 204 | return value % 1 !== 0 205 | ? { ok: false, message: 'Value must be an integer.' } 206 | : { ok: true, value }; 207 | default: 208 | return { ok: true, value }; 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export enum EnvironmentsEnum { 2 | devnet = 'devnet', 3 | testnet = 'testnet', 4 | mainnet = 'mainnet' 5 | } 6 | 7 | export interface Config { 8 | chainID: string; 9 | networkProvider: string; 10 | } 11 | 12 | const devnetNetworkConfig: Config = { 13 | chainID: 'D', 14 | networkProvider: 'https://devnet-api.multiversx.com' 15 | }; 16 | 17 | const mainnetNetworkConfig: Config = { 18 | chainID: '1', 19 | networkProvider: 'https://api.multiversx.com' 20 | }; 21 | 22 | const testnetNetworkConfig: Config = { 23 | chainID: 'T', 24 | networkProvider: 'https://testnet-api.multiversx.com' 25 | }; 26 | 27 | export const itheumTokenIdentifier: { [key in EnvironmentsEnum]: string } = { 28 | devnet: 'ITHEUM-fce905', 29 | mainnet: 'ITHEUM-df6f26', 30 | testnet: '' 31 | }; 32 | 33 | export const dataNftTokenIdentifier: { [key in EnvironmentsEnum]: string } = { 34 | devnet: 'DATANFTFT-e0b917', 35 | mainnet: 'DATANFTFT-e936d4', 36 | testnet: '' 37 | }; //[future] list of whitelisted tokens as Data NFTs 38 | 39 | export const marketPlaceContractAddress: { [key in EnvironmentsEnum]: string } = 40 | { 41 | devnet: 'erd1qqqqqqqqqqqqqpgqlhewm06p4c9qhq32p239hs45dvry948tfsxshx3e0l', 42 | mainnet: 'erd1qqqqqqqqqqqqqpgqay2r64l9nhhvmaqw4qanywfd0954w2m3c77qm7drxc', 43 | testnet: '' 44 | }; 45 | 46 | export const minterContractAddress: { [key in EnvironmentsEnum]: string } = { 47 | devnet: 'erd1qqqqqqqqqqqqqpgq7thwlde9hvc5ty7lx2j3l9tvy3wgkwu7fsxsvz9rat', 48 | mainnet: 'erd1qqqqqqqqqqqqqpgqmuzgkurn657afd3r2aldqy2snsknwvrhc77q3lj8l6', 49 | testnet: '' 50 | }; 51 | 52 | export const bondContractAddress: { [key in EnvironmentsEnum]: string } = { 53 | devnet: 'erd1qqqqqqqqqqqqqpgqhlyaj872kyh620zsfew64l2k4djerw2tfsxsmrxlan', 54 | mainnet: 'erd1qqqqqqqqqqqqqpgq9yfa4vcmtmn55z0e5n84zphf2uuuxxw9c77qgqqwkn', 55 | testnet: '' 56 | }; 57 | 58 | export const livelinessStakeContractAddress: { 59 | [key in EnvironmentsEnum]: string; 60 | } = { 61 | devnet: 'erd1qqqqqqqqqqqqqpgq9j3dj650amzz8lyvek6uq0w0yvgtgggjfsxsf489hq', 62 | mainnet: 'erd1qqqqqqqqqqqqqpgq65rn8zmf2tckftpu5lvxg2pzlg0dhfrwc77qcuynw7', 63 | testnet: '' 64 | }; 65 | 66 | export const apiConfiguration: { [key in EnvironmentsEnum]: string } = { 67 | devnet: 'https://devnet-api.multiversx.com', 68 | mainnet: 'https://api.multiversx.com', 69 | testnet: 'https://testnet-api.multiversx.com' 70 | }; 71 | 72 | export const networkConfiguration: { [key in EnvironmentsEnum]: Config } = { 73 | devnet: devnetNetworkConfig, 74 | mainnet: mainnetNetworkConfig, 75 | testnet: testnetNetworkConfig 76 | }; 77 | 78 | export const imageService: { [key in EnvironmentsEnum]: string } = { 79 | devnet: 'https://api.itheumcloud-stg.com/datadexapi', 80 | mainnet: 'https://api.itheumcloud.com/datadexapi', 81 | testnet: '' 82 | }; 83 | 84 | export const marshalUrls = { 85 | devnet: 'https://api.itheumcloud-stg.com/datamarshalapi/router/v1', 86 | mainnet: 'https://api.itheumcloud.com/datamarshalapi/router/v1', 87 | testnet: '' 88 | }; 89 | 90 | export const dataMarshalUrlOverride: { 91 | [key in EnvironmentsEnum]: { 92 | tokenIdentifier: string; 93 | nonce: number; 94 | url: string; 95 | chainId: string; 96 | }[]; 97 | } = { 98 | devnet: [], 99 | mainnet: [ 100 | { 101 | tokenIdentifier: dataNftTokenIdentifier[EnvironmentsEnum.mainnet], 102 | nonce: 5, 103 | url: marshalUrls.mainnet, 104 | chainId: '1' 105 | } 106 | ], 107 | testnet: [] 108 | }; 109 | 110 | export const MAX_ITEMS = 50; 111 | -------------------------------------------------------------------------------- /src/contract.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbiRegistry, 3 | ErrContract, 4 | IAddress, 5 | SmartContract 6 | } from '@multiversx/sdk-core/out'; 7 | import { ApiNetworkProvider } from '@multiversx/sdk-network-providers/out'; 8 | import { EnvironmentsEnum, networkConfiguration } from './config'; 9 | import { ErrContractAddressNotSet, ErrNetworkConfig } from './errors'; 10 | 11 | export abstract class Contract { 12 | readonly contract: SmartContract; 13 | readonly chainID: string; 14 | readonly networkProvider: ApiNetworkProvider; 15 | readonly env: string; 16 | 17 | protected constructor( 18 | env: string, 19 | contractAddress: IAddress, 20 | abiFile: any, 21 | timeout: number = 10000 22 | ) { 23 | if (!(env in EnvironmentsEnum)) { 24 | throw new ErrNetworkConfig( 25 | `Invalid environment: ${env}, Expected: 'devnet' | 'mainnet' | 'testnet'` 26 | ); 27 | } 28 | if (!contractAddress.bech32()) { 29 | throw new ErrContractAddressNotSet(env); 30 | } 31 | 32 | this.env = env; 33 | const networkConfig = networkConfiguration[env as EnvironmentsEnum]; 34 | this.chainID = networkConfig.chainID; 35 | this.networkProvider = new ApiNetworkProvider( 36 | networkConfig.networkProvider, 37 | { 38 | timeout: timeout 39 | } 40 | ); 41 | this.contract = new SmartContract({ 42 | address: contractAddress, 43 | abi: AbiRegistry.create(abiFile) 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | import { MAX_ITEMS } from './config'; 2 | 3 | export class ErrNetworkConfig extends Error { 4 | public constructor(message?: string) { 5 | super( 6 | message || 7 | 'Network configuration is not set. Call setNetworkConfig static method before calling any method that requires network configuration.' 8 | ); 9 | } 10 | } 11 | 12 | export class ErrContractAddressNotSet extends Error { 13 | public constructor(env: string, message?: string) { 14 | super(message || 'Contract address is not deployed on ' + env); 15 | } 16 | } 17 | 18 | export class ErrArgumentNotSet extends Error { 19 | public constructor(argument: string, message?: string) { 20 | super(`Argument "${argument}" is not set. ${message}`); 21 | } 22 | } 23 | export class ErrInvalidArgument extends Error { 24 | public constructor(message: string) { 25 | super(`Invalid argument: ${message}`); 26 | } 27 | } 28 | 29 | export class ErrBadType extends Error { 30 | public constructor(name: string, type: any, value?: any, context?: string) { 31 | super( 32 | `Bad type of "${name}": ${value}. Expected type: ${type}. Context: ${context}` 33 | ); 34 | } 35 | } 36 | 37 | export class ErrDataNftCreate extends Error { 38 | public constructor(message?: string) { 39 | super(`Failed to create Data NFT: ${message}`); 40 | } 41 | } 42 | 43 | export class ErrFetch extends Error { 44 | public constructor(status: number, message: string) { 45 | super(`Fetch error with status code: ${status} and message: ${message}`); 46 | } 47 | } 48 | 49 | export class ErrDecodeAttributes extends Error { 50 | public constructor(message?: string) { 51 | super(`Failed to decode attributes: ${message}`); 52 | } 53 | } 54 | 55 | export class ErrParseNft extends Error { 56 | public constructor(message?: string) { 57 | super(`Failed to parse NFT: ${message}`); 58 | } 59 | } 60 | 61 | export class ErrAttributeNotSet extends Error { 62 | public constructor(attribute: string) { 63 | super(`Attribute "${attribute}" is not set`); 64 | } 65 | } 66 | 67 | export class ErrContractQuery extends Error { 68 | public constructor(method: string, message?: string) { 69 | super(`Failed to query contract with method: ${method} : ${message}`); 70 | } 71 | } 72 | 73 | export class ErrParamValidation extends Error { 74 | public constructor(message: string) { 75 | super(`Params have validation issues : ${message}`); 76 | } 77 | } 78 | 79 | export class ErrFailedOperation extends Error { 80 | public constructor(method: string, message?: string) { 81 | super(`Failed to perform operation: ${method} : ${message}`); 82 | } 83 | } 84 | 85 | export class ErrMissingTrait extends Error { 86 | public constructor(trait: string) { 87 | super(`Missing trait: ${trait}`); 88 | } 89 | } 90 | 91 | export class ErrMissingValueForTrait extends Error { 92 | public constructor(trait: string) { 93 | super(`Missing value for trait: ${trait}`); 94 | } 95 | } 96 | 97 | export class ErrTooManyItems extends Error { 98 | public constructor() { 99 | super(`Too many items. Max: ${MAX_ITEMS}`); 100 | } 101 | } 102 | 103 | export class ErrInvalidTokenIdentifier extends Error { 104 | public constructor() { 105 | super(`Invalid token identifier. Format: ticker-randomString-nonce`); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './datanft'; 3 | export * from './interfaces'; 4 | export * from './marketplace'; 5 | export * from './minter'; 6 | export * from './nft-minter'; 7 | export * from './sft-minter'; 8 | export * from './bond'; 9 | export * from './contract'; 10 | export * from './liveliness-stake'; 11 | export { parseTokenIdentifier, createTokenIdentifier } from './common/utils'; 12 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | 3 | export interface NftType { 4 | identifier: string; 5 | collection: string; 6 | ticker?: string; 7 | timestamp: number; 8 | attributes: string; 9 | nonce: number; 10 | type: NftEnumType; 11 | name: string; 12 | creator: string; 13 | royalties: number; 14 | balance: string; 15 | uris?: string[]; 16 | url?: string; 17 | thumbnailUrl?: string; 18 | tags?: string[]; 19 | decimals?: number; 20 | owner?: string; 21 | supply?: string; 22 | isWhitelistedStorage?: boolean; 23 | owners?: { 24 | address: string; 25 | balance: string; 26 | }[]; 27 | assets?: { 28 | website?: string; 29 | description?: string; 30 | status?: string; 31 | pngUrl?: string; 32 | svgUrl?: string; 33 | }; 34 | metadata?: { 35 | description?: string; 36 | fileType?: string; 37 | fileUri?: string; 38 | fileName?: string; 39 | itheum_data_preview_url?: string; 40 | itheum_data_stream_url?: string; 41 | itheum_data_marshal_url?: string; 42 | itheum_creator?: string; 43 | }; 44 | media?: { 45 | url: string; 46 | originalUrl: string; 47 | thumbnailUrl: string; 48 | fileType: string; 49 | fileSize: number; 50 | }[]; 51 | } 52 | 53 | export interface DataNftType { 54 | readonly tokenIdentifier: string; 55 | readonly nftImgUrl: string; 56 | readonly type: NftEnumType; 57 | readonly dataPreview: string; 58 | readonly dataStream: string; 59 | readonly dataMarshal: string; 60 | readonly tokenName: string; 61 | readonly creator: string; 62 | readonly creationTime: Date; 63 | readonly supply: number | BigNumber.Value; 64 | readonly description: string; 65 | readonly title: string; 66 | readonly royalties: number; 67 | readonly nonce: number; 68 | readonly collection: string; 69 | readonly balance: number | BigNumber.Value; 70 | readonly owner: string; 71 | readonly overrideDataMarshal: string; 72 | readonly overrideDataMarshalChainId: string; 73 | readonly isDataNFTPH: boolean; 74 | readonly extraAssets: string[]; 75 | readonly media: { 76 | url: string; 77 | originalUrl: string; 78 | thumbnailUrl: string; 79 | fileType: string; 80 | fileSize: number; 81 | }[]; 82 | } 83 | 84 | export enum NftEnumType { 85 | NonFungibleESDT = 'NonFungibleESDT', 86 | SemiFungibleESDT = 'SemiFungibleESDT', 87 | MetaESDT = 'MetaESDT' 88 | } 89 | export interface MarketplaceRequirements { 90 | acceptedTokens: string[]; 91 | acceptedPayments: string[]; 92 | maximumPaymentFees: string[]; 93 | buyerTaxPercentageDiscount: number; 94 | sellerTaxPercentageDiscount: number; 95 | buyerTaxPercentage: number; 96 | sellerTaxPercentage: number; 97 | maxDefaultQuantity: number; 98 | } 99 | 100 | export interface SftMinterRequirements { 101 | antiSpamTaxValue: number; 102 | addressFrozen: boolean; 103 | frozenNonces: number[]; 104 | contractPaused: boolean; 105 | userWhitelistedForMint: boolean; 106 | lastUserMintTime: number; 107 | maxRoyalties: number; 108 | maxSupply: number; 109 | minRoyalties: number; 110 | mintTimeLimit: number; 111 | numberOfMintsForUser: number; 112 | totalNumberOfMints: number; 113 | contractWhitelistEnabled: boolean; 114 | maxDonationPecentage: number; 115 | } 116 | 117 | export interface NftMinterRequirements { 118 | antiSpamTaxValue: number; 119 | addressFrozen: boolean; 120 | frozenNonces: number[]; 121 | contractPaused: boolean; 122 | userWhitelistedForMint: boolean; 123 | lastUserMintTime: number; 124 | maxRoyalties: number; 125 | minRoyalties: number; 126 | mintTimeLimit: number; 127 | numberOfMintsForUser: number; 128 | totalNumberOfMints: number; 129 | contractWhitelistEnabled: boolean; 130 | } 131 | 132 | export interface Offer { 133 | index: number; 134 | owner: string; 135 | offeredTokenIdentifier: string; 136 | offeredTokenNonce: number; 137 | offeredTokenAmount: BigNumber.Value; 138 | wantedTokenIdentifier: string; 139 | wantedTokenNonce: number; 140 | wantedTokenAmount: BigNumber.Value; 141 | quantity: number; 142 | maxQuantityPerAddress: number; 143 | } 144 | 145 | export interface Bond { 146 | bondId: number; 147 | address: string; 148 | tokenIdentifier: string; 149 | nonce: number; 150 | lockPeriod: number; // seconds 151 | bondTimestamp: number; 152 | unbondTimestamp: number; 153 | bondAmount: BigNumber.Value; 154 | remainingAmount: BigNumber.Value; 155 | } 156 | 157 | export interface BondConfiguration { 158 | contractState: State; 159 | bondPaymentTokenIdentifier: string; 160 | lockPeriodsWithBonds: { lockPeriod: number; amount: BigNumber.Value }[]; 161 | minimumPenalty: number; 162 | maximumPenalty: number; 163 | withdrawPenalty: number; 164 | acceptedCallers: string[]; 165 | } 166 | 167 | export interface LivelinessStakeConfiguration { 168 | rewardsReserve: BigNumber.Value; 169 | accumulatedRewards: BigNumber.Value; 170 | rewardsTokenIdentifier: string; 171 | rewardsPerBlock: BigNumber.Value; 172 | lastRewardBlockNonce: number; 173 | maxApr: number; 174 | administrator: string; 175 | bondContractAddress: string; 176 | } 177 | 178 | export interface Refund { 179 | compensationId: number; 180 | address: string; 181 | proofOfRefund: { 182 | tokenIdentifier: string; 183 | nonce: number; 184 | amount: BigNumber.Value; 185 | }; 186 | } 187 | 188 | export interface Compensation { 189 | compensationId: number; 190 | tokenIdentifier: string; 191 | nonce: number; 192 | accumulatedAmount: BigNumber.Value; 193 | proofAmount: BigNumber.Value; 194 | endDate: number; 195 | } 196 | 197 | export enum State { 198 | Inactive = 0, 199 | Active = 1 200 | } 201 | 202 | export enum PenaltyType { 203 | Minimum = 0, 204 | Custom = 1, 205 | Maximum = 2 206 | } 207 | 208 | export interface ViewDataReturnType { 209 | data: any; 210 | contentType: string; 211 | error?: string; 212 | } 213 | 214 | export interface ContractConfiguration { 215 | tokenIdentifier: string; 216 | mintedTokens: number; 217 | isTaxRequired: boolean; 218 | isContractPaused: boolean; 219 | maxRoyalties: number; 220 | minRoyalties: number; 221 | mintTimeLimit: number; 222 | isWhitelistEnabled: boolean; 223 | rolesAreSet: boolean; 224 | claimsAddress: string; 225 | administratorAddress: string; 226 | taxToken: string; 227 | } 228 | 229 | export interface UserData { 230 | totalStakedAmount: BigNumber.Value; 231 | userStakedAmount: BigNumber.Value; 232 | livelinessScore: number; 233 | accumulatedRewards: BigNumber.Value; 234 | accumulatedRewardsBypass: BigNumber.Value; 235 | vaultNonce: number; 236 | } 237 | -------------------------------------------------------------------------------- /src/liveliness-stake.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | AddressValue, 4 | BooleanType, 5 | BooleanValue, 6 | ContractCallPayloadBuilder, 7 | IAddress, 8 | OptionValue, 9 | ResultsParser, 10 | TokenIdentifierValue, 11 | Transaction, 12 | VariadicValue 13 | } from '@multiversx/sdk-core/out'; 14 | import { 15 | dataNftTokenIdentifier, 16 | EnvironmentsEnum, 17 | livelinessStakeContractAddress 18 | } from './config'; 19 | import { Contract } from './contract'; 20 | import livelinessStakeAbi from './abis/core-mx-liveliness-stake.abi.json'; 21 | import { 22 | ContractConfiguration, 23 | LivelinessStakeConfiguration, 24 | State, 25 | UserData 26 | } from './interfaces'; 27 | import { ErrContractQuery } from './errors'; 28 | import { 29 | parseLivelinessStakeConfiguration, 30 | parseUserData 31 | } from './common/utils'; 32 | import BigNumber from 'bignumber.js'; 33 | import { Token } from 'nft.storage'; 34 | 35 | export class LivelinessStake extends Contract { 36 | constructor(env: string, timeout: number = 10000) { 37 | super( 38 | env, 39 | new Address(livelinessStakeContractAddress[env as EnvironmentsEnum]), 40 | livelinessStakeAbi, 41 | timeout 42 | ); 43 | } 44 | 45 | /** 46 | * Returns the contract address 47 | */ 48 | getContractAddress(): IAddress { 49 | return this.contract.getAddress(); 50 | } 51 | 52 | /** 53 | * Returns the contract state as a `State` enum 54 | */ 55 | async viewContractState(): Promise { 56 | const interaction = this.contract.methodsExplicit.getContractState([]); 57 | const query = interaction.buildQuery(); 58 | const queryResponse = await this.networkProvider.queryContract(query); 59 | const endpointDefinition = interaction.getEndpoint(); 60 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 61 | queryResponse, 62 | endpointDefinition 63 | ); 64 | if (returnCode.isSuccess()) { 65 | const stateValue = firstValue?.valueOf(); 66 | return stateValue.name as State; 67 | } else { 68 | throw new ErrContractQuery('viewContractState', returnCode.toString()); 69 | } 70 | } 71 | 72 | /** 73 | * Returns the `liveliness stake` contract configuration 74 | */ 75 | async viewContractConfiguration(): Promise { 76 | const interaction = this.contract.methodsExplicit.contractDetails([]); 77 | const query = interaction.buildQuery(); 78 | const queryResponse = await this.networkProvider.queryContract(query); 79 | const endpointDefinition = interaction.getEndpoint(); 80 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 81 | queryResponse, 82 | endpointDefinition 83 | ); 84 | if (returnCode.isSuccess()) { 85 | const firstValueAsVariadic = firstValue as VariadicValue; 86 | const returnValue = firstValueAsVariadic?.valueOf(); 87 | const livelinessConfiguration = 88 | parseLivelinessStakeConfiguration(returnValue); 89 | return livelinessConfiguration; 90 | } else { 91 | throw new ErrContractQuery( 92 | 'viewContractConfiguration', 93 | returnCode.toString() 94 | ); 95 | } 96 | } 97 | 98 | /** 99 | * Returns the `user data out` for a given address 100 | * @param address address to check user data out 101 | * @param tokenIdentifier the token identifier of the Data Nft [default is the Data Nft token identifier based on {@link EnvironmentsEnum}] 102 | */ 103 | async getUserDataOut( 104 | address: IAddress, 105 | tokenIdentifier = dataNftTokenIdentifier[this.env as EnvironmentsEnum] 106 | ): Promise<{ 107 | contractDetails: LivelinessStakeConfiguration; 108 | userData: UserData; 109 | }> { 110 | const interaction = this.contract.methodsExplicit.userDataOut([ 111 | new AddressValue(address), 112 | new TokenIdentifierValue(tokenIdentifier) 113 | ]); 114 | const query = interaction.buildQuery(); 115 | const queryResponse = await this.networkProvider.queryContract(query); 116 | const endpointDefinition = interaction.getEndpoint(); 117 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 118 | queryResponse, 119 | endpointDefinition 120 | ); 121 | if (returnCode.isSuccess()) { 122 | const returnValue = firstValue?.valueOf(); 123 | const livelinessConfiguration = parseLivelinessStakeConfiguration( 124 | returnValue.field0.valueOf() 125 | ); 126 | const userData = parseUserData(returnValue.field1.valueOf()); 127 | return { contractDetails: livelinessConfiguration, userData }; 128 | } else { 129 | throw new ErrContractQuery('getUserDataOut', returnCode.toString()); 130 | } 131 | } 132 | 133 | /** 134 | * Returns rewards state as a `State` enum 135 | */ 136 | async viewRewardsState(): Promise { 137 | const interaction = this.contract.methodsExplicit.rewardsState([]); 138 | const query = interaction.buildQuery(); 139 | const queryResponse = await this.networkProvider.queryContract(query); 140 | const endpointDefinition = interaction.getEndpoint(); 141 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 142 | queryResponse, 143 | endpointDefinition 144 | ); 145 | if (returnCode.isSuccess()) { 146 | const stateValue = firstValue?.valueOf(); 147 | return stateValue.name as State; 148 | } else { 149 | throw new ErrContractQuery('viewContractState', returnCode.toString()); 150 | } 151 | } 152 | 153 | /** 154 | * Returns a `BigNumber.Value` representing the claimable rewards 155 | * @param address address to check claimable rewards 156 | * @param bypass_liveliness_check boolean value to bypass liveliness check 157 | * @returns 158 | */ 159 | 160 | async viewClaimableRewards( 161 | address: IAddress, 162 | bypass_liveliness_check: boolean = false 163 | ): Promise { 164 | let interaction = bypass_liveliness_check 165 | ? this.contract.methodsExplicit.claimableRewards([ 166 | new AddressValue(address), 167 | new OptionValue( 168 | new BooleanType(), 169 | new BooleanValue(bypass_liveliness_check) 170 | ) 171 | ]) 172 | : this.contract.methodsExplicit.claimableRewards([ 173 | new AddressValue(address), 174 | new OptionValue( 175 | new BooleanType(), 176 | new BooleanValue(bypass_liveliness_check) 177 | ) 178 | ]); 179 | 180 | const query = interaction.buildQuery(); 181 | const queryResponse = await this.networkProvider.queryContract(query); 182 | const endpointDefinition = interaction.getEndpoint(); 183 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 184 | queryResponse, 185 | endpointDefinition 186 | ); 187 | if (returnCode.isSuccess()) { 188 | return BigNumber(firstValue?.valueOf()); 189 | } else { 190 | throw new ErrContractQuery('viewClaimableRewards', returnCode.toString()); 191 | } 192 | } 193 | 194 | /** 195 | * Builds a `claimRewards` transaction 196 | * @param senderAddress address of the sender 197 | * @returns 198 | */ 199 | claimRewards(senderAddress: IAddress): Transaction { 200 | const claimRewardsTx = new Transaction({ 201 | value: 0, 202 | data: new ContractCallPayloadBuilder() 203 | .setFunction('claimRewards') 204 | .build(), 205 | receiver: this.contract.getAddress(), 206 | sender: senderAddress, 207 | gasLimit: 50_000_000, 208 | chainID: this.chainID 209 | }); 210 | return claimRewardsTx; 211 | } 212 | 213 | /** 214 | * Builds a `stakeRewards` transaction 215 | * @param senderAddress address of the sender 216 | * @param tokenIdentifier the token identifier of the Data Nft [default is the Data Nft token identifier based on {@link EnvironmentsEnum}] 217 | */ 218 | stakeRewards( 219 | senderAddress: IAddress, 220 | tokenIdentifier = dataNftTokenIdentifier[this.env as EnvironmentsEnum] 221 | ): Transaction { 222 | const stakeRewardsTx = new Transaction({ 223 | value: 0, 224 | data: new ContractCallPayloadBuilder() 225 | .setFunction('stakeRewards') 226 | .addArg(new TokenIdentifierValue(tokenIdentifier)) 227 | .build(), 228 | receiver: this.contract.getAddress(), 229 | sender: senderAddress, 230 | gasLimit: 90_000_000, 231 | chainID: this.chainID 232 | }); 233 | return stakeRewardsTx; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/marketplace.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbiRegistry, 3 | Address, 4 | AddressValue, 5 | BigUIntValue, 6 | BooleanValue, 7 | ContractCallPayloadBuilder, 8 | ContractFunction, 9 | IAddress, 10 | ResultsParser, 11 | SmartContract, 12 | StringValue, 13 | TokenIdentifierValue, 14 | Transaction, 15 | U64Value, 16 | U8Value, 17 | VariadicValue 18 | } from '@multiversx/sdk-core/out'; 19 | import { ApiNetworkProvider } from '@multiversx/sdk-network-providers/out'; 20 | import dataMarketAbi from './abis/data_market.abi.json'; 21 | import { parseOffer } from './common/utils'; 22 | import { 23 | EnvironmentsEnum, 24 | itheumTokenIdentifier, 25 | marketPlaceContractAddress, 26 | networkConfiguration 27 | } from './config'; 28 | import { ErrContractQuery, ErrNetworkConfig } from './errors'; 29 | import { MarketplaceRequirements, Offer } from './interfaces'; 30 | import BigNumber from 'bignumber.js'; 31 | // import { ErrContractQuery } from './errors'; 32 | 33 | export class DataNftMarket { 34 | readonly contract: SmartContract; 35 | readonly chainID: string; 36 | readonly networkProvider: ApiNetworkProvider; 37 | readonly env: string; 38 | 39 | /** 40 | * Creates a new instance of the DataNftMarket which can be used to interact with the marketplace smart contract 41 | * @param env 'devnet' | 'mainnet' | 'testnet' 42 | * @param timeout Timeout for the network provider (DEFAULT = 10000ms) 43 | */ 44 | constructor(env: string, timeout: number = 10000) { 45 | if (!(env in EnvironmentsEnum)) { 46 | throw new ErrNetworkConfig( 47 | `Invalid environment: ${env}, Expected: 'devnet' | 'mainnet' | 'testnet'` 48 | ); 49 | } 50 | this.env = env; 51 | const networkConfig = networkConfiguration[env as EnvironmentsEnum]; 52 | this.chainID = networkConfig.chainID; 53 | this.networkProvider = new ApiNetworkProvider( 54 | networkConfig.networkProvider, 55 | { 56 | timeout: timeout 57 | } 58 | ); 59 | const contractAddress = marketPlaceContractAddress[env as EnvironmentsEnum]; 60 | 61 | this.contract = new SmartContract({ 62 | address: new Address(contractAddress), 63 | abi: AbiRegistry.create(dataMarketAbi) 64 | }); 65 | } 66 | 67 | /** 68 | * Retrieves the address of the marketplace smart contract based on the environment 69 | */ 70 | getContractAddress(): IAddress { 71 | return this.contract.getAddress(); 72 | } 73 | 74 | /** 75 | * Retrieves all `Offer` objects listed on the marketplace for a given address 76 | * @param address Address to query 77 | */ 78 | async viewAddressListedOffers(address: IAddress): Promise { 79 | const interaction = this.contract.methodsExplicit.viewUserListedOffers([ 80 | new AddressValue(address) 81 | ]); 82 | const query = interaction.buildQuery(); 83 | const queryResponse = await this.networkProvider.queryContract(query); 84 | const endpointDefinition = interaction.getEndpoint(); 85 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 86 | queryResponse, 87 | endpointDefinition 88 | ); 89 | if (returnCode.isSuccess()) { 90 | const firstValueAsVariadic = firstValue as VariadicValue; 91 | const returnValue = firstValueAsVariadic?.valueOf(); 92 | const offers: Offer[] = returnValue.map((offer: any) => 93 | parseOffer(offer) 94 | ); 95 | return offers; 96 | } else { 97 | throw new ErrContractQuery( 98 | 'viewAddressListedOffers', 99 | returnCode.toString() 100 | ); 101 | } 102 | } 103 | 104 | /** 105 | * Retrieves an array of `Offer` objects listed on the marketplace for a given address within a specified range. 106 | * @param from The starting index of the desired range of offers. 107 | * @param to The ending index of the desired range of offers. 108 | * @param address The address to query. 109 | */ 110 | async viewAddressPagedOffers( 111 | from: number, 112 | to: number, 113 | address: IAddress 114 | ): Promise { 115 | const interaction = this.contract.methodsExplicit.viewUserPagedOffers([ 116 | new U64Value(from), 117 | new U64Value(to), 118 | new AddressValue(address) 119 | ]); 120 | const query = interaction.buildQuery(); 121 | const queryResponse = await this.networkProvider.queryContract(query); 122 | const endpointDefinition = interaction.getEndpoint(); 123 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 124 | queryResponse, 125 | endpointDefinition 126 | ); 127 | if (returnCode.isSuccess()) { 128 | const firstValueAsVariadic = firstValue as VariadicValue; 129 | const returnValue = firstValueAsVariadic?.valueOf(); 130 | const offers: Offer[] = returnValue.map((offer: any) => 131 | parseOffer(offer) 132 | ); 133 | return offers; 134 | } else { 135 | throw new ErrContractQuery( 136 | 'viewAddressPagedOffers', 137 | returnCode.toString() 138 | ); 139 | } 140 | } 141 | 142 | /** 143 | * Returns the total number of offers listed for a given address 144 | * @param address Address to query 145 | */ 146 | async viewAddressTotalOffers(address: IAddress): Promise { 147 | const interaction = this.contract.methodsExplicit.viewUserTotalOffers([ 148 | new AddressValue(address) 149 | ]); 150 | const query = interaction.buildQuery(); 151 | const queryResponse = await this.networkProvider.queryContract(query); 152 | const endpointDefinition = interaction.getEndpoint(); 153 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 154 | queryResponse, 155 | endpointDefinition 156 | ); 157 | if (returnCode.isSuccess()) { 158 | const returnValue = firstValue?.valueOf(); 159 | return returnValue.toNumber(); 160 | } else { 161 | throw new ErrContractQuery( 162 | 'viewAddressTotalOffers', 163 | returnCode.toString() 164 | ); 165 | } 166 | } 167 | 168 | /** 169 | * Retrieves all cancelled `Offer` objects for a given address which opted to not withdraw the funds 170 | * @param address Address to query 171 | */ 172 | async viewAddressCancelledOffers(address: IAddress): Promise { 173 | const interaction = this.contract.methodsExplicit.viewCancelledOffers([ 174 | new AddressValue(address) 175 | ]); 176 | const query = interaction.buildQuery(); 177 | const queryResponse = await this.networkProvider.queryContract(query); 178 | const endpointDefinition = interaction.getEndpoint(); 179 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 180 | queryResponse, 181 | endpointDefinition 182 | ); 183 | if (returnCode.isSuccess()) { 184 | const returnValue = firstValue?.valueOf(); 185 | const offers: Offer[] = returnValue.map((offer: any) => 186 | parseOffer(offer) 187 | ); 188 | return offers; 189 | } else { 190 | throw new ErrContractQuery( 191 | 'viewAddressCancelledOffers', 192 | returnCode.toString() 193 | ); 194 | } 195 | } 196 | 197 | /** 198 | * Retrieves an array of `Offer` objects in an arbitrary order. 199 | * @param from first index 200 | * @param to last index 201 | * @param senderAddress the address of the sender (optional) 202 | */ 203 | async viewPagedOffers( 204 | from: number, 205 | to: number, 206 | senderAddress?: IAddress 207 | ): Promise { 208 | let interaction = this.contract.methodsExplicit.viewPagedOffers([ 209 | new U64Value(from), 210 | new U64Value(to) 211 | ]); 212 | if (senderAddress) { 213 | interaction = this.contract.methodsExplicit.viewPagedOffers([ 214 | new U64Value(from), 215 | new U64Value(to), 216 | new AddressValue(senderAddress) 217 | ]); 218 | } 219 | const query = interaction.buildQuery(); 220 | const queryResponse = await this.networkProvider.queryContract(query); 221 | const endpointDefinition = interaction.getEndpoint(); 222 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 223 | queryResponse, 224 | endpointDefinition 225 | ); 226 | if (returnCode.isSuccess()) { 227 | const returnValue = firstValue?.valueOf(); 228 | const offers: Offer[] = returnValue.map((offer: any) => 229 | parseOffer(offer) 230 | ); 231 | return offers; 232 | } else { 233 | throw new ErrContractQuery('viewPagedOffers', returnCode.toString()); 234 | } 235 | } 236 | 237 | /** 238 | * Retrieves an `Offer` object based on the offer id 239 | * @param offerId The id of the offer to be retrieved 240 | */ 241 | async viewOffer(offerId: number): Promise { 242 | let interaction = this.contract.methodsExplicit.viewOffer([ 243 | new U64Value(offerId) 244 | ]); 245 | 246 | const query = interaction.buildQuery(); 247 | const queryResponse = await this.networkProvider.queryContract(query); 248 | const endpointDefinition = interaction.getEndpoint(); 249 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 250 | queryResponse, 251 | endpointDefinition 252 | ); 253 | if (returnCode.isSuccess()) { 254 | const returnValue = firstValue?.valueOf(); 255 | const offer: Offer = parseOffer(returnValue); 256 | return offer; 257 | } else { 258 | throw new ErrContractQuery('viewPagedOffers', returnCode.toString()); 259 | } 260 | } 261 | 262 | /** 263 | * Retrieves an array of `Offer` objects. 264 | */ 265 | async viewOffers(offerIds: number[]): Promise { 266 | const input = offerIds.map((id) => new U64Value(id)); 267 | const interaction = this.contract.methodsExplicit.viewOffers(input); 268 | const query = interaction.buildQuery(); 269 | const queryResponse = await this.networkProvider.queryContract(query); 270 | const endpointDefinition = interaction.getEndpoint(); 271 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 272 | queryResponse, 273 | endpointDefinition 274 | ); 275 | if (returnCode.isSuccess()) { 276 | const returnValue = firstValue?.valueOf(); 277 | const offers: Offer[] = returnValue.map((offer: any) => 278 | parseOffer(offer) 279 | ); 280 | return offers; 281 | } else { 282 | throw new ErrContractQuery('viewOffers', returnCode.toString()); 283 | } 284 | } 285 | 286 | /** 287 | * Retrieves the smart contract requirements for the marketplace 288 | */ 289 | async viewRequirements(): Promise { 290 | const interaction = this.contract.methodsExplicit.viewRequirements(); 291 | const query = interaction.buildQuery(); 292 | const queryResponse = await this.networkProvider.queryContract(query); 293 | const endpointDefinition = interaction.getEndpoint(); 294 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 295 | queryResponse, 296 | endpointDefinition 297 | ); 298 | if (returnCode.isSuccess()) { 299 | const returnValue = firstValue?.valueOf(); 300 | const requirements: MarketplaceRequirements = { 301 | acceptedTokens: returnValue.accepted_tokens as string[], 302 | acceptedPayments: returnValue.accepted_payments as string[], 303 | maximumPaymentFees: returnValue.maximum_payment_fees.map((v: any) => 304 | v.toFixed(0) 305 | ), 306 | buyerTaxPercentageDiscount: 307 | returnValue.discount_fee_percentage_buyer.toNumber(), 308 | sellerTaxPercentageDiscount: 309 | returnValue.discount_fee_percentage_seller.toNumber(), 310 | buyerTaxPercentage: returnValue.percentage_cut_from_buyer.toNumber(), 311 | sellerTaxPercentage: returnValue.percentage_cut_from_seller.toNumber(), 312 | maxDefaultQuantity: returnValue.max_default_quantity.toNumber() 313 | }; 314 | return requirements; 315 | } else { 316 | throw new ErrContractQuery('viewRequirements', returnCode.toString()); 317 | } 318 | } 319 | 320 | /** 321 | * Retrieves the smart contract number of offers 322 | */ 323 | async viewNumberOfOffers(): Promise { 324 | const interaction = this.contract.methodsExplicit.viewNumberOfOffers(); 325 | const query = interaction.buildQuery(); 326 | const queryResponse = await this.networkProvider.queryContract(query); 327 | const endpointDefinition = interaction.getEndpoint(); 328 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 329 | queryResponse, 330 | endpointDefinition 331 | ); 332 | if (returnCode.isSuccess()) { 333 | const returnValue = firstValue?.valueOf(); 334 | return new U8Value(returnValue).valueOf().toNumber(); 335 | } 336 | throw new ErrContractQuery('viewNumberOfOffers', returnCode.toString()); 337 | } 338 | 339 | /** 340 | * Retrieves the last valid offer id in the storage 341 | */ 342 | async viewLastValidOfferId(): Promise { 343 | const interaction = this.contract.methodsExplicit.getLastValidOfferId(); 344 | const query = interaction.buildQuery(); 345 | const queryResponse = await this.networkProvider.queryContract(query); 346 | const endpointDefinition = interaction.getEndpoint(); 347 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 348 | queryResponse, 349 | endpointDefinition 350 | ); 351 | if (returnCode.isSuccess()) { 352 | const returnValue = firstValue?.valueOf(); 353 | return new U64Value(returnValue).valueOf().toNumber(); 354 | } 355 | 356 | throw new ErrContractQuery('viewLastValidOfferId', returnCode.toString()); 357 | } 358 | 359 | /** 360 | * Retrieves if the smart contract is paused or not 361 | */ 362 | async viewContractPauseState(): Promise { 363 | const interaction = this.contract.methodsExplicit.getIsPaused(); 364 | const query = interaction.buildQuery(); 365 | const queryResponse = await this.networkProvider.queryContract(query); 366 | const endpointDefinition = interaction.getEndpoint(); 367 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 368 | queryResponse, 369 | endpointDefinition 370 | ); 371 | if (returnCode.isSuccess()) { 372 | const returnValue = firstValue?.valueOf(); 373 | return new BooleanValue(returnValue).valueOf(); 374 | } else { 375 | throw new ErrContractQuery( 376 | 'viewContractPauseState', 377 | returnCode.toString() 378 | ); 379 | } 380 | } 381 | 382 | /** 383 | * Creates a `addOffer` transaction 384 | * @param senderAddress the address of the sender 385 | * @param dataNftIdentifier the identifier of the DATA-NFT 386 | * @param dataNftNonce the nonce of the DATA-NFT 387 | * @param dataNftAmount the amount of the DATA-NFT 388 | * @param paymentTokenIdentifier the identifier of the payment token `sender` wants to receive 389 | * @param paymentTokenNonce the nonce of the payment token 390 | * @param paymentTokenAmount the amount of the payment token 391 | * @param minimumPaymentTokenAmount the minimum amount of which the `sender` is willing to receive (useful in case where an offer was added and the smart contract fee was changed afterwards) 392 | * @param maxQuantity the maximum quantity a user can buy (default could be 0 - (no enforced max value) or admin defined value) 393 | */ 394 | addOffer( 395 | senderAddress: IAddress, 396 | dataNftIdentifier: string, 397 | dataNftNonce: number, 398 | dataNftAmount: BigNumber.Value, 399 | paymentTokenIdentifier: string, 400 | paymentTokenNonce: number, 401 | paymentTokenAmount: BigNumber.Value, 402 | minimumPaymentTokenAmount = 0, 403 | maxQuantity: BigNumber.Value 404 | ): Transaction { 405 | const addOfferTx = new Transaction({ 406 | value: 0, 407 | data: new ContractCallPayloadBuilder() 408 | .setFunction(new ContractFunction('ESDTNFTTransfer')) 409 | .addArg(new TokenIdentifierValue(dataNftIdentifier)) 410 | .addArg(new U64Value(dataNftNonce)) 411 | .addArg(new BigUIntValue(dataNftAmount)) 412 | .addArg(new AddressValue(this.contract.getAddress())) 413 | .addArg(new StringValue('addOffer')) 414 | .addArg(new TokenIdentifierValue(paymentTokenIdentifier)) 415 | .addArg(new U64Value(paymentTokenNonce)) 416 | .addArg(new U64Value(paymentTokenAmount)) 417 | .addArg(new U64Value(minimumPaymentTokenAmount)) 418 | .addArg(new BigUIntValue(dataNftAmount)) 419 | .addArg(new BigUIntValue(maxQuantity)) 420 | .build(), 421 | receiver: senderAddress, 422 | sender: senderAddress, 423 | gasLimit: 12000000, 424 | chainID: this.chainID 425 | }); 426 | 427 | return addOfferTx; 428 | } 429 | 430 | /** 431 | * Creates a `acceptOffer` transaction with ESDT tokens 432 | * @param senderAddress the address of the sender 433 | * @param offerId the id of the offer to be accepted 434 | * @param amount the amount of tokens to be bought 435 | * @param price the price of the offer for the total amount to be bought (must include the buyer fee) 436 | * @param paymentTokenIdentifier the identifier of the payment token (default = `ITHEUM` token identifier based on the {@link EnvironmentsEnum})) 437 | */ 438 | acceptOfferWithESDT( 439 | senderAddress: IAddress, 440 | offerId: number, 441 | amount: BigNumber.Value, 442 | price: BigNumber.Value, 443 | paymentTokenIdentifier = itheumTokenIdentifier[this.env as EnvironmentsEnum] 444 | ): Transaction { 445 | const data = new ContractCallPayloadBuilder() 446 | .setFunction(new ContractFunction('ESDTTransfer')) 447 | .addArg(new TokenIdentifierValue(paymentTokenIdentifier)) 448 | .addArg(new BigUIntValue(price)) 449 | .addArg(new StringValue('acceptOffer')) 450 | .addArg(new U64Value(offerId)) 451 | .addArg(new BigUIntValue(amount)) 452 | .build(); 453 | 454 | const acceptTx = new Transaction({ 455 | value: 0, 456 | data: data, 457 | receiver: this.contract.getAddress(), 458 | gasLimit: 20_000_000, 459 | sender: senderAddress, 460 | chainID: this.chainID 461 | }); 462 | 463 | return acceptTx; 464 | } 465 | 466 | /** 467 | * Creates a `acceptOffer` transaction with NFT/SFT tokens 468 | * @param senderAddress the address of the sender 469 | * @param offerId the id of the offer to be accepted 470 | * @param amount the amount of tokens to be bought 471 | * @param tokenIdentifier the identifier of the token for the payment 472 | * @param nonce the nonce of the token for the payment 473 | * @param paymentAmount the amount of the token for the payment 474 | */ 475 | 476 | acceptOfferWithNFT( 477 | senderAddress: IAddress, 478 | offerId: number, 479 | amount: BigNumber.Value, 480 | tokenIdentifier: string, 481 | nonce: number, 482 | paymentAmount: BigNumber.Value 483 | ): Transaction { 484 | const offerEsdtTx = new Transaction({ 485 | value: 0, 486 | data: new ContractCallPayloadBuilder() 487 | .setFunction(new ContractFunction('ESDTNFTTransfer')) 488 | .addArg(new TokenIdentifierValue(tokenIdentifier)) 489 | .addArg(new U64Value(nonce)) 490 | .addArg(new BigUIntValue(paymentAmount)) 491 | .addArg(new AddressValue(this.contract.getAddress())) 492 | .addArg(new StringValue('acceptOffer')) 493 | .addArg(new U64Value(offerId)) 494 | .addArg(new BigUIntValue(amount)) 495 | .build(), 496 | receiver: senderAddress, 497 | sender: senderAddress, 498 | gasLimit: 20_000_000, 499 | chainID: this.chainID 500 | }); 501 | return offerEsdtTx; 502 | } 503 | 504 | /** 505 | * Creates a `acceptOffer` transaction with EGLD 506 | * @param senderAddress the address of the sender 507 | * @param offerId the id of the offer to be accepted 508 | * @param amount the price of the offer for the total amount to be bought (must include the buyer fee) 509 | * @param price the price of the offer (must include the buyer fee) 510 | */ 511 | acceptOfferWithEGLD( 512 | senderAddress: IAddress, 513 | offerId: number, 514 | amount: BigNumber.Value, 515 | price: BigNumber.Value 516 | ): Transaction { 517 | const data = new ContractCallPayloadBuilder() 518 | .setFunction(new ContractFunction('acceptOffer')) 519 | .addArg(new U64Value(offerId)) 520 | .addArg(new BigUIntValue(amount)) 521 | .build(); 522 | 523 | const acceptTx = new Transaction({ 524 | value: price, 525 | data: data, 526 | receiver: this.contract.getAddress(), 527 | gasLimit: 20_000_000, 528 | sender: senderAddress, 529 | chainID: this.chainID 530 | }); 531 | 532 | return acceptTx; 533 | } 534 | 535 | /** 536 | * Creates a `acceptOffer` without payment token (Free) 537 | * @param senderAddress the address of the sender 538 | * @param offerId the id of the offer to be accepted 539 | * @param amount the amount of tokens to be bought 540 | */ 541 | acceptOfferWithNoPayment( 542 | senderAddress: IAddress, 543 | offerId: number, 544 | amount: BigNumber.Value 545 | ): Transaction { 546 | const data = new ContractCallPayloadBuilder() 547 | .setFunction(new ContractFunction('acceptOffer')) 548 | .addArg(new U64Value(offerId)) 549 | .addArg(new BigUIntValue(amount)) 550 | .build(); 551 | 552 | const acceptTx = new Transaction({ 553 | value: 0, 554 | data: data, 555 | receiver: this.contract.getAddress(), 556 | gasLimit: 12000000, 557 | sender: senderAddress, 558 | chainID: this.chainID 559 | }); 560 | 561 | return acceptTx; 562 | } 563 | 564 | /** 565 | * Creates a `cancelOffer` transaction 566 | * @param senderAddress the address of the sender 567 | * @param offerId the id of the offer to be cancelled 568 | * @param quantity the quantity of the offer to be cancelled 569 | * @param sendFundsBackToOwner default `true`, if `false` the offer will be cancelled, but the funds will be kept in the contract until withdrawal 570 | */ 571 | cancelOffer( 572 | senderAddress: IAddress, 573 | offerId: number, 574 | quantity: number, 575 | sendFundsBackToOwner = true 576 | ): Transaction { 577 | const cancelTx = new Transaction({ 578 | value: 0, 579 | data: new ContractCallPayloadBuilder() 580 | .setFunction(new ContractFunction('cancelOffer')) 581 | .addArg(new U64Value(offerId)) 582 | .addArg(new U64Value(quantity)) 583 | .addArg(new BooleanValue(sendFundsBackToOwner)) 584 | .build(), 585 | receiver: this.contract.getAddress(), 586 | gasLimit: 10000000, 587 | sender: senderAddress, 588 | chainID: this.chainID 589 | }); 590 | 591 | return cancelTx; 592 | } 593 | 594 | /** 595 | * Creates a `changeOfferPrice` transaction 596 | * @param senderAddress the address of the sender 597 | * @param offerId the id of the offer to be changed 598 | * @param newPrice the new price of the offer 599 | * @param newMinimumPaymentTokenAmount the new minimum amount of which the `sender` is willing to receive (useful in case where an offer was added and the smart contract fee was changed afterwards) 600 | */ 601 | changeOfferPrice( 602 | senderAddress: IAddress, 603 | offerId: number, 604 | newPrice: BigNumber.Value, 605 | newMinimumPaymentTokenAmount = 0 606 | ): Transaction { 607 | const changePriceTx = new Transaction({ 608 | value: 0, 609 | data: new ContractCallPayloadBuilder() 610 | .setFunction(new ContractFunction('changeOfferPrice')) 611 | .addArg(new U64Value(offerId)) 612 | .addArg(new U64Value(newPrice)) 613 | .addArg(new U64Value(newMinimumPaymentTokenAmount)) 614 | .build(), 615 | receiver: this.contract.getAddress(), 616 | gasLimit: 10000000, 617 | sender: senderAddress, 618 | chainID: this.chainID 619 | }); 620 | 621 | return changePriceTx; 622 | } 623 | 624 | /** 625 | * Creates a `withdrawCancelledOffer` transaction 626 | * @param senderAddress the address of the sender 627 | * @param offerId the id of the offer from which the funds should be withdrawn 628 | * 629 | * `offerId` must be firstly cancelled. {@link cancelOffer} 630 | */ 631 | withdrawCancelledOffer( 632 | senderAddress: IAddress, 633 | offerId: number 634 | ): Transaction { 635 | const withdrawTx = new Transaction({ 636 | value: 0, 637 | data: new ContractCallPayloadBuilder() 638 | .setFunction(new ContractFunction('withdrawCancelledOffer')) 639 | .addArg(new U64Value(offerId)) 640 | .build(), 641 | receiver: this.contract.getAddress(), 642 | gasLimit: 12000000, 643 | sender: senderAddress, 644 | chainID: this.chainID 645 | }); 646 | 647 | return withdrawTx; 648 | } 649 | } 650 | -------------------------------------------------------------------------------- /src/minter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbiRegistry, 3 | Address, 4 | AddressValue, 5 | BigUIntValue, 6 | BooleanValue, 7 | ContractCallPayloadBuilder, 8 | ContractFunction, 9 | IAddress, 10 | ResultsParser, 11 | SmartContract, 12 | StringValue, 13 | TokenIdentifierValue, 14 | Transaction, 15 | U64Value 16 | } from '@multiversx/sdk-core/out'; 17 | import { ApiNetworkProvider } from '@multiversx/sdk-network-providers/out'; 18 | import { 19 | EnvironmentsEnum, 20 | dataNftTokenIdentifier, 21 | imageService, 22 | networkConfiguration 23 | } from './config'; 24 | import { ErrContractQuery, ErrNetworkConfig } from './errors'; 25 | import BigNumber from 'bignumber.js'; 26 | import { Contract } from './contract'; 27 | 28 | export abstract class Minter extends Contract { 29 | readonly imageServiceUrl: string; 30 | 31 | protected constructor( 32 | env: string, 33 | contractAddress: IAddress, 34 | abiFile: any, 35 | timeout: number = 10000 36 | ) { 37 | super(env, contractAddress, abiFile, timeout); 38 | this.imageServiceUrl = imageService[env as EnvironmentsEnum]; 39 | } 40 | 41 | /** 42 | * Retrieves the address of the minter smart contract based on the environment 43 | */ 44 | getContractAddress(): IAddress { 45 | return this.contract.getAddress(); 46 | } 47 | 48 | /** 49 | * Retrieves the smart contract pause state 50 | */ 51 | async viewContractPauseState(): Promise { 52 | const interaction = this.contract.methodsExplicit.getIsPaused(); 53 | const query = interaction.buildQuery(); 54 | const queryResponse = await this.networkProvider.queryContract(query); 55 | const endpointDefinition = interaction.getEndpoint(); 56 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 57 | queryResponse, 58 | endpointDefinition 59 | ); 60 | if (returnCode.isSuccess()) { 61 | const returnValue = firstValue?.valueOf(); 62 | return new BooleanValue(returnValue).valueOf(); 63 | } else { 64 | throw new ErrContractQuery( 65 | 'viewContractPauseState', 66 | returnCode.toString() 67 | ); 68 | } 69 | } 70 | 71 | /** 72 | * Retrieves the minter whitelist 73 | */ 74 | async viewWhitelist(): Promise { 75 | const interaction = this.contract.methodsExplicit.getWhiteList(); 76 | const query = interaction.buildQuery(); 77 | const queryResponse = await this.networkProvider.queryContract(query); 78 | const endpointDefinition = interaction.getEndpoint(); 79 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 80 | queryResponse, 81 | endpointDefinition 82 | ); 83 | if (returnCode.isSuccess()) { 84 | const returnValue = firstValue?.valueOf(); 85 | const whitelist: string[] = returnValue.map((addres: any) => 86 | addres.toString() 87 | ); 88 | return whitelist; 89 | } else { 90 | throw new ErrContractQuery('viewWhitelist', returnCode.toString()); 91 | } 92 | } 93 | 94 | /** 95 | * Retrieves a list of addresses that are frozen for collection 96 | */ 97 | async viewCollectionFrozenAddresses(): Promise { 98 | const interaction = this.contract.methodsExplicit.getCollectionFrozenList(); 99 | const query = interaction.buildQuery(); 100 | const queryResponse = await this.networkProvider.queryContract(query); 101 | const endpointDefinition = interaction.getEndpoint(); 102 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 103 | queryResponse, 104 | endpointDefinition 105 | ); 106 | if (returnCode.isSuccess()) { 107 | const returnValue = firstValue?.valueOf(); 108 | const frozenAddresses: string[] = returnValue.map((addres: any) => 109 | addres.toString() 110 | ); 111 | return frozenAddresses; 112 | } else { 113 | throw new ErrContractQuery( 114 | 'viewCollectionFrozenAddresses', 115 | returnCode.toString() 116 | ); 117 | } 118 | } 119 | 120 | /** 121 | * Creates a `burn` transaction 122 | * @param senderAddress the address of the user 123 | * @param dataNftNonce the nonce of the DataNFT-FT 124 | * @param quantityToBurn the quantity to burn 125 | * @param dataNftIdentifier the DataNFT-FT token identifier (default = `DATA-NFT-FT` token identifier based on the {@link EnvironmentsEnum}) 126 | */ 127 | burn( 128 | senderAddress: IAddress, 129 | dataNftNonce: number, 130 | quantityToBurn: BigNumber.Value, 131 | dataNftIdentifier = dataNftTokenIdentifier[this.env as EnvironmentsEnum] 132 | ): Transaction { 133 | const burnTx = new Transaction({ 134 | value: 0, 135 | data: new ContractCallPayloadBuilder() 136 | .setFunction(new ContractFunction('ESDTNFTTransfer')) 137 | .addArg(new TokenIdentifierValue(dataNftIdentifier)) 138 | .addArg(new U64Value(dataNftNonce)) 139 | .addArg(new BigUIntValue(quantityToBurn)) 140 | .addArg(new AddressValue(this.contract.getAddress())) 141 | .addArg(new StringValue('burn')) 142 | .build(), 143 | receiver: senderAddress, 144 | sender: senderAddress, 145 | gasLimit: 12000000, 146 | chainID: this.chainID 147 | }); 148 | return burnTx; 149 | } 150 | 151 | /** 152 | * Creates a setLocalRoles transaction for the contract 153 | * @param senderAddress The address of the sender, must be the admin of the contract 154 | */ 155 | setLocalRoles(senderAddress: IAddress): Transaction { 156 | const setLocalRolesTx = new Transaction({ 157 | value: 0, 158 | data: new ContractCallPayloadBuilder() 159 | .setFunction(new ContractFunction('setLocalRoles')) 160 | .build(), 161 | receiver: this.contract.getAddress(), 162 | gasLimit: 100000000, 163 | sender: senderAddress, 164 | chainID: this.chainID 165 | }); 166 | return setLocalRolesTx; 167 | } 168 | 169 | /** Creates a pause transaction for the contract 170 | * @param senderAddress The address of the sender, must be the admin of the contract 171 | */ 172 | pauseContract(senderAddress: IAddress): Transaction { 173 | const pauseContractTx = new Transaction({ 174 | value: 0, 175 | data: new ContractCallPayloadBuilder() 176 | .setFunction(new ContractFunction('setIsPaused')) 177 | .addArg(new BooleanValue(true)) 178 | .build(), 179 | receiver: this.contract.getAddress(), 180 | gasLimit: 6000000, 181 | sender: senderAddress, 182 | chainID: this.chainID 183 | }); 184 | 185 | return pauseContractTx; 186 | } 187 | 188 | /** Creates a unpause transaction for the contract 189 | * @param senderAddress The address of the sender, must be the admin of the contract 190 | */ 191 | unpauseContract(senderAddress: IAddress): Transaction { 192 | const unpauseContractTx = new Transaction({ 193 | value: 0, 194 | data: new ContractCallPayloadBuilder() 195 | .setFunction(new ContractFunction('setIsPaused')) 196 | .addArg(new BooleanValue(false)) 197 | .build(), 198 | receiver: this.contract.getAddress(), 199 | gasLimit: 6000000, 200 | sender: senderAddress, 201 | chainID: this.chainID 202 | }); 203 | 204 | return unpauseContractTx; 205 | } 206 | 207 | /** 208 | * 209 | * @param senderAddress The address of the sender, must be the admin of the contract 210 | * @param minRoyalties The minimum royalties to set for minting 211 | * @param maxRoyalties The maximum royalties to set for minting 212 | * 213 | * Remarks: The royalties are set in percentage (e.g. 100% = 10000) 214 | */ 215 | setRoyaltiesLimits( 216 | senderAddress: IAddress, 217 | minRoyalties: BigNumber.Value, 218 | maxRoyalties: BigNumber.Value 219 | ): Transaction { 220 | const setRoyaltiesLimitsTx = new Transaction({ 221 | value: 0, 222 | data: new ContractCallPayloadBuilder() 223 | .setFunction(new ContractFunction('setRoyaltiesLimits')) 224 | .addArg(new BigUIntValue(minRoyalties)) 225 | .addArg(new BigUIntValue(maxRoyalties)) 226 | .build(), 227 | receiver: this.contract.getAddress(), 228 | gasLimit: 6000000, 229 | sender: senderAddress, 230 | chainID: this.chainID 231 | }); 232 | return setRoyaltiesLimitsTx; 233 | } 234 | 235 | /** Creates a set mint tax transaction for the contract 236 | * @param senderAddress The address of the sender, must be the admin of the contract 237 | * @param is_enabled A boolean value to set if whitelist is enabled or not 238 | */ 239 | setWhitelistIsEnabled( 240 | senderAddress: IAddress, 241 | is_enabled: boolean 242 | ): Transaction { 243 | const setWhitelistIsEnabledTx = new Transaction({ 244 | value: 0, 245 | data: new ContractCallPayloadBuilder() 246 | .setFunction(new ContractFunction('setWhiteListEnabled')) 247 | .addArg(new BooleanValue(is_enabled)) 248 | .build(), 249 | receiver: this.contract.getAddress(), 250 | gasLimit: 6000000, 251 | sender: senderAddress, 252 | chainID: this.chainID 253 | }); 254 | return setWhitelistIsEnabledTx; 255 | } 256 | 257 | /** Creates a whitelist transaction for the contract 258 | * @param senderAddress The address of the sender, must be the admin of the contract 259 | * @param addresses The addresses to whitelist 260 | * @param extraGas The extra gas to add to the transaction 261 | */ 262 | 263 | whitelist( 264 | senderAddress: IAddress, 265 | addresses: string[], 266 | extraGas = 0 267 | ): Transaction { 268 | const whitelistTx = new Transaction({ 269 | value: 0, 270 | data: new ContractCallPayloadBuilder() 271 | .setFunction(new ContractFunction('setWhiteListSpots')) 272 | .setArgs( 273 | addresses.map((address) => new AddressValue(new Address(address))) 274 | ) 275 | .build(), 276 | receiver: this.contract.getAddress(), 277 | gasLimit: 50000000 + extraGas, 278 | sender: senderAddress, 279 | chainID: this.chainID 280 | }); 281 | return whitelistTx; 282 | } 283 | 284 | /** Creates a remove whitelist transaction for the contract 285 | * @param senderAddress The address of the sender, must be the admin of the contract 286 | * @param addresses The addresses to remove from the whitelist 287 | * @param extraGas The extra gas to add to the transaction 288 | */ 289 | removeWhitelist( 290 | senderAddress: IAddress, 291 | addresses: string[], 292 | extraGas = 0 293 | ): Transaction { 294 | const removeWhitelistTx = new Transaction({ 295 | value: 0, 296 | data: new ContractCallPayloadBuilder() 297 | .setFunction(new ContractFunction('removeWhiteListSpots')) 298 | .setArgs( 299 | addresses.map((address) => new AddressValue(new Address(address))) 300 | ) 301 | .build(), 302 | receiver: this.contract.getAddress(), 303 | gasLimit: 50000000 + extraGas, 304 | sender: senderAddress, 305 | chainID: this.chainID 306 | }); 307 | return removeWhitelistTx; 308 | } 309 | 310 | /** Creates a set mint time limit transaction for the contract 311 | * @param senderAddress The address of the sender, must be the admin of the contract 312 | * @param timeLimit(seconds) The time limit to set between mints 313 | */ 314 | setMintTimeLimit(senderAddress: IAddress, timeLimit: number): Transaction { 315 | const setMintTimeLimitTx = new Transaction({ 316 | value: 0, 317 | data: new ContractCallPayloadBuilder() 318 | .setFunction(new ContractFunction('setMintTimeLimit')) 319 | .addArg(new U64Value(timeLimit)) 320 | .build(), 321 | receiver: this.contract.getAddress(), 322 | gasLimit: 6000000, 323 | sender: senderAddress, 324 | chainID: this.chainID 325 | }); 326 | return setMintTimeLimitTx; 327 | } 328 | 329 | /** Sets a new administrator for the contract 330 | * @param senderAddress The address of the sender, must be the admin of the contract 331 | * @param newAdministrator The address of the new administrator 332 | */ 333 | setAdministrator( 334 | senderAddress: IAddress, 335 | newAdministrator: IAddress 336 | ): Transaction { 337 | const setAdministratorTx = new Transaction({ 338 | value: 0, 339 | data: new ContractCallPayloadBuilder() 340 | .setFunction(new ContractFunction('setAdministrator')) 341 | .addArg(new AddressValue(newAdministrator)) 342 | .build(), 343 | receiver: this.contract.getAddress(), 344 | gasLimit: 6000000, 345 | sender: senderAddress, 346 | chainID: this.chainID 347 | }); 348 | return setAdministratorTx; 349 | } 350 | 351 | // Collection management methods 352 | 353 | /** 354 | * Pause collection transaction 355 | * @param senderAddress The address of the sender, must be the admin or owner of the contract 356 | */ 357 | pauseCollection(senderAddress: IAddress): Transaction { 358 | const pauseCollectionTx = new Transaction({ 359 | value: 0, 360 | data: new ContractCallPayloadBuilder() 361 | .setFunction(new ContractFunction('pause')) 362 | .build(), 363 | receiver: this.contract.getAddress(), 364 | gasLimit: 100000000, 365 | sender: senderAddress, 366 | chainID: this.chainID 367 | }); 368 | return pauseCollectionTx; 369 | } 370 | 371 | /** 372 | * Unpause collection transaction 373 | * @param senderAddress The address of the sender, must be the admin or owner of the contract 374 | */ 375 | unpauseCollection(senderAddress: IAddress): Transaction { 376 | const unpauseCollectionTx = new Transaction({ 377 | value: 0, 378 | data: new ContractCallPayloadBuilder() 379 | .setFunction(new ContractFunction('unpause')) 380 | .build(), 381 | receiver: this.contract.getAddress(), 382 | gasLimit: 100000000, 383 | sender: senderAddress, 384 | chainID: this.chainID 385 | }); 386 | 387 | return unpauseCollectionTx; 388 | } 389 | 390 | /** 391 | * Freeze transaction 392 | * @param senderAddress The address of the sender, must be the admin or owner of the contract 393 | */ 394 | freeze(senderAddress: IAddress, freezeAddress: IAddress): Transaction { 395 | const freezeTx = new Transaction({ 396 | value: 0, 397 | data: new ContractCallPayloadBuilder() 398 | .setFunction(new ContractFunction('freeze')) 399 | .addArg(new AddressValue(freezeAddress)) 400 | .build(), 401 | receiver: this.contract.getAddress(), 402 | gasLimit: 100000000, 403 | sender: senderAddress, 404 | chainID: this.chainID 405 | }); 406 | 407 | return freezeTx; 408 | } 409 | 410 | /** 411 | * Unfreeze transaction 412 | * @param senderAddress The address of the sender, must be the admin or owner of the contract 413 | */ 414 | unfreeze(senderAddress: IAddress, unfreezeAddress: IAddress): Transaction { 415 | const unfreezeTx = new Transaction({ 416 | value: 0, 417 | data: new ContractCallPayloadBuilder() 418 | .setFunction(new ContractFunction('unfreeze')) 419 | .addArg(new AddressValue(unfreezeAddress)) 420 | .build(), 421 | receiver: this.contract.getAddress(), 422 | gasLimit: 100000000, 423 | sender: senderAddress, 424 | chainID: this.chainID 425 | }); 426 | 427 | return unfreezeTx; 428 | } 429 | 430 | /** 431 | * 432 | * @param senderAddress The address of the sender, must be the admin or owner of the contract 433 | * @param nonce The nonce of the token to freeze for `freezeAddress` 434 | * @param freezeAddress The address to freeze 435 | */ 436 | freezeSingleNFT( 437 | senderAddress: IAddress, 438 | nonce: number, 439 | freezeAddress: IAddress 440 | ): Transaction { 441 | const freezeSingleNFTTx = new Transaction({ 442 | value: 0, 443 | data: new ContractCallPayloadBuilder() 444 | .setFunction(new ContractFunction('freezeSingleNFT')) 445 | .addArg(new U64Value(nonce)) 446 | .addArg(new AddressValue(freezeAddress)) 447 | .build(), 448 | receiver: this.contract.getAddress(), 449 | gasLimit: 100000000, 450 | sender: senderAddress, 451 | chainID: this.chainID 452 | }); 453 | return freezeSingleNFTTx; 454 | } 455 | 456 | /** 457 | * 458 | * @param senderAddress The address of the sender, must be the admin or owner of the contract 459 | * @param nonce The nonce of the token to unfreeze for `unfreezeAddress` 460 | * @param unfreezeAddress The address to unfreeze 461 | */ 462 | unFreezeSingleNFT( 463 | senderAddress: IAddress, 464 | nonce: number, 465 | unfreezeAddress: IAddress 466 | ): Transaction { 467 | const unFreezeSingleNFTTx = new Transaction({ 468 | value: 0, 469 | data: new ContractCallPayloadBuilder() 470 | .setFunction(new ContractFunction('unFreezeSingleNFT')) 471 | .addArg(new U64Value(nonce)) 472 | .addArg(new AddressValue(unfreezeAddress)) 473 | .build(), 474 | receiver: this.contract.getAddress(), 475 | gasLimit: 100000000, 476 | sender: senderAddress, 477 | chainID: this.chainID 478 | }); 479 | return unFreezeSingleNFTTx; 480 | } 481 | 482 | /** 483 | * 484 | * @param senderAddress The address of the sender, must be the admin or owner of the contract 485 | * @param nonce The nonce of the token to wipe for `wipeAddress` 486 | * @param wipeAddress The address to wipe from 487 | * Important: This will wipe all NFTs from the address 488 | * Note: The nonce must be freezed before wiping 489 | */ 490 | wipeSingleNFT( 491 | senderAddress: IAddress, 492 | nonce: number, 493 | wipeAddress: IAddress 494 | ): Transaction { 495 | const wipeSingleNFTTx = new Transaction({ 496 | value: 0, 497 | data: new ContractCallPayloadBuilder() 498 | .setFunction(new ContractFunction('wipeSingleNFT')) 499 | .addArg(new U64Value(nonce)) 500 | .addArg(new AddressValue(wipeAddress)) 501 | .build(), 502 | receiver: this.contract.getAddress(), 503 | gasLimit: 100000000, 504 | sender: senderAddress, 505 | chainID: this.chainID 506 | }); 507 | return wipeSingleNFTTx; 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /src/nft-minter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AddressValue, 3 | BigUIntValue, 4 | BooleanValue, 5 | ContractCallPayloadBuilder, 6 | ContractFunction, 7 | IAddress, 8 | ResultsParser, 9 | StringValue, 10 | TokenIdentifierValue, 11 | Transaction, 12 | U64Value 13 | } from '@multiversx/sdk-core/out'; 14 | import dataNftLeaseAbi from './abis/data-nft-lease.abi.json'; 15 | import { 16 | createFileFromUrl, 17 | dataNFTDataStreamAdvertise, 18 | storeToIpfs 19 | } from './common/mint-utils'; 20 | import { checkTraitsUrl, checkUrlIsUp } from './common/utils'; 21 | import { EnvironmentsEnum, itheumTokenIdentifier } from './config'; 22 | import { ErrArgumentNotSet, ErrContractQuery } from './errors'; 23 | import { ContractConfiguration, NftMinterRequirements } from './interfaces'; 24 | import { Minter } from './minter'; 25 | import BigNumber from 'bignumber.js'; 26 | 27 | export class NftMinter extends Minter { 28 | /** 29 | * Creates a new instance of the `NftMinter` class, which is used to interact with the factory generated smart contract. 30 | * @param env 'devnet' | 'mainnet' | 'testnet' 31 | * @param contractAddress The address of the factory generated smart contract 32 | * @param timeout Timeout for the network provider (DEFAULT = 10000ms) 33 | */ 34 | constructor(env: string, contractAddress: IAddress, timeout: number = 10000) { 35 | super(env, contractAddress, dataNftLeaseAbi, timeout); 36 | } 37 | 38 | /** 39 | * Creates an initialize contract transaction for the contract 40 | * @param senderAddress The address of the sender, must be the admin of the contract 41 | * @param collectionName The name of the NFT collection 42 | * @param tokenTicker The ticker of the NFT collection 43 | * @param mintLimit(seconds)- The mint limit between mints 44 | * @param requireMintTax - A boolean value to set if the mint tax is required or not 45 | * @param options - If `requireMintTax` is true, the `options` object must contain the `taxTokenIdentifier` and `taxTokenAmount` 46 | */ 47 | initializeContract( 48 | senderAddress: IAddress, 49 | collectionName: string, 50 | tokenTicker: string, 51 | mintLimit: number, 52 | requireMintTax: boolean, 53 | claimsAddress: IAddress, 54 | options?: { 55 | taxTokenIdentifier: string; 56 | taxTokenAmount: BigNumber.Value; 57 | } 58 | ): Transaction { 59 | let data; 60 | if (requireMintTax && options) { 61 | data = new ContractCallPayloadBuilder() 62 | .setFunction(new ContractFunction('initializeContract')) 63 | .addArg(new StringValue(collectionName)) 64 | .addArg(new StringValue(tokenTicker)) 65 | .addArg(new BigUIntValue(mintLimit)) 66 | .addArg(new BooleanValue(requireMintTax)) 67 | .addArg(new AddressValue(claimsAddress)) 68 | .addArg(new TokenIdentifierValue(options.taxTokenIdentifier)) 69 | .addArg(new BigUIntValue(options.taxTokenAmount)) 70 | .build(); 71 | } else { 72 | data = new ContractCallPayloadBuilder() 73 | .setFunction(new ContractFunction('initializeContract')) 74 | .addArg(new StringValue(collectionName)) 75 | .addArg(new StringValue(tokenTicker)) 76 | .addArg(new BigUIntValue(mintLimit)) 77 | .addArg(new BooleanValue(requireMintTax)) 78 | .addArg(new AddressValue(claimsAddress)) 79 | .build(); 80 | } 81 | 82 | const initializeContractTx = new Transaction({ 83 | value: 50000000000000000, 84 | data: data, 85 | receiver: this.contract.getAddress(), 86 | gasLimit: 100000000, 87 | sender: senderAddress, 88 | chainID: this.chainID 89 | }); 90 | return initializeContractTx; 91 | } 92 | 93 | /** 94 | * Creates a updateAttributes transaction for the contract 95 | * @param senderAddress The address of the sender, must be the admin of the contract 96 | * @param tokenIdentifier The token identifier of the data nft to update attributes 97 | * @param nonce The nonce of the token to update attributes 98 | * @param attributes The new attributes to update 99 | * @param quantity The quantity of the token to update attributes (default: 1) 100 | */ 101 | updateAttributes( 102 | senderAddress: IAddress, 103 | tokenIdentifier: string, 104 | nonce: number, 105 | attributes: { 106 | dataMarshalUrl: string; 107 | dataStreamUrl: string; 108 | dataPreviewUrl: string; 109 | creator: IAddress; 110 | title: string; 111 | description: string; 112 | }, 113 | quantity = 1 114 | ): Transaction { 115 | const updateAttributesTx = new Transaction({ 116 | value: 0, 117 | data: new ContractCallPayloadBuilder() 118 | .setFunction(new ContractFunction('ESDTNFTTransfer')) 119 | .addArg(new TokenIdentifierValue(tokenIdentifier)) 120 | .addArg(new U64Value(nonce)) 121 | .addArg(new U64Value(quantity)) 122 | .addArg(new AddressValue(this.contract.getAddress())) 123 | .addArg(new StringValue('updateAttributes')) 124 | .addArg(new StringValue(attributes.dataMarshalUrl)) 125 | .addArg(new StringValue(attributes.dataStreamUrl)) 126 | .addArg(new StringValue(attributes.dataPreviewUrl)) 127 | .addArg(new AddressValue(attributes.creator)) 128 | .addArg(new StringValue(attributes.title)) 129 | .addArg(new StringValue(attributes.description)) 130 | .build(), 131 | receiver: senderAddress, 132 | gasLimit: 12000000, 133 | sender: senderAddress, 134 | chainID: this.chainID 135 | }); 136 | return updateAttributesTx; 137 | } 138 | 139 | /** 140 | * Creates a `mint` transaction 141 | * 142 | * NOTE: The `dataStreamUrl` is being encrypted and the `media` and `metadata` urls are build and uploaded to IPFS 143 | * 144 | * NOTE: The `options.nftStorageToken` is required when not using custom image and traits, when using custom image and traits the traits should be compliant with the [Traits](https://github.com/Itheum/sdk-mx-data-nft#traits-structure) structure 145 | * 146 | * For more information, see the [README documentation](https://github.com/Itheum/sdk-mx-data-nft#create-a-mint-transaction). 147 | * 148 | * @param senderAddress the address of the user 149 | * @param tokenName the name of the DataNFT-FT. Between 3 and 20 alphanumeric characters, no spaces. 150 | * @param dataMarshalUrl the url of the data marshal. A live HTTPS URL that returns a 200 OK HTTP code. 151 | * @param dataStreamUrl the url of the data stream to be encrypted. A live HTTPS URL that returns a 200 OK HTTP code. 152 | * @param dataPreviewUrl the url of the data preview. A live HTTPS URL that returns a 200 OK HTTP code. 153 | * @param royalties the royalties to be set for the Data NFT-FT. A number between 0 and 50. This equates to a % value. e.g. 10% 154 | * @param datasetTitle the title of the dataset. Between 10 and 60 alphanumeric characters. 155 | * @param datasetDescription the description of the dataset. Between 10 and 400 alphanumeric characters. 156 | * @param options [optional] below parameters are optional or required based on use case 157 | * - imageUrl: the URL of the image for the Data NFT 158 | * - traitsUrl: the URL of the traits for the Data NFT 159 | * - nftStorageToken: the nft storage token to be used to upload the image and metadata to IPFS 160 | * - antiSpamTokenIdentifier: the anti spam token identifier to be used for the minting 161 | * - antiSpamTax: the anti spam tax to be set for the Data NFT-FT with decimals. Needs to be greater than 0 and should be obtained in real time via {@link viewMinterRequirements} prior to calling mint. 162 | * - extraAssets [optional] extra URIs to attached to the NFT. Can be media files, documents, etc. These URIs are public 163 | * - imgGenBg: [optional] the custom series bg to influence the image generation service 164 | * - imgGenSet: [optional] the custom series layer set to influence the image generation service 165 | */ 166 | async mint( 167 | senderAddress: IAddress, 168 | tokenName: string, 169 | dataMarshalUrl: string, 170 | dataStreamUrl: string, 171 | dataPreviewUrl: string, 172 | royalties: number, 173 | datasetTitle: string, 174 | datasetDescription: string, 175 | options?: { 176 | imageUrl?: string; 177 | traitsUrl?: string; 178 | nftStorageToken?: string; 179 | antiSpamTokenIdentifier?: string; 180 | antiSpamTax?: BigNumber.Value; 181 | extraAssets?: string[]; 182 | imgGenBg?: string; 183 | imgGenSet?: string; 184 | } 185 | ): Promise { 186 | const { 187 | imageUrl, 188 | traitsUrl, 189 | nftStorageToken, 190 | antiSpamTokenIdentifier, 191 | antiSpamTax, 192 | extraAssets, 193 | imgGenBg, 194 | imgGenSet 195 | } = options ?? {}; 196 | 197 | // deep validate all mandatory URLs 198 | try { 199 | await checkUrlIsUp(dataPreviewUrl, [200]); 200 | await checkUrlIsUp(dataMarshalUrl + '/health-check', [200]); 201 | } catch (error) { 202 | throw error; 203 | } 204 | 205 | let imageOnIpfsUrl: string; 206 | let metadataOnIpfsUrl: string; 207 | 208 | const { dataNftHash, dataNftStreamUrlEncrypted } = 209 | await dataNFTDataStreamAdvertise( 210 | dataStreamUrl, 211 | dataMarshalUrl, 212 | this.getContractAddress().bech32() // the minter is the Creator 213 | ); 214 | 215 | if (!imageUrl) { 216 | if (!nftStorageToken) { 217 | throw new ErrArgumentNotSet( 218 | 'nftStorageToken', 219 | 'NFT Storage token is required when not using custom image and traits' 220 | ); 221 | } 222 | 223 | // create the img generative service API based on user options 224 | let imgGenServiceApi = `${this.imageServiceUrl}/v1/generateNFTArt?hash=${dataNftHash}`; 225 | 226 | if (imgGenBg && imgGenBg.trim() !== '') { 227 | imgGenServiceApi += `&bg=${imgGenBg.trim()}`; 228 | } 229 | 230 | if (imgGenSet && imgGenSet.trim() !== '') { 231 | imgGenServiceApi += `&set=${imgGenSet.trim()}`; 232 | } 233 | 234 | const { image, traits } = await createFileFromUrl( 235 | imgGenServiceApi, 236 | datasetTitle, 237 | datasetDescription, 238 | dataPreviewUrl, 239 | senderAddress.bech32(), 240 | extraAssets ?? [] 241 | ); 242 | 243 | const { 244 | imageOnIpfsUrl: imageIpfsUrl, 245 | metadataOnIpfsUrl: metadataIpfsUrl 246 | } = await storeToIpfs(nftStorageToken, traits, image); 247 | 248 | imageOnIpfsUrl = imageIpfsUrl; 249 | metadataOnIpfsUrl = metadataIpfsUrl; 250 | } else { 251 | if (!traitsUrl) { 252 | throw new ErrArgumentNotSet( 253 | 'traitsUrl', 254 | 'Traits URL is required when using custom image' 255 | ); 256 | } 257 | 258 | await checkTraitsUrl(traitsUrl); 259 | 260 | imageOnIpfsUrl = imageUrl; 261 | metadataOnIpfsUrl = traitsUrl; 262 | } 263 | 264 | let data; 265 | if ( 266 | antiSpamTax && 267 | antiSpamTokenIdentifier && 268 | antiSpamTokenIdentifier != 'EGLD' && 269 | antiSpamTax > BigNumber(0) 270 | ) { 271 | data = new ContractCallPayloadBuilder() 272 | .setFunction(new ContractFunction('ESDTTransfer')) 273 | .addArg(new TokenIdentifierValue(antiSpamTokenIdentifier)) 274 | .addArg(new BigUIntValue(antiSpamTax)) 275 | .addArg(new StringValue('mint')); 276 | } else { 277 | data = new ContractCallPayloadBuilder().setFunction( 278 | new ContractFunction('mint') 279 | ); 280 | } 281 | 282 | data 283 | .addArg(new StringValue(tokenName)) 284 | .addArg(new StringValue(imageOnIpfsUrl)) 285 | .addArg(new StringValue(metadataOnIpfsUrl)) 286 | .addArg(new StringValue(dataMarshalUrl)) 287 | .addArg(new StringValue(dataNftStreamUrlEncrypted)) 288 | .addArg(new StringValue(dataPreviewUrl)) 289 | .addArg(new U64Value(royalties)) 290 | .addArg(new StringValue(datasetTitle)) 291 | .addArg(new StringValue(datasetDescription)); 292 | 293 | for (const extraAsset of extraAssets ?? []) { 294 | data.addArg(new StringValue(extraAsset)); 295 | } 296 | 297 | const mintTx = new Transaction({ 298 | value: antiSpamTokenIdentifier == 'EGLD' ? antiSpamTax : 0, 299 | data: data.build(), 300 | sender: senderAddress, 301 | receiver: this.contract.getAddress(), 302 | gasLimit: 130_000_000, 303 | chainID: this.chainID 304 | }); 305 | 306 | return mintTx; 307 | } 308 | 309 | /** 310 | * Creates a setTransferRoles transaction for the contract 311 | * @param senderAddress The address of the sender, must be the admin of the contract 312 | * @param address The address to set the transfer roles 313 | */ 314 | setTransferRole(senderAddress: IAddress, address: IAddress): Transaction { 315 | const setTransferRolesTx = new Transaction({ 316 | value: 0, 317 | data: new ContractCallPayloadBuilder() 318 | .setFunction(new ContractFunction('setTransferRole')) 319 | .addArg(new AddressValue(address)) 320 | .build(), 321 | receiver: this.contract.getAddress(), 322 | gasLimit: 10000000, 323 | sender: senderAddress, 324 | chainID: this.chainID 325 | }); 326 | return setTransferRolesTx; 327 | } 328 | 329 | /** 330 | * Creates an unsetTransferRoles transaction for the contract 331 | * @param senderAddress The address of the sender, must be the admin of the contract 332 | * @param address The address to unset the transfer roles 333 | */ 334 | unsetTransferRole(senderAddress: IAddress, address: IAddress): Transaction { 335 | const unsetTransferRolesTx = new Transaction({ 336 | value: 0, 337 | data: new ContractCallPayloadBuilder() 338 | .setFunction(new ContractFunction('unsetTransferRole')) 339 | .addArg(new AddressValue(address)) 340 | .build(), 341 | receiver: this.contract.getAddress(), 342 | gasLimit: 10000000, 343 | sender: senderAddress, 344 | chainID: this.chainID 345 | }); 346 | return unsetTransferRolesTx; 347 | } 348 | 349 | /** Creates a set mint tax transaction for the contract 350 | * @param senderAddress The address of the sender, must be the admin of the contract 351 | * @param is_required A boolean value to set if the mint tax is required or not 352 | */ 353 | setMintTaxIsRequired( 354 | senderAddress: IAddress, 355 | is_required: boolean 356 | ): Transaction { 357 | const setMintTaxIsRequiredTx = new Transaction({ 358 | value: 0, 359 | data: new ContractCallPayloadBuilder() 360 | .setFunction(new ContractFunction('setTaxIsRequired')) 361 | .addArg(new BooleanValue(is_required)) 362 | .build(), 363 | receiver: this.contract.getAddress(), 364 | gasLimit: 10000000, 365 | sender: senderAddress, 366 | chainID: this.chainID 367 | }); 368 | 369 | return setMintTaxIsRequiredTx; 370 | } 371 | 372 | /** Sets the claim address for the contract 373 | * @param senderAddress The address of the sender, must be the admin of the contract 374 | * @param claimsAddress The claims address 375 | */ 376 | setClaimsAddress( 377 | senderAddress: IAddress, 378 | claimsAddress: IAddress 379 | ): Transaction { 380 | const setClaimsAddressTx = new Transaction({ 381 | value: 0, 382 | data: new ContractCallPayloadBuilder() 383 | .setFunction(new ContractFunction('setClaimsAddress')) 384 | .addArg(new AddressValue(claimsAddress)) 385 | .build(), 386 | receiver: this.contract.getAddress(), 387 | gasLimit: 10000000, 388 | sender: senderAddress, 389 | chainID: this.chainID 390 | }); 391 | return setClaimsAddressTx; 392 | } 393 | 394 | /** Creates a claim royalties transaction for the contract 395 | * @param senderAddress The address of the sender, must be the admin of the contract 396 | * @param tokenIdentifier The token identifier of the token to claim royalties 397 | * @param nonce The nonce of the token to claim royalties (default: 0 for ESDT) 398 | */ 399 | claimRoyalties( 400 | senderAddress: IAddress, 401 | tokenIdentifier: string, 402 | nonce = 0 403 | ): Transaction { 404 | const claimRoyaltiesTx = new Transaction({ 405 | value: 0, 406 | data: new ContractCallPayloadBuilder() 407 | .setFunction(new ContractFunction('claimRoyalties')) 408 | .addArg(new TokenIdentifierValue(tokenIdentifier)) 409 | .addArg(new BigUIntValue(nonce)) 410 | .build(), 411 | receiver: this.contract.getAddress(), 412 | gasLimit: 10000000, 413 | sender: senderAddress, 414 | chainID: this.chainID 415 | }); 416 | return claimRoyaltiesTx; 417 | } 418 | 419 | /** 420 | * Retrieves the smart contract configuration 421 | */ 422 | async viewContractConfiguration(): Promise { 423 | const interaction = 424 | this.contract.methodsExplicit.getContractConfiguration(); 425 | const query = interaction.buildQuery(); 426 | const queryResponse = await this.networkProvider.queryContract(query); 427 | const endpointDefinition = interaction.getEndpoint(); 428 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 429 | queryResponse, 430 | endpointDefinition 431 | ); 432 | if (returnCode.isSuccess()) { 433 | const returnValue = firstValue?.valueOf(); 434 | const contractConfiguration: ContractConfiguration = { 435 | tokenIdentifier: returnValue?.token_identifier.toString(), 436 | mintedTokens: returnValue?.minted_tokens.toNumber(), 437 | isTaxRequired: returnValue?.tax_required as boolean, 438 | maxRoyalties: returnValue?.max_royalties.toNumber(), 439 | minRoyalties: returnValue?.min_royalties.toNumber(), 440 | mintTimeLimit: returnValue?.mint_time_limit.toNumber(), 441 | isWhitelistEnabled: returnValue?.is_whitelist_enabled as boolean, 442 | isContractPaused: returnValue?.is_paused as boolean, 443 | rolesAreSet: returnValue?.roles_are_set as boolean, 444 | claimsAddress: returnValue?.claims_address.toString(), 445 | administratorAddress: returnValue?.administrator_address.toString(), 446 | taxToken: returnValue?.tax_token.toString() 447 | }; 448 | return contractConfiguration; 449 | } else { 450 | throw new ErrContractQuery( 451 | 'viewContractConfiguration', 452 | returnCode.toString() 453 | ); 454 | } 455 | } 456 | 457 | /** 458 | * Retrieves the addresses with transfer roles for contract collection 459 | */ 460 | async viewTransferRoles(): Promise { 461 | const interaction = 462 | this.contract.methodsExplicit.getAddressesWithTransferRole(); 463 | const query = interaction.buildQuery(); 464 | const queryResponse = await this.networkProvider.queryContract(query); 465 | const endpointDefinition = interaction.getEndpoint(); 466 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 467 | queryResponse, 468 | endpointDefinition 469 | ); 470 | if (returnCode.isSuccess()) { 471 | const returnValue = firstValue?.valueOf(); 472 | const addressesWithTransferRole: string[] = returnValue?.map( 473 | (address: any) => address.toString() 474 | ); 475 | return addressesWithTransferRole; 476 | } else { 477 | throw new ErrContractQuery('viewTransferRoles', returnCode.toString()); 478 | } 479 | } 480 | 481 | /** 482 | * Retrieves a list of nonces that are frozen 483 | */ 484 | async viewFrozenNonces(): Promise { 485 | const interaction = this.contract.methodsExplicit.getFrozenNonces(); 486 | const query = interaction.buildQuery(); 487 | const queryResponse = await this.networkProvider.queryContract(query); 488 | const endpointDefinition = interaction.getEndpoint(); 489 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 490 | queryResponse, 491 | endpointDefinition 492 | ); 493 | if (returnCode.isSuccess()) { 494 | const returnValue = firstValue?.valueOf(); 495 | const frozenNonces: number[] = returnValue.map((nonce: any) => 496 | nonce.toNumber() 497 | ); 498 | return frozenNonces; 499 | } else { 500 | throw new ErrContractQuery('viewFrozenNonces', returnCode.toString()); 501 | } 502 | } 503 | 504 | /** 505 | * Retrieves the address with update attributes roles for contract collection 506 | */ 507 | async viewUpdateAttributesRoles(): Promise { 508 | const interaction = 509 | this.contract.methodsExplicit.getAddressesWithUpdateAttributesRole(); 510 | const query = interaction.buildQuery(); 511 | const queryResponse = await this.networkProvider.queryContract(query); 512 | const endpointDefinition = interaction.getEndpoint(); 513 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 514 | queryResponse, 515 | endpointDefinition 516 | ); 517 | if (returnCode.isSuccess()) { 518 | const returnValue = firstValue?.valueOf(); 519 | const addressesWithUpdateAttributesRole: string[] = returnValue?.map( 520 | (address: any) => address.toString() 521 | ); 522 | return addressesWithUpdateAttributesRole; 523 | } else { 524 | throw new ErrContractQuery( 525 | 'viewUpdateAttributesRoles', 526 | returnCode.toString() 527 | ); 528 | } 529 | } 530 | 531 | /** 532 | * Retrieves the minter smart contract requirements for the given user 533 | * @param address the address of the user 534 | * @param taxToken the tax token to be used for the minting (default = `ITHEUM` token identifier based on the {@link EnvironmentsEnum}) 535 | */ 536 | async viewMinterRequirements( 537 | address: IAddress, 538 | taxToken = itheumTokenIdentifier[this.env as EnvironmentsEnum] 539 | ): Promise { 540 | const interaction = this.contract.methodsExplicit.getUserDataOut([ 541 | new AddressValue(address), 542 | new TokenIdentifierValue(taxToken) 543 | ]); 544 | const query = interaction.buildQuery(); 545 | const queryResponse = await this.networkProvider.queryContract(query); 546 | const endpointDefinition = interaction.getEndpoint(); 547 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 548 | queryResponse, 549 | endpointDefinition 550 | ); 551 | if (returnCode.isSuccess()) { 552 | const returnValue = firstValue?.valueOf(); 553 | const requirements: NftMinterRequirements = { 554 | antiSpamTaxValue: returnValue.anti_spam_tax_value.toNumber(), 555 | contractPaused: returnValue.is_paused, 556 | maxRoyalties: returnValue.max_royalties.toNumber(), 557 | minRoyalties: returnValue.min_royalties.toNumber(), 558 | mintTimeLimit: returnValue.mint_time_limit.toNumber(), 559 | lastUserMintTime: returnValue.last_mint_time, 560 | userWhitelistedForMint: returnValue.is_whitelisted, 561 | contractWhitelistEnabled: returnValue.whitelist_enabled, 562 | numberOfMintsForUser: returnValue.minted_per_user.toNumber(), 563 | totalNumberOfMints: returnValue.total_minted.toNumber(), 564 | addressFrozen: returnValue.frozen, 565 | frozenNonces: returnValue.frozen_nonces.map((v: any) => v.toNumber()) 566 | }; 567 | return requirements; 568 | } else { 569 | throw new ErrContractQuery( 570 | 'viewMinterRequirements', 571 | returnCode.toString() 572 | ); 573 | } 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /src/sft-minter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | AddressValue, 4 | BigUIntValue, 5 | ContractCallPayloadBuilder, 6 | ContractFunction, 7 | IAddress, 8 | ResultsParser, 9 | StringValue, 10 | TokenIdentifierValue, 11 | Transaction, 12 | U64Value 13 | } from '@multiversx/sdk-core/out'; 14 | import dataNftMinterAbi from './abis/datanftmint.abi.json'; 15 | import { 16 | createFileFromUrl, 17 | dataNFTDataStreamAdvertise, 18 | storeToIpfs 19 | } from './common/mint-utils'; 20 | import { checkTraitsUrl, checkUrlIsUp } from './common/utils'; 21 | import { 22 | EnvironmentsEnum, 23 | itheumTokenIdentifier, 24 | minterContractAddress 25 | } from './config'; 26 | import { ErrArgumentNotSet, ErrContractQuery } from './errors'; 27 | import { SftMinterRequirements } from './interfaces'; 28 | import { Minter } from './minter'; 29 | import BigNumber from 'bignumber.js'; 30 | import { 31 | NumericValidator, 32 | StringValidator, 33 | validateResults 34 | } from './common/validator'; 35 | 36 | export class SftMinter extends Minter { 37 | /** 38 | * Creates a new instance of the `SftMinter` class, which can be used to interact with the Data NFT-FT minter smart contract 39 | * @param env 'devnet' | 'mainnet' | 'testnet' 40 | * @param timeout Timeout for the network provider (DEFAULT = 10000ms) 41 | */ 42 | constructor(env: string, timeout: number = 10000) { 43 | super( 44 | env, 45 | new Address(minterContractAddress[env as EnvironmentsEnum]), 46 | dataNftMinterAbi, 47 | timeout 48 | ); 49 | } 50 | 51 | /** 52 | * Retrieves the minter smart contract requirements for the given user 53 | * @param address the address of the user 54 | * @param taxToken the tax token to be used for the minting (default = `ITHEUM` token identifier based on the {@link EnvironmentsEnum}) 55 | */ 56 | async viewMinterRequirements( 57 | address: IAddress, 58 | taxToken = itheumTokenIdentifier[this.env as EnvironmentsEnum] 59 | ): Promise { 60 | const interaction = this.contract.methodsExplicit.getUserDataOut([ 61 | new AddressValue(address), 62 | new TokenIdentifierValue(taxToken) 63 | ]); 64 | const query = interaction.buildQuery(); 65 | const queryResponse = await this.networkProvider.queryContract(query); 66 | const endpointDefinition = interaction.getEndpoint(); 67 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 68 | queryResponse, 69 | endpointDefinition 70 | ); 71 | if (returnCode.isSuccess()) { 72 | const returnValue = firstValue?.valueOf(); 73 | const requirements: SftMinterRequirements = { 74 | antiSpamTaxValue: returnValue.anti_spam_tax_value.toNumber(), 75 | contractPaused: returnValue.is_paused, 76 | maxRoyalties: returnValue.max_royalties.toNumber(), 77 | minRoyalties: returnValue.min_royalties.toNumber(), 78 | maxSupply: returnValue.max_supply.toNumber(), 79 | mintTimeLimit: returnValue.mint_time_limit.toNumber(), 80 | lastUserMintTime: returnValue.last_mint_time, 81 | userWhitelistedForMint: returnValue.is_whitelisted, 82 | contractWhitelistEnabled: returnValue.whitelist_enabled, 83 | numberOfMintsForUser: returnValue.minted_per_user.toNumber(), 84 | totalNumberOfMints: returnValue.total_minted.toNumber(), 85 | addressFrozen: returnValue.frozen, 86 | frozenNonces: returnValue.frozen_nonces.map((v: any) => v.toNumber()), 87 | maxDonationPecentage: returnValue.max_donation_percentage.toNumber() 88 | }; 89 | return requirements; 90 | } else { 91 | throw new ErrContractQuery( 92 | 'viewMinterRequirements', 93 | returnCode.toString() 94 | ); 95 | } 96 | } 97 | 98 | /** 99 | * Retrieves a list of nonces that are frozen for address 100 | * @param address The address to check 101 | */ 102 | async viewAddressFrozenNonces(address: IAddress): Promise { 103 | const interaction = this.contract.methodsExplicit.getSftsFrozenForAddress([ 104 | new AddressValue(address) 105 | ]); 106 | const query = interaction.buildQuery(); 107 | const queryResponse = await this.networkProvider.queryContract(query); 108 | const endpointDefinition = interaction.getEndpoint(); 109 | const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( 110 | queryResponse, 111 | endpointDefinition 112 | ); 113 | if (returnCode.isSuccess()) { 114 | const returnValue = firstValue?.valueOf(); 115 | const frozenNonces: number[] = returnValue.map((nonce: any) => 116 | nonce.toNumber() 117 | ); 118 | return frozenNonces; 119 | } else { 120 | throw new ErrContractQuery( 121 | 'viewAddressFrozenNonces', 122 | returnCode.toString() 123 | ); 124 | } 125 | } 126 | 127 | /** 128 | * Creates an initialize contract transaction for the contract 129 | * @param senderAddress The address of the sender, must be the admin of the contract 130 | * @param collectionName The name of the NFT collection 131 | * @param tokenTicker The ticker of the NFT collection 132 | * @param antiSpamTaxTokenIdentifier The token identifier of the anti spam token 133 | * @param antiSpamTaxTokenAmount The amount of anti spam token to be used for minting as tax 134 | * @param mintLimit(seconds)- The mint limit between mints 135 | * @param treasury_address The address of the treasury to collect the anti spam tax 136 | */ 137 | initializeContract( 138 | senderAddress: IAddress, 139 | collectionName: string, 140 | tokenTicker: string, 141 | antiSpamTaxTokenIdentifier: string, 142 | antiSpamTaxTokenAmount: BigNumber.Value, 143 | mintLimit: number, 144 | treasury_address: IAddress 145 | ): Transaction { 146 | const initializeContractTx = new Transaction({ 147 | value: 0, 148 | data: new ContractCallPayloadBuilder() 149 | .setFunction(new ContractFunction('initializeContract')) 150 | .addArg(new StringValue(collectionName)) 151 | .addArg(new StringValue(tokenTicker)) 152 | .addArg(new TokenIdentifierValue(antiSpamTaxTokenIdentifier)) 153 | .addArg(new BigUIntValue(antiSpamTaxTokenAmount)) 154 | .addArg(new U64Value(mintLimit)) 155 | .addArg(new AddressValue(treasury_address)) 156 | .build(), 157 | receiver: this.contract.getAddress(), 158 | gasLimit: 10000000, 159 | sender: senderAddress, 160 | chainID: this.chainID 161 | }); 162 | return initializeContractTx; 163 | } 164 | 165 | /** 166 | * Creates a `setTreasuryAddress` transaction 167 | * @param senderAddress The address of the sender, must be the admin of the contract 168 | * @param treasuryAddress The address of the treasury to collect the anti spam tax 169 | */ 170 | setTreasuryAddress( 171 | senderAddress: IAddress, 172 | treasuryAddress: IAddress 173 | ): Transaction { 174 | const setTreasuryAddressTx = new Transaction({ 175 | value: 0, 176 | data: new ContractCallPayloadBuilder() 177 | .setFunction(new ContractFunction('setTreasuryAddress')) 178 | .addArg(new AddressValue(treasuryAddress)) 179 | .build(), 180 | receiver: this.contract.getAddress(), 181 | gasLimit: 10000000, 182 | sender: senderAddress, 183 | chainID: this.chainID 184 | }); 185 | return setTreasuryAddressTx; 186 | } 187 | 188 | /** 189 | * Creates a `setDonationTreasuryAddress` transaction 190 | * @param senderAddress The address of the sender, must be the admin of the contract 191 | * @param donationTreasuryAddress The address of the donation treasury to collect the donation tax 192 | */ 193 | setDonationTreasuryAddress( 194 | senderAddress: IAddress, 195 | donationTreasuryAddress: IAddress 196 | ): Transaction { 197 | const setDonationTreasuryAddressTx = new Transaction({ 198 | value: 0, 199 | data: new ContractCallPayloadBuilder() 200 | .setFunction(new ContractFunction('setDonationTreasuryAddress')) 201 | .addArg(new AddressValue(donationTreasuryAddress)) 202 | .build(), 203 | receiver: this.contract.getAddress(), 204 | gasLimit: 10000000, 205 | sender: senderAddress, 206 | chainID: this.chainID 207 | }); 208 | return setDonationTreasuryAddressTx; 209 | } 210 | 211 | /** 212 | * Creates a `setMaxDonationPercentage` transaction 213 | * @param senderAddress The address of the sender, must be the admin of the contract 214 | * @param maxDonationPercentage The maximum donation percentage that can be set 215 | */ 216 | setMaxDonationPercentage( 217 | senderAddress: IAddress, 218 | maxDonationPercentage: BigNumber.Value 219 | ): Transaction { 220 | const setMaxDonationPercentageTx = new Transaction({ 221 | value: 0, 222 | data: new ContractCallPayloadBuilder() 223 | .setFunction(new ContractFunction('setMaxDonationPercentage')) 224 | .addArg(new U64Value(maxDonationPercentage)) 225 | .build(), 226 | receiver: this.contract.getAddress(), 227 | gasLimit: 10000000, 228 | sender: senderAddress, 229 | chainID: this.chainID 230 | }); 231 | return setMaxDonationPercentageTx; 232 | } 233 | 234 | /** 235 | * Creates a `setAntiSpamTax` transaction 236 | * @param senderAddress The address of the sender, must be the admin of the contract 237 | * @param maxSupply The maximum supply that can be minted 238 | */ 239 | setMaxSupply( 240 | senderAddress: IAddress, 241 | maxSupply: BigNumber.Value 242 | ): Transaction { 243 | const setMaxSupplyTx = new Transaction({ 244 | value: 0, 245 | data: new ContractCallPayloadBuilder() 246 | .setFunction(new ContractFunction('setMaxSupply')) 247 | .addArg(new BigUIntValue(maxSupply)) 248 | .build(), 249 | receiver: this.contract.getAddress(), 250 | gasLimit: 10000000, 251 | sender: senderAddress, 252 | chainID: this.chainID 253 | }); 254 | return setMaxSupplyTx; 255 | } 256 | 257 | /** 258 | * Creates a `mint` transaction 259 | * 260 | * NOTE: The `dataStreamUrl` is being encrypted and the `media` and `metadata` urls are build and uploaded to IPFS 261 | * 262 | * NOTE: The `options.nftStorageToken` is required when not using custom image and traits, when using custom image and traits the traits should be compliant with the `traits` structure 263 | * 264 | * For more information, see the [README documentation](https://github.com/Itheum/sdk-mx-data-nft#create-a-mint-transaction). 265 | * 266 | * @param senderAddress the address of the user 267 | * @param tokenName the name of the DataNFT-FT. Between 3 and 20 alphanumeric characters, no spaces. 268 | * @param dataMarshalUrl the url of the data marshal. A live HTTPS URL that returns a 200 OK HTTP code. 269 | * @param dataStreamUrl the url of the data stream to be encrypted. A live HTTPS URL that returns a 200 OK HTTP code. 270 | * @param dataPreviewUrl the url of the data preview. A live HTTPS URL that returns a 200 OK HTTP code. 271 | * @param royalties the royalties to be set for the Data NFT-FT. A number between 0 and 50. This equates to a % value. e.g. 10% 272 | * @param supply the supply of the Data NFT-FT. A number between 1 and 1000. 273 | * @param datasetTitle the title of the dataset. Between 10 and 60 alphanumeric characters. 274 | * @param datasetDescription the description of the dataset. Between 10 and 400 alphanumeric characters. 275 | * @param lockPeriod the lock period for the bond in days 276 | * @param amountToSend the amount of the bond + anti spam tax (if anti spam tax > 0) to be sent 277 | * @param options [optional] below parameters are optional or required based on use case 278 | * - imageUrl: the URL of the image for the Data NFT 279 | * - traitsUrl: the URL of the traits for the Data NFT 280 | * - nftStorageToken: the nft storage token to be used to upload the image and metadata to IPFS 281 | * - extraAssets: [optional] extra URIs to attached to the NFT. Can be media files, documents, etc. These URIs are public 282 | * - donationPercentage: [optional] the donation percentage to be set for the Data NFT-FT supply to be sent to the donation 283 | * - imgGenBg: [optional] the custom series bg to influence the image generation service 284 | * - imgGenSet: [optional] the custom series layer set to influence the image generation service 285 | * 286 | */ 287 | async mint( 288 | senderAddress: IAddress, 289 | tokenName: string, 290 | dataMarshalUrl: string, 291 | dataStreamUrl: string, 292 | dataPreviewUrl: string, 293 | royalties: number, 294 | supply: number, 295 | datasetTitle: string, 296 | datasetDescription: string, 297 | amountToSend: number, 298 | lockPeriod?: number, 299 | donationPercentage = 0, 300 | options?: { 301 | imageUrl?: string; 302 | traitsUrl?: string; 303 | nftStorageToken?: string; 304 | extraAssets?: string[]; 305 | imgGenBg?: string; 306 | imgGenSet?: string; 307 | } 308 | ): Promise<{ imageUrl: string; metadataUrl: string; tx: Transaction }> { 309 | const { 310 | imageUrl, 311 | traitsUrl, 312 | nftStorageToken, 313 | extraAssets, 314 | imgGenBg, 315 | imgGenSet 316 | } = options ?? {}; 317 | 318 | const tokenNameValidator = new StringValidator() 319 | .notEmpty() 320 | .alphanumeric() 321 | .minLength(3) 322 | .maxLength(20) 323 | .validate(tokenName); 324 | 325 | const datasetTitleValidator = new StringValidator() 326 | .notEmpty() 327 | .minLength(10) 328 | .maxLength(60) 329 | .validate(datasetTitle.trim()); 330 | 331 | const datasetDescriptionValidator = new StringValidator() 332 | .notEmpty() 333 | .minLength(10) 334 | .maxLength(400) 335 | .validate(datasetDescription); 336 | 337 | const royaltiesValidator = new NumericValidator() 338 | .integer() 339 | .minValue(0) 340 | .validate(royalties); 341 | 342 | const supplyValidator = new NumericValidator() 343 | .integer() 344 | .minValue(1) 345 | .validate(supply); 346 | 347 | validateResults([ 348 | tokenNameValidator, 349 | datasetTitleValidator, 350 | datasetDescriptionValidator, 351 | royaltiesValidator, 352 | supplyValidator 353 | ]); 354 | 355 | // deep validate all mandatory URLs 356 | try { 357 | await checkUrlIsUp(dataPreviewUrl, [200]); 358 | await checkUrlIsUp(dataMarshalUrl + '/health-check', [200]); 359 | } catch (error) { 360 | throw error; 361 | } 362 | 363 | let imageOnIpfsUrl: string; 364 | let metadataOnIpfsUrl: string; 365 | 366 | const { dataNftHash, dataNftStreamUrlEncrypted } = 367 | await dataNFTDataStreamAdvertise( 368 | dataStreamUrl, 369 | dataMarshalUrl, 370 | senderAddress.bech32() // the caller is the Creator 371 | ); 372 | 373 | if (!imageUrl) { 374 | if (!nftStorageToken) { 375 | throw new ErrArgumentNotSet( 376 | 'nftStorageToken', 377 | 'NFT Storage token is required when not using custom image and traits' 378 | ); 379 | } 380 | 381 | // create the img generative service API based on user options 382 | let imgGenServiceApi = `${this.imageServiceUrl}/v1/generateNFTArt?hash=${dataNftHash}`; 383 | 384 | if (imgGenBg && imgGenBg.trim() !== '') { 385 | imgGenServiceApi += `&bg=${imgGenBg.trim()}`; 386 | } 387 | 388 | if (imgGenSet && imgGenSet.trim() !== '') { 389 | imgGenServiceApi += `&set=${imgGenSet.trim()}`; 390 | } 391 | 392 | const { image, traits } = await createFileFromUrl( 393 | imgGenServiceApi, 394 | datasetTitle, 395 | datasetDescription, 396 | dataPreviewUrl, 397 | senderAddress.bech32(), 398 | extraAssets ?? [] 399 | ); 400 | 401 | const { 402 | imageOnIpfsUrl: imageIpfsUrl, 403 | metadataOnIpfsUrl: metadataIpfsUrl 404 | } = await storeToIpfs(nftStorageToken, traits, image); 405 | 406 | imageOnIpfsUrl = imageIpfsUrl; 407 | metadataOnIpfsUrl = metadataIpfsUrl; 408 | } else { 409 | if (!traitsUrl) { 410 | throw new ErrArgumentNotSet( 411 | 'traitsUrl', 412 | 'Traits URL is required when using custom image' 413 | ); 414 | } 415 | 416 | await checkTraitsUrl(traitsUrl); 417 | 418 | imageOnIpfsUrl = imageUrl; 419 | metadataOnIpfsUrl = traitsUrl; 420 | } 421 | 422 | const data = new ContractCallPayloadBuilder() 423 | .setFunction(new ContractFunction('ESDTTransfer')) 424 | .addArg( 425 | new TokenIdentifierValue( 426 | itheumTokenIdentifier[this.env as EnvironmentsEnum] 427 | ) 428 | ) 429 | .addArg(new BigUIntValue(amountToSend)) 430 | .addArg(new StringValue('mint')) 431 | .addArg(new StringValue(tokenName)) 432 | .addArg(new StringValue(imageOnIpfsUrl)) 433 | .addArg(new StringValue(metadataOnIpfsUrl)) 434 | .addArg(new StringValue(dataMarshalUrl)) 435 | .addArg(new StringValue(dataNftStreamUrlEncrypted)) 436 | .addArg(new StringValue(dataPreviewUrl)) 437 | .addArg(new U64Value(royalties)) 438 | .addArg(new U64Value(supply)) 439 | .addArg(new StringValue(datasetTitle)) 440 | .addArg(new StringValue(datasetDescription)); 441 | 442 | if (lockPeriod) { 443 | data.addArg(new U64Value(lockPeriod)); 444 | } 445 | 446 | data.addArg(new U64Value(donationPercentage)); 447 | 448 | for (const extraAsset of extraAssets ?? []) { 449 | data.addArg(new StringValue(extraAsset)); 450 | } 451 | 452 | const mintTx = new Transaction({ 453 | data: data.build(), 454 | sender: senderAddress, 455 | receiver: this.contract.getAddress(), 456 | gasLimit: 130_000_000, 457 | chainID: this.chainID 458 | }); 459 | 460 | return { 461 | imageUrl: imageOnIpfsUrl, 462 | metadataUrl: metadataOnIpfsUrl, 463 | tx: mintTx 464 | }; 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /tests/bond.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BondConfiguration, 3 | BondContract, 4 | Compensation, 5 | State, 6 | createTokenIdentifier, 7 | dataNftTokenIdentifier 8 | } from '../src'; 9 | import { Bond } from '../src'; 10 | 11 | describe('Bond test', () => { 12 | const tokenIdentifier = dataNftTokenIdentifier.devnet; 13 | test('#test no deploy', () => { 14 | try { 15 | const bondContract = new BondContract('testnet'); 16 | } catch (e: any) { 17 | expect(e.message).toBe('Contract address is not deployed on testnet'); 18 | } 19 | }); 20 | 21 | // test('#view bond configuration', async () => { 22 | // const bondContract = new BondContract('devnet'); 23 | // const bondConfiguration = await bondContract.viewContractConfiguration(); 24 | 25 | // expect(bondConfiguration).toMatchObject; 26 | // }); 27 | // test('#test view methods', async () => { 28 | // const bondContract = new BondContract('devnet'); 29 | 30 | // const bondPaymentToken = await bondContract.viewBondPaymentToken(); 31 | // expect(typeof bondPaymentToken).toBe('string'); 32 | 33 | // const lockPeriodsWithBonds = await bondContract.viewLockPeriodsWithBonds(); 34 | // expect(typeof lockPeriodsWithBonds).toBe('object'); 35 | 36 | // const contractState = await bondContract.viewContractState(); 37 | // expect(Object.values(State).includes(contractState)).toBe(true); 38 | 39 | // const acceptedCallers = await bondContract.viewAcceptedCallers(); 40 | // expect(typeof acceptedCallers).toBe('object'); 41 | 42 | // const bond: Bond[] = await bondContract.viewBonds([1]); 43 | // expect(bond).toMatchObject; 44 | // const sameBond: Bond[] = await bondContract.viewBonds( 45 | // [tokenIdentifier], 46 | // [172] 47 | // ); 48 | // expect(sameBond).toMatchObject; 49 | // const sameBond2: Bond[] = await bondContract.viewBonds([ 50 | // createTokenIdentifier(tokenIdentifier, 172) 51 | // ]); 52 | // expect(sameBond2).toMatchObject; 53 | // expect(sameBond).toStrictEqual(sameBond2); 54 | 55 | // const singleBond: Bond = await bondContract.viewBond(1); 56 | // expect(singleBond).toMatchObject; 57 | // expect(singleBond).toStrictEqual(sameBond2[0]); 58 | 59 | // const pagedBonds: Bond[] = await bondContract.viewPagedBonds(0, 2); 60 | // expect(pagedBonds).toMatchObject; 61 | // expect(pagedBonds.length).toBe(3); 62 | // expect(pagedBonds[0]).toStrictEqual(singleBond); 63 | 64 | // const compensation: Compensation = await bondContract.viewCompensation(1); 65 | // expect(compensation).toMatchObject; 66 | 67 | // const compensations: Compensation[] = await bondContract.viewCompensations([ 68 | // { tokenIdentifier: tokenIdentifier, nonce: 172 } 69 | // ]); 70 | // expect(compensations).toMatchObject; 71 | // expect(compensations[0]).toStrictEqual(compensation); 72 | 73 | // const pagedCompensations: Compensation[] = 74 | // await bondContract.viewPagedCompensations(0, 2); 75 | // expect(pagedCompensations).toMatchObject; 76 | // expect(pagedCompensations.length).toBe(3); 77 | // expect(pagedCompensations[0]).toStrictEqual(compensation); 78 | // }, 20000); 79 | }); 80 | -------------------------------------------------------------------------------- /tests/datanft.test.ts: -------------------------------------------------------------------------------- 1 | import { SignableMessage } from '@multiversx/sdk-core/out'; 2 | import { 3 | DataNft, 4 | EnvironmentsEnum, 5 | marshalUrls, 6 | parseTokenIdentifier 7 | } from '../src'; 8 | import { ErrInvalidTokenIdentifier } from '../src/errors'; 9 | 10 | describe('Data NFT test', () => { 11 | test('#test not setting network config', async () => { 12 | try { 13 | await DataNft.createFromApi({ nonce: 62 }); 14 | } catch (error: any) { 15 | expect(error.message).toBe( 16 | 'Network configuration is not set. Call setNetworkConfig static method before calling any method that requires network configuration.' 17 | ); 18 | } 19 | }); 20 | 21 | test('#test not setting network config', async () => { 22 | try { 23 | const dataNft = new DataNft({ 24 | dataMarshal: 'https://api.itheumcloud-stg.com/datamarshalapi/router/v1' 25 | }); 26 | 27 | await dataNft.viewData({ 28 | signedMessage: 'x', 29 | signableMessage: new SignableMessage({ message: Buffer.from('test') }), 30 | stream: true 31 | }); 32 | } catch (error: any) { 33 | expect(error.message).toBe( 34 | 'Network configuration is not set. Call setNetworkConfig static method before calling any method that requires network configuration.' 35 | ); 36 | } 37 | }); 38 | 39 | test('#test bad input on createFromApi', async () => { 40 | try { 41 | DataNft.setNetworkConfig('devnet'); 42 | await DataNft.createFromApi({ 43 | nonce: 62, 44 | tokenIdentifier: 'DATANFTFT3-d0978a' 45 | }); 46 | } catch (error: any) { 47 | expect(error.message).toBe( 48 | 'Fetch error with status code: 404 and message: Not Found' 49 | ); 50 | } 51 | }); 52 | 53 | test('#getMessageToSign', async () => { 54 | DataNft.setNetworkConfig('devnet'); 55 | const dataNft = new DataNft({ 56 | dataMarshal: 'https://api.itheumcloud-stg.com/datamarshalapi/router/v1' 57 | }); 58 | 59 | const nonceToSign = await dataNft.getMessageToSign(); 60 | 61 | expect(typeof nonceToSign).toBe('string'); 62 | }, 10000); 63 | 64 | test('#getOwnedByAddress', async () => { 65 | DataNft.setNetworkConfig('devnet'); 66 | const dataNfts = await DataNft.ownedByAddress( 67 | 'erd1w6ffeexmumd5qzme78grrvp33qngcgqk2prjyuuyawpc955gvcxqqrsrtw' 68 | ); 69 | 70 | expect(dataNfts).toBeInstanceOf(Array); 71 | for (const item of dataNfts) { 72 | expect(item).toBeInstanceOf(Object as unknown as DataNft); 73 | } 74 | }, 10000); 75 | 76 | test('#Create nft from payload', async () => { 77 | DataNft.setNetworkConfig('mainnet'); 78 | const query = 79 | 'https://api.multiversx.com/nfts?identifiers=DATANFTFT-e936d4-02&withSupply=true'; 80 | 81 | const response = await fetch(query); 82 | const data = await response.json(); 83 | 84 | const dataNfts: DataNft[] = DataNft.createFromApiResponseOrBulk(data); 85 | 86 | for (const item of dataNfts) { 87 | expect(item).toBeInstanceOf(Object as unknown as DataNft); 88 | } 89 | }); 90 | 91 | test('#create many data NFTs different token identifiers', async () => { 92 | DataNft.setNetworkConfig('devnet'); 93 | 94 | const dataNfts = await DataNft.createManyFromApi([ 95 | { nonce: 1, tokenIdentifier: 'DATANFTFT-e0b917' }, 96 | { nonce: 2, tokenIdentifier: 'DATANFTFT-e0b917' }, 97 | { nonce: 3 } 98 | ]); 99 | 100 | for (const item of dataNfts) { 101 | expect(item).toBeInstanceOf(Object as unknown as DataNft); 102 | } 103 | }, 12000); 104 | 105 | test('#get owners of data Nft', async () => { 106 | DataNft.setNetworkConfig('devnet'); 107 | 108 | let dataNft = new DataNft({ 109 | nonce: 2, 110 | tokenIdentifier: 'DATANFTFT-e0b917' 111 | }); 112 | 113 | const owners = await dataNft.getOwners(); 114 | }, 200000); 115 | 116 | test('#parse token identifier', () => { 117 | const tokenIdentifier = 'DATANFTFT3-d0978a'; 118 | 119 | expect(() => parseTokenIdentifier(tokenIdentifier)).toThrow( 120 | ErrInvalidTokenIdentifier 121 | ); 122 | 123 | const tokenIdentifier2 = 'DATANFTFT-e0b917-02'; 124 | 125 | const parsed = parseTokenIdentifier(tokenIdentifier2); 126 | 127 | expect(parsed).toBeInstanceOf( 128 | Object as unknown as { collection: string; nonce: String } 129 | ); 130 | }, 20000); 131 | 132 | test('#override marhsal url', async () => { 133 | DataNft.setNetworkConfig('mainnet'); 134 | 135 | const dataNft = await DataNft.createFromApi({ nonce: 5 }); 136 | 137 | expect(dataNft.overrideDataMarshal).toBe( 138 | marshalUrls[EnvironmentsEnum.mainnet] 139 | ); 140 | expect(dataNft.dataMarshal).toBe(marshalUrls[EnvironmentsEnum.devnet]); 141 | expect(dataNft.overrideDataMarshalChainId).toBe('1'); 142 | }, 20000); 143 | 144 | test('#override marshal url should be empty', async () => { 145 | DataNft.setNetworkConfig('mainnet'); 146 | 147 | const dataNft = await DataNft.createFromApi({ nonce: 1 }); 148 | 149 | expect(dataNft.overrideDataMarshal).toBe(''); 150 | expect(dataNft.overrideDataMarshalChainId).toBe(''); 151 | 152 | dataNft.updateDataNft({ 153 | overrideDataMarshal: 'overrideUrl', 154 | overrideDataMarshalChainId: 'D' 155 | }); 156 | 157 | expect(dataNft.overrideDataMarshal).toBe('overrideUrl'); 158 | expect(dataNft.overrideDataMarshalChainId).toBe('D'); 159 | }, 20000); 160 | }); 161 | -------------------------------------------------------------------------------- /tests/environment.test.ts: -------------------------------------------------------------------------------- 1 | import { ApiNetworkProvider } from '@multiversx/sdk-network-providers/out'; 2 | import { DataNftMarket, SftMinter } from '../src/index'; 3 | 4 | describe('testing environment market', () => { 5 | test('#devnet-default', async () => { 6 | const datanft = new DataNftMarket('devnet'); 7 | 8 | expect(datanft.chainID).toStrictEqual('D'); 9 | expect(datanft.networkProvider).toStrictEqual( 10 | new ApiNetworkProvider('https://devnet-api.multiversx.com', { 11 | timeout: 10000 12 | }) 13 | ); 14 | }); 15 | 16 | test('#mainnet-default', async () => { 17 | const datanft = new DataNftMarket('mainnet'); 18 | 19 | expect(datanft.chainID).toStrictEqual('1'); 20 | expect(datanft.networkProvider).toStrictEqual( 21 | new ApiNetworkProvider('https://api.multiversx.com', { 22 | timeout: 10000 23 | }) 24 | ); 25 | }); 26 | 27 | test('#devnet-custom-timeout', async () => { 28 | const datanft = new DataNftMarket('devnet', 5000); 29 | 30 | expect(datanft.chainID).toStrictEqual('D'); 31 | expect(datanft.networkProvider).toStrictEqual( 32 | new ApiNetworkProvider('https://devnet-api.multiversx.com', { 33 | timeout: 5000 34 | }) 35 | ); 36 | }); 37 | 38 | test('#mainnet-custom-timeout', async () => { 39 | const datanft = new DataNftMarket('mainnet', 5000); 40 | 41 | expect(datanft.chainID).toStrictEqual('1'); 42 | expect(datanft.networkProvider).toStrictEqual( 43 | new ApiNetworkProvider('https://api.multiversx.com', { 44 | timeout: 5000 45 | }) 46 | ); 47 | }); 48 | }); 49 | 50 | describe('testing environment minter', () => { 51 | test('#devnet-default', async () => { 52 | const datanft = new SftMinter('devnet'); 53 | 54 | expect(datanft.chainID).toStrictEqual('D'); 55 | expect(datanft.networkProvider).toStrictEqual( 56 | new ApiNetworkProvider('https://devnet-api.multiversx.com', { 57 | timeout: 10000 58 | }) 59 | ); 60 | }); 61 | 62 | test('#mainnet-default', async () => { 63 | const datanft = new DataNftMarket('mainnet'); 64 | 65 | expect(datanft.chainID).toStrictEqual('1'); 66 | expect(datanft.networkProvider).toStrictEqual( 67 | new ApiNetworkProvider('https://api.multiversx.com', { 68 | timeout: 10000 69 | }) 70 | ); 71 | }); 72 | 73 | test('#devnet-custom-timeout', async () => { 74 | const datanft = new SftMinter('devnet', 5000); 75 | 76 | expect(datanft.chainID).toStrictEqual('D'); 77 | expect(datanft.networkProvider).toStrictEqual( 78 | new ApiNetworkProvider('https://devnet-api.multiversx.com', { 79 | timeout: 5000 80 | }) 81 | ); 82 | }); 83 | 84 | test('#mainnet-custom-timeout', async () => { 85 | const datanft = new SftMinter('mainnet', 5000); 86 | 87 | expect(datanft.chainID).toStrictEqual('1'); 88 | expect(datanft.networkProvider).toStrictEqual( 89 | new ApiNetworkProvider('https://api.multiversx.com', { 90 | timeout: 5000 91 | }) 92 | ); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /tests/marketplace.test.ts: -------------------------------------------------------------------------------- 1 | import { Address, Transaction } from '@multiversx/sdk-core/out'; 2 | import { DataNftMarket, MarketplaceRequirements, Offer } from '../src'; 3 | 4 | describe('Marketplace Sdk test', () => { 5 | test('#getAddress', async () => { 6 | const dataNftMarket = new DataNftMarket('devnet'); 7 | expect(dataNftMarket.getContractAddress()).toBeInstanceOf(Address); 8 | }); 9 | 10 | test('#viewAddressListedOffers', async () => { 11 | const dataNftMarket = new DataNftMarket('devnet'); 12 | 13 | const result = await dataNftMarket.viewAddressListedOffers( 14 | new Address( 15 | 'erd1qqqqqqqqqqqqqpgq7ykazrzd905zvnlr88dpfw06677lxe9w0n4suz00uh' 16 | ) 17 | ); 18 | 19 | expect(result).toBeInstanceOf(Array); 20 | for (const item of result) { 21 | expect(item).toBeInstanceOf(Object as unknown as Offer); 22 | } 23 | }, 10000); 24 | 25 | test('#viewAddressTotalOffers', async () => { 26 | const dataNftMarket = new DataNftMarket('devnet'); 27 | 28 | const result = await dataNftMarket.viewAddressTotalOffers( 29 | new Address( 30 | 'erd1qqqqqqqqqqqqqpgq7ykazrzd905zvnlr88dpfw06677lxe9w0n4suz00uh' 31 | ) 32 | ); 33 | 34 | expect(typeof result).toBe('number'); 35 | }); 36 | 37 | // test('#viewRequirements', async () => { 38 | // const dataNftMarket = new DataNftMarket('devnet'); 39 | 40 | // const result = await dataNftMarket.viewRequirements(); 41 | 42 | // expect(result).toBeInstanceOf(Object as unknown as MarketplaceRequirements); 43 | // }); 44 | 45 | test('#viewLastValidOfferId', async () => { 46 | const dataNftMarket = new DataNftMarket('devnet'); 47 | 48 | const result = await dataNftMarket.viewLastValidOfferId(); 49 | 50 | expect(typeof result).toBe('number'); 51 | }); 52 | 53 | test('#getPauseState', async () => { 54 | const dataNftMarket = new DataNftMarket('devnet'); 55 | 56 | const result = await dataNftMarket.viewContractPauseState(); 57 | 58 | expect(typeof result).toBe('boolean'); 59 | }); 60 | 61 | test('#addOffer', () => { 62 | const dataNftMarket = new DataNftMarket('devnet'); 63 | 64 | const result = dataNftMarket.addOffer( 65 | new Address( 66 | 'erd1qqqqqqqqqqqqqpgq7ykazrzd905zvnlr88dpfw06677lxe9w0n4suz00uh' 67 | ), 68 | 'Foo', 69 | 0, 70 | 0, 71 | 'Bar', 72 | 0, 73 | 0, 74 | 0, 75 | 0 76 | ); 77 | 78 | expect(result).toBeInstanceOf(Transaction); 79 | }); 80 | 81 | test('#acceptOffer', () => { 82 | const dataNftMarket = new DataNftMarket('devnet'); 83 | 84 | const result = dataNftMarket.acceptOfferWithEGLD( 85 | new Address( 86 | 'erd1qqqqqqqqqqqqqpgq7ykazrzd905zvnlr88dpfw06677lxe9w0n4suz00uh' 87 | ), 88 | 0, 89 | 0, 90 | '100' 91 | ); 92 | const result2 = dataNftMarket.acceptOfferWithESDT( 93 | new Address( 94 | 'erd1qqqqqqqqqqqqqpgq7ykazrzd905zvnlr88dpfw06677lxe9w0n4suz00uh' 95 | ), 96 | 0, 97 | 0, 98 | '1000' 99 | ); 100 | 101 | const result3 = dataNftMarket.acceptOfferWithNoPayment( 102 | new Address( 103 | 'erd1qqqqqqqqqqqqqpgq7ykazrzd905zvnlr88dpfw06677lxe9w0n4suz00uh' 104 | ), 105 | 0, 106 | 0 107 | ); 108 | 109 | expect(result).toBeInstanceOf(Transaction); 110 | expect(result2).toBeInstanceOf(Transaction); 111 | expect(result3).toBeInstanceOf(Transaction); 112 | }); 113 | 114 | test('#cancelOffer', () => { 115 | const dataNftMarket = new DataNftMarket('devnet'); 116 | 117 | const result = dataNftMarket.cancelOffer( 118 | new Address( 119 | 'erd1qqqqqqqqqqqqqpgq7ykazrzd905zvnlr88dpfw06677lxe9w0n4suz00uh' 120 | ), 121 | 0, 122 | 0 123 | ); 124 | 125 | const result2 = dataNftMarket.cancelOffer( 126 | new Address( 127 | 'erd1qqqqqqqqqqqqqpgq7ykazrzd905zvnlr88dpfw06677lxe9w0n4suz00uh' 128 | ), 129 | 0, 130 | 0, 131 | false 132 | ); 133 | 134 | expect(result).toBeInstanceOf(Transaction); 135 | expect(result2).toBeInstanceOf(Transaction); 136 | }); 137 | 138 | test('#changeOfferPrice', () => { 139 | const dataNftMarket = new DataNftMarket('devnet'); 140 | 141 | const result = dataNftMarket.changeOfferPrice( 142 | new Address( 143 | 'erd1qqqqqqqqqqqqqpgq7ykazrzd905zvnlr88dpfw06677lxe9w0n4suz00uh' 144 | ), 145 | 0, 146 | 0 147 | ); 148 | 149 | expect(result).toBeInstanceOf(Transaction); 150 | }); 151 | 152 | test('#withdrawCancelledOffer', () => { 153 | const dataNftMarket = new DataNftMarket('devnet'); 154 | 155 | const result = dataNftMarket.withdrawCancelledOffer( 156 | new Address( 157 | 'erd1qqqqqqqqqqqqqpgq7ykazrzd905zvnlr88dpfw06677lxe9w0n4suz00uh' 158 | ), 159 | 0 160 | ); 161 | 162 | expect(result).toBeInstanceOf(Transaction); 163 | }); 164 | 165 | test('#viewOffer', async () => { 166 | const dataNftMarket = new DataNftMarket('devnet'); 167 | 168 | const result = await dataNftMarket.viewNumberOfOffers(); 169 | 170 | expect(typeof result).toBe('number'); 171 | }); 172 | 173 | // test('#viewOffers', async () => { 174 | // const dataNftMarket = new DataNftMarket('devnet'); 175 | 176 | // const result = await dataNftMarket.viewOffers(); 177 | 178 | // expect(result).toBeInstanceOf(Array); 179 | // }, 10000); 180 | }); 181 | -------------------------------------------------------------------------------- /tests/nftminter.test.ts: -------------------------------------------------------------------------------- 1 | import { Address, Transaction } from '@multiversx/sdk-core/out'; 2 | import { NftMinter } from '../src'; 3 | 4 | describe('Nft minter test', () => { 5 | test('#initialize minter', async () => { 6 | const factoryGeneratedContract = new Address( 7 | 'erd1qqqqqqqqqqqqqpgqpd9qxrq5a03jrneafmlmckmlj5zgdj55fsxsqa7jsm' 8 | ); 9 | const nftMinter = new NftMinter('devnet', factoryGeneratedContract); 10 | 11 | expect(nftMinter).toBeInstanceOf(NftMinter); 12 | }); 13 | 14 | test('#initialize minter contract transaction', async () => { 15 | const factoryGeneratedContract = new Address( 16 | 'erd1qqqqqqqqqqqqqpgqpd9qxrq5a03jrneafmlmckmlj5zgdj55fsxsqa7jsm' 17 | ); 18 | const nftMinter = new NftMinter('devnet', factoryGeneratedContract); 19 | 20 | const adminOfContract = new Address( 21 | 'erd1qqqqqqqqqqqqqpgqpd9qxrq5a03jrneafmlmckmlj5zgdj55fsxsqa7jsm' 22 | ); 23 | 24 | const initTx = nftMinter.initializeContract( 25 | adminOfContract, 26 | 'Collection-Name-To-Mint', 27 | 'CollectionTicker', 28 | 0, 29 | false, 30 | new Address( 31 | 'erd1qqqqqqqqqqqqqpgqpd9qxrq5a03jrneafmlmckmlj5zgdj55fsxsqa7jsm' 32 | ) 33 | ); 34 | expect(initTx).toBeInstanceOf(Transaction); 35 | }); 36 | 37 | test('#mint nft using itheum generated image', async () => { 38 | const factoryGeneratedContract = new Address( 39 | 'erd1qqqqqqqqqqqqqpgqpd9qxrq5a03jrneafmlmckmlj5zgdj55fsxsqa7jsm' 40 | ); 41 | const nftMinter = new NftMinter('devnet', factoryGeneratedContract); 42 | 43 | const senderAddress = new Address( 44 | 'erd1qqqqqqqqqqqqqpgqpd9qxrq5a03jrneafmlmckmlj5zgdj55fsxsqa7jsm' 45 | ); 46 | 47 | const mintTx = await nftMinter.mint( 48 | senderAddress, 49 | 'TokenName', 50 | 'https://d37x5igq4vw5mq.cloudfront.net/datamarshalapi/router/v1', 51 | 'https://raw.githubusercontent.com/Itheum/data-assets/main/Health/H1__Signs_of_Anxiety_in_American_Households_due_to_Covid19/dataset.json', 52 | 'https://itheumapi.com/programReadingPreview/70dc6bd0-59b0-11e8-8d54-2d562f6cba54', 53 | 1000, 54 | 'Title for token', 55 | 'Description for token', 56 | { 57 | nftStorageToken: 58 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweDMzQjZGNzhmOTZmZjVmMGIwMUJFNzJmZTQ0NDRmMjBCYzhkOEQ0REUiLCJpc3MiOiJuZnQtc3RvcmFnZSIsImlhdCI6MTY5MTc0NDc5NDY5MCwibmFtZSI6InRlc3QifQ.lTjq16CgrDipiVClOrbWNt0A0zYkJ9YGVeDz1TlGqQ0' 59 | } 60 | ); 61 | expect(mintTx).toBeInstanceOf(Transaction); 62 | }, 40000); 63 | 64 | test('#mint nft using your image and metadata', async () => { 65 | const factoryGeneratedContract = new Address( 66 | 'erd1qqqqqqqqqqqqqpgqpd9qxrq5a03jrneafmlmckmlj5zgdj55fsxsqa7jsm' 67 | ); 68 | const nftMinter = new NftMinter('devnet', factoryGeneratedContract); 69 | 70 | const senderAddress = new Address( 71 | 'erd1qqqqqqqqqqqqqpgqpd9qxrq5a03jrneafmlmckmlj5zgdj55fsxsqa7jsm' 72 | ); 73 | 74 | const mintTx = await nftMinter.mint( 75 | senderAddress, 76 | 'TokenName', 77 | 'https://d37x5igq4vw5mq.cloudfront.net/datamarshalapi/router/v1', 78 | 'https://raw.githubusercontent.com/Itheum/data-assets/main/Health/H1__Signs_of_Anxiety_in_American_Households_due_to_Covid19/dataset.json', 79 | 'https://itheumapi.com/programReadingPreview/70dc6bd0-59b0-11e8-8d54-2d562f6cba54', 80 | 1000, 81 | 'Title for token', 82 | 'Description for token', 83 | { 84 | imageUrl: 85 | 'https://ipfs.io/ipfs/bafybeih7bvpcfj42nawm7g4bkbu25cqxbhlzth5sxm6qjwis3tke23p7ty/image.png', 86 | traitsUrl: 87 | 'https://ipfs.io/ipfs/bafybeih7bvpcfj42nawm7g4bkbu25cqxbhlzth5sxm6qjwis3tke23p7ty/metadata.json' 88 | } 89 | ); 90 | expect(mintTx).toBeInstanceOf(Transaction); 91 | }, 200000); 92 | 93 | test('#mint nft using tax for minting', async () => { 94 | const factoryGeneratedContract = new Address( 95 | 'erd1qqqqqqqqqqqqqpgqpd9qxrq5a03jrneafmlmckmlj5zgdj55fsxsqa7jsm' 96 | ); 97 | const nftMinter = new NftMinter('devnet', factoryGeneratedContract); 98 | 99 | const senderAddress = new Address( 100 | 'erd1qqqqqqqqqqqqqpgqpd9qxrq5a03jrneafmlmckmlj5zgdj55fsxsqa7jsm' 101 | ); 102 | const mintTx = await nftMinter.mint( 103 | senderAddress, 104 | 'TokenName', 105 | 'https://d37x5igq4vw5mq.cloudfront.net/datamarshalapi/router/v1', 106 | 'https://raw.githubusercontent.com/Itheum/data-assets/main/Health/H1__Signs_of_Anxiety_in_American_Households_due_to_Covid19/dataset.json', 107 | 'https://itheumapi.com/programReadingPreview/70dc6bd0-59b0-11e8-8d54-2d562f6cba54', 108 | 1000, 109 | 'Title for token', 110 | 'Description for token', 111 | { 112 | nftStorageToken: 113 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweDMzQjZGNzhmOTZmZjVmMGIwMUJFNzJmZTQ0NDRmMjBCYzhkOEQ0REUiLCJpc3MiOiJuZnQtc3RvcmFnZSIsImlhdCI6MTY5MTc0NDc5NDY5MCwibmFtZSI6InRlc3QifQ.lTjq16CgrDipiVClOrbWNt0A0zYkJ9YGVeDz1TlGqQ0', 114 | antiSpamTax: 1000000000000000000, 115 | antiSpamTokenIdentifier: 'EGLD' 116 | } 117 | ); 118 | 119 | expect(mintTx).toBeInstanceOf(Transaction); 120 | }, 40000); 121 | }); 122 | -------------------------------------------------------------------------------- /tests/sftminter.test.ts: -------------------------------------------------------------------------------- 1 | import { Address, Transaction } from '@multiversx/sdk-core/out'; 2 | import { SftMinter, Minter, SftMinterRequirements } from '../src'; 3 | 4 | describe('Data Nft Minter Test', () => { 5 | test('#viewMinterRequirements', async () => { 6 | const dataNftMarket = new SftMinter('devnet'); 7 | 8 | const result = await dataNftMarket.viewMinterRequirements( 9 | new Address( 10 | 'erd10uavg8hd92620mfll2lt4jdmrg6xlf60awjp9ze5gthqjjhactvswfwuv8' 11 | ) 12 | ); 13 | expect(result).toBeInstanceOf(Object as unknown as SftMinterRequirements); 14 | }); 15 | 16 | test('#burn', async () => { 17 | const dataNftMarket = new SftMinter('devnet'); 18 | 19 | const result = await dataNftMarket.burn( 20 | new Address( 21 | 'erd10uavg8hd92620mfll2lt4jdmrg6xlf60awjp9ze5gthqjjhactvswfwuv8' 22 | ), 23 | 1, 24 | 1 25 | ); 26 | expect(result).toBeInstanceOf(Transaction); 27 | }); 28 | 29 | test('#viewContractpauseState', async () => { 30 | const dataNftMarket = new SftMinter('devnet'); 31 | 32 | const result = await dataNftMarket.viewContractPauseState(); 33 | expect(typeof result).toBe('boolean'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/stake.test.ts: -------------------------------------------------------------------------------- 1 | import { ContractConfiguration, LivelinessStake, State } from '../src'; 2 | 3 | describe('Bond test', () => { 4 | test('#test liveliness', async () => { 5 | const livelinessStake = new LivelinessStake('devnet'); 6 | 7 | const response = await livelinessStake.viewContractConfiguration(); 8 | 9 | expect(response).toBeInstanceOf(Object as unknown as ContractConfiguration); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/traits-check.test.ts: -------------------------------------------------------------------------------- 1 | import { checkTraitsUrl } from '../src/common/utils'; 2 | 3 | describe('Traits strucutre test', () => { 4 | // test('#json traits strucutre check', async () => { 5 | // try { 6 | // await checkTraitsUrl( 7 | // 'https://ipfs.io/ipfs/bafybeih7bvpcfj42nawm7g4bkbu25cqxbhlzth5sxm6qjwis3tke23p7ty/metadata.json' 8 | // ); 9 | // expect(true).toBe(true); 10 | // } catch (error) {} 11 | // }, 100000); 12 | test('#json traits strucutre check', async () => { 13 | // try { 14 | // // await checkTraitsUrl( 15 | // // 'https://ipfs.io/ipfs/bafybeicbmpiehja5rjk425ol4rmrorrg5xh62vcbeqigv3zjcrfk4rtggm/metadata.json' 16 | // // ); 17 | // } catch (error: any) { 18 | // expect(error.message).toBe('Missing trait: Creator'); 19 | // } 20 | 21 | expect(true).toBe(true); 22 | }, 100000); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/validator.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NumericValidator, 3 | StringValidator, 4 | validateResults 5 | } from '../src/common/validator'; 6 | 7 | describe('test validator', () => { 8 | test('#test validator', () => { 9 | let value = new StringValidator().notEmpty().validate(''); 10 | expect(value.ok).toBe(false); 11 | 12 | value = new StringValidator().notEmpty().validate('test'); 13 | expect(value.ok).toBe(true); 14 | 15 | value = new StringValidator().alphanumeric().validate('tes333t'); 16 | expect(value.ok).toBe(true); 17 | 18 | value = new StringValidator().alphanumeric().validate('tes333t@'); 19 | expect(value.ok).toBe(false); 20 | 21 | let numberValue = new NumericValidator().validate(333); 22 | expect(numberValue.ok).toBe(true); 23 | 24 | numberValue = new NumericValidator().minValue(10).validate(9); 25 | expect(numberValue.ok).toBe(false); 26 | 27 | numberValue = new NumericValidator().minValue(10).validate('11'); 28 | expect(numberValue.ok).toBe(false); 29 | 30 | numberValue = new NumericValidator().minValue(10).validate(11); 31 | expect(numberValue.ok).toBe(true); 32 | 33 | numberValue = new NumericValidator().maxValue(10).validate('11'); 34 | expect(numberValue.ok).toBe(false); 35 | 36 | numberValue = new NumericValidator().maxValue(10).validate(11); 37 | expect(numberValue.ok).toBe(false); 38 | 39 | numberValue = new NumericValidator().maxValue(10).validate('9'); 40 | expect(numberValue.ok).toBe(false); 41 | 42 | numberValue = new NumericValidator().maxValue(10).validate(9); 43 | expect(numberValue.ok).toBe(true); 44 | 45 | numberValue = new NumericValidator().integer().validate(9.5); 46 | expect(numberValue.ok).toBe(false); 47 | 48 | numberValue = new NumericValidator().integer().validate(900); 49 | expect(numberValue.ok).toBe(true); 50 | 51 | value = new StringValidator().maxLength(10).validate('123456789011'); 52 | expect(value.ok).toBe(false); 53 | 54 | value = new StringValidator().maxLength(10).validate('123456789'); 55 | expect(value.ok).toBe(true); 56 | 57 | value = new StringValidator().minLength(10).validate('123456789'); 58 | expect(value.ok).toBe(false); 59 | 60 | value = new StringValidator().minLength(10).validate('123456789011'); 61 | expect(value.ok).toBe(true); 62 | 63 | value = new StringValidator().equals('test').validate('test'); 64 | expect(value.ok).toBe(true); 65 | 66 | value = new StringValidator().equals('test').validate('test2'); 67 | expect(value.ok).toBe(false); 68 | 69 | value = new StringValidator().notEquals('test').validate('test'); 70 | expect(value.ok).toBe(false); 71 | 72 | value = new StringValidator().notEquals('test').validate('test2'); 73 | }); 74 | 75 | test('#validateResults', () => { 76 | const error1 = new StringValidator().notEmpty().validate(''); 77 | const error2 = new StringValidator().alphanumeric().validate('abc33$$'); 78 | 79 | try { 80 | validateResults([error1, error2]); 81 | } catch (e: any) { 82 | expect(e.message).toBe( 83 | `Validation Error: Result at index 0: String length must be greater than or equal to 1 but was 0.\nResult at index 1: Value must contain only alphanumeric characters.` 84 | ); 85 | } 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "es2021", 5 | "outDir": "out", 6 | "lib": ["es2021", "DOM"], 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "strict": true, 10 | "strictPropertyInitialization": true, 11 | "strictNullChecks": true, 12 | "skipLibCheck": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "resolveJsonModule": true, 16 | "noUnusedParameters": false, 17 | "esModuleInterop": true, 18 | "declaration": true 19 | }, 20 | "include": ["src/**/*.ts"], 21 | "exclude": ["node_modules", "out", "tests"] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | "no-duplicate-variable": true, 6 | "no-floating-promises": true, 7 | "curly": true, 8 | "class-name": true, 9 | "semicolon": [true, "always"], 10 | "triple-equals": false 11 | }, 12 | "defaultSeverity": "warning" 13 | } 14 | --------------------------------------------------------------------------------