├── .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 | [](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 |
--------------------------------------------------------------------------------