diff --git a/README.md b/README.md index 5c9961b..4ae1957 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ const dataNft = new DataNft({ // Create a new DataNft object from API const nonce = 1; -const nft = await DataNft.createFromApi(nonce); +const nft = await DataNft.createFromApi({ nonce }); // Create a new DataNft object from API Response const response = await fetch('https://devnet-api.multiversx.com/address/nfts'); @@ -60,7 +60,7 @@ const dataNfts = []; dataNfts = await DataNft.ownedByAddress(address); // Retrieves the specific DataNft -const dataNft = DataNft.createFromApi(nonce); +const dataNft = DataNft.createFromApi({ nonce }); // (A) Get a message from the Data Marshal node for your to sign to prove ownership const message = await dataNft.messageToSign(); diff --git a/package-lock.json b/package-lock.json index f6c1422..47f9f4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@itheum/sdk-mx-data-nft", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@itheum/sdk-mx-data-nft", - "version": "1.0.0", + "version": "1.1.0", "license": "GPL-3.0-only", "dependencies": { "@multiversx/sdk-core": "12.6.0", diff --git a/package.json b/package.json index 78df9ff..7889621 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@itheum/sdk-mx-data-nft", - "version": "1.0.0", + "version": "1.2.0", "description": "SDK for Itheum's Data NFT Technology on MultiversX Blockchain", "main": "out/index.js", "types": "out/index.d.js", @@ -16,17 +16,17 @@ "author": "Itheum Protocol", "license": "GPL-3.0-only", "dependencies": { - "@multiversx/sdk-core": "12.6.0", - "@multiversx/sdk-network-providers": "1.5.0", - "bignumber.js": "^9.1.1", - "nft.storage": "^7.1.1" + "@multiversx/sdk-core": "12.8.0", + "@multiversx/sdk-network-providers": "2.0.0", + "bignumber.js": "9.1.2", + "nft.storage": "7.1.1" }, "devDependencies": { - "@types/jest": "^29.5.1", - "jest": "^29.5.0", - "ts-jest": "^29.1.0", - "tslint": "^6.1.3", - "typescript": "^5.0.4" + "@types/jest": "29.5.4", + "jest": "29.7.0", + "ts-jest": "29.1.1", + "tslint": "6.1.3", + "typescript": "5.2.2" }, "repository": { "type": "git", diff --git a/src/abis/data-nft-lease.abi.json b/src/abis/data-nft-lease.abi.json new file mode 100644 index 0000000..05adf82 --- /dev/null +++ b/src/abis/data-nft-lease.abi.json @@ -0,0 +1,1069 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.71.0-nightly", + "commitHash": "7f94b314cead7059a71a265a8b64905ef2511796", + "commitDate": "2023-04-23", + "channel": "Nightly", + "short": "rustc 1.71.0-nightly (7f94b314c 2023-04-23)" + }, + "contractCrate": { + "name": "data-nft-lease", + "version": "1.0.0" + }, + "framework": { + "name": "multiversx-sc", + "version": "0.43.2" + } + }, + "name": "DataNftMint", + "constructor": { + "inputs": [ + { + "name": "administrator", + "type": "optional
", + "multi_arg": true + } + ], + "outputs": [] + }, + "endpoints": [ + { + "name": "initializeContract", + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "collection_name", + "type": "bytes" + }, + { + "name": "token_ticker", + "type": "bytes" + }, + { + "name": "mint_time_limit", + "type": "u64" + }, + { + "name": "require_mint_tax", + "type": "bool" + }, + { + "name": "payment", + "type": "optional>", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "setLocalRoles", + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "updateAttributes", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "data_marshal", + "type": "bytes" + }, + { + "name": "data_stream", + "type": "bytes" + }, + { + "name": "data_preview", + "type": "bytes" + }, + { + "name": "creator", + "type": "Address" + }, + { + "name": "title", + "type": "bytes" + }, + { + "name": "description", + "type": "bytes" + } + ], + "outputs": [] + }, + { + "name": "setTransferRole", + "mutability": "mutable", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "unsetTransferRole", + "mutability": "mutable", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "mint", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "name", + "type": "bytes" + }, + { + "name": "media", + "type": "bytes" + }, + { + "name": "metadata", + "type": "bytes" + }, + { + "name": "data_marshal", + "type": "bytes" + }, + { + "name": "data_stream", + "type": "bytes" + }, + { + "name": "data_preview", + "type": "bytes" + }, + { + "name": "royalties", + "type": "BigUint" + }, + { + "name": "title", + "type": "bytes" + }, + { + "name": "description", + "type": "bytes" + } + ], + "outputs": [ + { + "type": "DataNftAttributes" + } + ] + }, + { + "name": "burn", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [], + "outputs": [] + }, + { + "name": "setIsPaused", + "mutability": "mutable", + "inputs": [ + { + "name": "is_paused", + "type": "bool" + } + ], + "outputs": [] + }, + { + "name": "setAntiSpamTax", + "mutability": "mutable", + "inputs": [ + { + "name": "token_id", + "type": "EgldOrEsdtTokenIdentifier" + }, + { + "name": "tax", + "type": "BigUint" + } + ], + "outputs": [] + }, + { + "name": "setTaxIsRequired", + "mutability": "mutable", + "inputs": [ + { + "name": "is_required", + "type": "bool" + } + ], + "outputs": [] + }, + { + "name": "setWhiteListEnabled", + "mutability": "mutable", + "inputs": [ + { + "name": "is_enabled", + "type": "bool" + } + ], + "outputs": [] + }, + { + "name": "setWhiteListSpots", + "mutability": "mutable", + "inputs": [ + { + "name": "whitelist", + "type": "variadic
", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "removeWhiteListSpots", + "mutability": "mutable", + "inputs": [ + { + "name": "whitelist", + "type": "variadic
", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "setMintTimeLimit", + "mutability": "mutable", + "inputs": [ + { + "name": "mint_time_limit", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "setRoyaltiesLimits", + "mutability": "mutable", + "inputs": [ + { + "name": "min_royalties", + "type": "BigUint" + }, + { + "name": "max_royalties", + "type": "BigUint" + } + ], + "outputs": [] + }, + { + "name": "setAdministrator", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "administrator", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "setTax", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "itheum_address", + "type": "Address" + }, + { + "name": "tax", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "setClaimsAddress", + "mutability": "mutable", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "claimRoyalties", + "mutability": "mutable", + "inputs": [ + { + "name": "token_identifier", + "type": "EgldOrEsdtTokenIdentifier" + }, + { + "name": "nonce", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "getTokenId", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "TokenIdentifier" + } + ] + }, + { + "name": "getMintedTokens", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getMintTax", + "mutability": "readonly", + "inputs": [ + { + "name": "token", + "type": "EgldOrEsdtTokenIdentifier" + } + ], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getTaxIsRequired", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "getIsPaused", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "getMaxRoyalties", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getMinRoyalties", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getMintedPerAddress", + "mutability": "readonly", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "mintTimeLimit", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "lastMintTime", + "mutability": "readonly", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "getWhiteList", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "variadic
", + "multi_result": true + } + ] + }, + { + "name": "getCollectionFrozenList", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "variadic
", + "multi_result": true + } + ] + }, + { + "name": "getSftsFrozenForAddress", + "mutability": "readonly", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "variadic", + "multi_result": true + } + ] + }, + { + "name": "getFrozenCount", + "mutability": "readonly", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "isWhiteListEnabled", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "rolesAreSet", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "getAddressesWithTransferRole", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "variadic
", + "multi_result": true + } + ] + }, + { + "name": "getAddressesWithUpdateAttributesRole", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "variadic
", + "multi_result": true + } + ] + }, + { + "name": "getAdministrator", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "getItheumTax", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getItheumTaxAddress", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "getClaimsAddress", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "getUserDataOut", + "mutability": "readonly", + "inputs": [ + { + "name": "address", + "type": "Address" + }, + { + "name": "tax_token", + "type": "EgldOrEsdtTokenIdentifier" + } + ], + "outputs": [ + { + "type": "UserDataOut" + } + ] + }, + { + "name": "pause", + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "unpause", + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "freeze", + "mutability": "mutable", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "unfreeze", + "mutability": "mutable", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "freezeSingleNFT", + "mutability": "mutable", + "inputs": [ + { + "name": "nonce", + "type": "u64" + }, + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "unFreezeSingleNFT", + "mutability": "mutable", + "inputs": [ + { + "name": "nonce", + "type": "u64" + }, + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "wipeSingleNFT", + "mutability": "mutable", + "inputs": [ + { + "name": "nonce", + "type": "u64" + }, + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + } + ], + "events": [ + { + "identifier": "mintPauseToggle", + "inputs": [ + { + "name": "pause_value", + "type": "bool", + "indexed": true + } + ] + }, + { + "identifier": "setTreasuryAddress", + "inputs": [ + { + "name": "treasury_address", + "type": "Address", + "indexed": true + } + ] + }, + { + "identifier": "whitelistEnableToggle", + "inputs": [ + { + "name": "enable_value", + "type": "bool", + "indexed": true + } + ] + }, + { + "identifier": "whitelistSpotSet", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + } + ] + }, + { + "identifier": "collectionFreezeListSpotSet", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + } + ] + }, + { + "identifier": "frozenSftsPerAddress", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + }, + { + "name": "nonce", + "type": "u64", + "indexed": true + } + ] + }, + { + "identifier": "unfrozenSftsPerAddress", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + }, + { + "name": "nonce", + "type": "u64", + "indexed": true + } + ] + }, + { + "identifier": "collectionFreezeListRemoved", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + } + ] + }, + { + "identifier": "whitelistSpotRemoved", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + } + ] + }, + { + "identifier": "setRoyaltiesLimits", + "inputs": [ + { + "name": "min_royalties", + "type": "BigUint", + "indexed": true + }, + { + "name": "max_royalties", + "type": "BigUint", + "indexed": true + } + ] + }, + { + "identifier": "antiSpamTaxSet", + "inputs": [ + { + "name": "token", + "type": "EgldOrEsdtTokenIdentifier", + "indexed": true + }, + { + "name": "amount", + "type": "BigUint", + "indexed": true + } + ] + }, + { + "identifier": "taxIsRequired", + "inputs": [ + { + "name": "tax_is_required", + "type": "bool", + "indexed": true + } + ] + }, + { + "identifier": "mintTimeLimitSet", + "inputs": [ + { + "name": "mint_time_limit", + "type": "u64", + "indexed": true + } + ] + }, + { + "identifier": "setAdministrator", + "inputs": [ + { + "name": "administrator", + "type": "Address", + "indexed": true + } + ] + }, + { + "identifier": "pauseCollection", + "inputs": [ + { + "name": "token_identifier", + "type": "TokenIdentifier", + "indexed": true + } + ] + }, + { + "identifier": "unpauseCollection", + "inputs": [ + { + "name": "token_identifier", + "type": "TokenIdentifier", + "indexed": true + } + ] + }, + { + "identifier": "freeze", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + }, + { + "name": "token_identifier", + "type": "TokenIdentifier", + "indexed": true + }, + { + "name": "nonce", + "type": "u64", + "indexed": true + } + ] + }, + { + "identifier": "unfreeze", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + }, + { + "name": "token_identifier", + "type": "TokenIdentifier", + "indexed": true + }, + { + "name": "nonce", + "type": "u64", + "indexed": true + } + ] + }, + { + "identifier": "wipe", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + }, + { + "name": "token_identifier", + "type": "TokenIdentifier", + "indexed": true + }, + { + "name": "nonce", + "type": "u64", + "indexed": true + } + ] + }, + { + "identifier": "burn", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + }, + { + "name": "token_identifier", + "type": "TokenIdentifier", + "indexed": true + }, + { + "name": "nonce", + "type": "u64", + "indexed": true + }, + { + "name": "amount", + "type": "BigUint", + "indexed": true + } + ] + }, + { + "identifier": "mint", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + }, + { + "name": "amount", + "type": "BigUint", + "indexed": true + }, + { + "name": "token", + "type": "EgldOrEsdtTokenIdentifier", + "indexed": true + }, + { + "name": "price", + "type": "BigUint", + "indexed": true + } + ] + } + ], + "hasCallback": true, + "types": { + "DataNftAttributes": { + "type": "struct", + "fields": [ + { + "name": "data_stream_url", + "type": "bytes" + }, + { + "name": "data_preview_url", + "type": "bytes" + }, + { + "name": "data_marshal_url", + "type": "bytes" + }, + { + "name": "creator", + "type": "Address" + }, + { + "name": "creation_time", + "type": "u64" + }, + { + "name": "title", + "type": "bytes" + }, + { + "name": "description", + "type": "bytes" + } + ] + }, + "UserDataOut": { + "type": "struct", + "fields": [ + { + "name": "anti_spam_tax_value", + "type": "BigUint" + }, + { + "name": "is_paused", + "type": "bool" + }, + { + "name": "max_royalties", + "type": "BigUint" + }, + { + "name": "min_royalties", + "type": "BigUint" + }, + { + "name": "mint_time_limit", + "type": "u64" + }, + { + "name": "last_mint_time", + "type": "u64" + }, + { + "name": "whitelist_enabled", + "type": "bool" + }, + { + "name": "is_whitelisted", + "type": "bool" + }, + { + "name": "minted_per_user", + "type": "BigUint" + }, + { + "name": "total_minted", + "type": "BigUint" + }, + { + "name": "frozen", + "type": "bool" + }, + { + "name": "frozen_nonces", + "type": "List" + } + ] + } + } +} diff --git a/src/datanft.ts b/src/datanft.ts index 7df3728..f2dac8a 100644 --- a/src/datanft.ts +++ b/src/datanft.ts @@ -15,7 +15,8 @@ import { createNftIdentifier, numberToPaddedHex, parseDataNft, - validateSpecificParamsViewData + validateSpecificParamsViewData, + checkStatus } from './utils'; import minterAbi from './abis/datanftmint.abi.json'; import { NftType, ViewDataReturnType } from './interfaces'; @@ -253,6 +254,7 @@ export class DataNft { * @param fwdAllHeaders [optional] Forward all request headers to the Origin Data Stream server. * @param fwdHeaderKeys [optional] Forward only selected headers to the Origin Data Stream server. Has priority over fwdAllHeaders param. A comma separated lowercase string with less than 5 items. e.g. cookie,authorization * @param fwdHeaderMapLookup [optional] Used with fwdHeaderKeys to set a front-end client side lookup map of headers the SDK uses to setup the forward. e.g. { cookie : "xyz", authorization : "Bearer zxy" }. Note that these are case-sensitive and need to match fwdHeaderKeys exactly. + * @param nestedIdxToStream [optional] If you are accessing a "nested stream", this is the index of the nested item you want drill into and fetch */ async viewData(p: { signedMessage: string; @@ -263,6 +265,7 @@ export class DataNft { fwdHeaderMapLookup?: { [key: string]: any; }; + nestedIdxToStream?: number; }): Promise { DataNft.ensureNetworkConfigSet(); if (!this.dataMarshal) { @@ -283,6 +286,7 @@ export class DataNft { fwdAllHeaders: p.fwdAllHeaders, fwdHeaderKeys: p.fwdHeaderKeys, fwdHeaderMapLookup: p.fwdHeaderMapLookup, + nestedIdxToStream: p.nestedIdxToStream, _mandatoryParamsList: ['signedMessage', 'signableMessage'] }); @@ -339,6 +343,10 @@ export class DataNft { url += p.fwdAllHeaders ? '&fwdAllHeaders=1' : ''; } + if (typeof p.nestedIdxToStream !== 'undefined') { + url += `&nestedIdxToStream=${p.nestedIdxToStream}`; + } + if (typeof p.fwdHeaderKeys !== 'undefined') { url += `&fwdHeaderKeys=${p.fwdHeaderKeys}`; @@ -360,6 +368,20 @@ export class DataNft { const contentType = response.headers.get('content-type'); const data = await response.blob(); + // if the marshal returned a error, we should throw it here so that the SDK integrator can handle it + // ... if we don't, the marshal error response is just passed through as a normal data stream response + // ... and the user won't know what went wrong + try { + checkStatus(response); + } catch (e: any) { + // as it's a data marshal error, we get it's payload which is in JSON and send that thrown as text + const errorPayload = await (data as Blob).text(); + + throw new Error( + `${e.toString()}. Detailed error trace follows : ${errorPayload}` + ); + } + return { data: data, contentType: contentType || '' @@ -381,6 +403,7 @@ export class DataNft { * @param fwdHeaderKeys [optional] Forward only selected headers to the Origin Data Stream server. Has priority over fwdAllHeaders param. A comma separated lowercase string with less than 5 items. e.g. cookie,authorization * @param fwdAllHeaders [optional] Forward all request headers to the Origin Data Stream server. * @param stream [optional] Instead of auto-downloading if possible, request if data should always be streamed or not.i.e true=stream, false/undefined=default behavior + * @param nestedIdxToStream [optional] If you are accessing a "nested stream", this is the index of the nested item you want drill into and fetch */ async viewDataViaMVXNativeAuth(p: { mvxNativeAuthOrigins: string[]; @@ -391,6 +414,7 @@ export class DataNft { fwdHeaderKeys?: string; fwdAllHeaders?: boolean; stream?: boolean; + nestedIdxToStream?: number; }): Promise { try { // S: run any format specific validation @@ -401,6 +425,7 @@ export class DataNft { fwdHeaderMapLookup: p.fwdHeaderMapLookup, fwdAllHeaders: p.fwdAllHeaders, stream: p.stream, + nestedIdxToStream: p.nestedIdxToStream, _fwdHeaderMapLookupMustContainBearerAuthHeader: true, _mandatoryParamsList: [ 'mvxNativeAuthOrigins', @@ -451,6 +476,10 @@ export class DataNft { url += p.fwdAllHeaders ? '&fwdAllHeaders=1' : ''; } + if (typeof p.nestedIdxToStream !== 'undefined') { + url += `&nestedIdxToStream=${p.nestedIdxToStream}`; + } + // if fwdHeaderMapLookup exists, send these headers and values to the data marshal for forwarding if ( typeof p.fwdHeaderMapLookup !== 'undefined' && @@ -480,6 +509,20 @@ export class DataNft { const contentType = response.headers.get('content-type'); const data = await response.blob(); + // if the marshal returned a error, we should throw it here so that the SDK integrator can handle it + // ... if we don't, the marshal error response is just passed through as a normal data stream response + // ... and the user won't know what went wrong + try { + checkStatus(response); + } catch (e: any) { + // as it's a data marshal error, we get it's payload which is in JSON and send that thrown as text + const errorPayload = await (data as Blob).text(); + + throw new Error( + `${e.toString()}. Detailed error trace follows : ${errorPayload}` + ); + } + return { data: data, contentType: contentType || '' diff --git a/src/index.ts b/src/index.ts index 875bff2..82ea680 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,5 @@ export * from './datanft'; export * from './utils'; export * from './utils'; export * from './minter'; +export * from './nft-minter'; +export * from './sft-minter'; diff --git a/src/minter.ts b/src/minter.ts index 4f979ce..68f2ff3 100644 --- a/src/minter.ts +++ b/src/minter.ts @@ -20,13 +20,9 @@ import { dataNftTokenIdentifier, imageService, itheumTokenIdentifier, - minterContractAddress, networkConfiguration } from './config'; -import dataNftMintAbi from './abis/datanftmint.abi.json'; -import { MinterRequirements } from './interfaces'; -import { NFTStorage } from 'nft.storage'; -import { File } from '@web-std/file'; +import { NFTStorage, File } from 'nft.storage'; import { checkStatus, checkTraitsUrl, @@ -41,7 +37,7 @@ import { ErrParamValidation } from './errors'; -export class DataNftMinter { +export class Minter { readonly contract: SmartContract; readonly chainID: string; readonly networkProvider: ApiNetworkProvider; @@ -53,12 +49,12 @@ export class DataNftMinter { * @param env 'devnet' | 'mainnet' | 'testnet' * @param timeout Timeout for the network provider (DEFAULT = 10000ms) */ - constructor(env: string, timeout: number = 10000) { - if (!(env in EnvironmentsEnum)) { - throw new ErrNetworkConfig( - `Invalid environment: ${env}, Expected: 'devnet' | 'mainnet' | 'testnet'` - ); - } + protected constructor( + env: string, + contractAddress: string, + abiFile: any, + timeout: number = 10000 + ) { this.env = env; const networkConfig = networkConfiguration[env as EnvironmentsEnum]; this.imageServiceUrl = imageService[env as EnvironmentsEnum]; @@ -69,10 +65,9 @@ export class DataNftMinter { timeout: timeout } ); - const contractAddress = minterContractAddress[env as EnvironmentsEnum]; this.contract = new SmartContract({ address: new Address(contractAddress), - abi: AbiRegistry.create(dataNftMintAbi) + abi: AbiRegistry.create(abiFile) }); } @@ -83,52 +78,6 @@ export class DataNftMinter { return this.contract.getAddress(); } - /** - * Retrieves the minter smart contract requirements for the given user - * @param address the address of the user - * @param taxToken the tax token to be used for the minting (default = `ITHEUM` token identifier based on the {@link EnvironmentsEnum}) - */ - async viewMinterRequirements( - address: IAddress, - taxToken = itheumTokenIdentifier[this.env as EnvironmentsEnum] - ): Promise { - const interaction = this.contract.methodsExplicit.getUserDataOut([ - new AddressValue(address), - new TokenIdentifierValue(taxToken) - ]); - const query = interaction.buildQuery(); - const queryResponse = await this.networkProvider.queryContract(query); - const endpointDefinition = interaction.getEndpoint(); - const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( - queryResponse, - endpointDefinition - ); - if (returnCode.isSuccess()) { - const returnValue = firstValue?.valueOf(); - const requirements: MinterRequirements = { - antiSpamTaxValue: returnValue.anti_spam_tax_value.toNumber(), - contractPaused: returnValue.is_paused, - maxRoyalties: returnValue.max_royalties.toNumber(), - minRoyalties: returnValue.min_royalties.toNumber(), - maxSupply: returnValue.max_supply.toNumber(), - mintTimeLimit: returnValue.mint_time_limit.toNumber(), - lastUserMintTime: returnValue.last_mint_time, - userWhitelistedForMint: returnValue.is_whitelisted, - contractWhitelistEnabled: returnValue.whitelist_enabled, - numberOfMintsForUser: returnValue.minted_per_user.toNumber(), - totalNumberOfMints: returnValue.total_minted.toNumber(), - addressFrozen: returnValue.frozen, - frozenNonces: returnValue.frozen_nonces.map((v: any) => v.toNumber()) - }; - return requirements; - } else { - throw new ErrContractQuery( - 'viewMinterRequirements', - returnCode.toString() - ); - } - } - /** * Retrieves the smart contract pause state */ diff --git a/src/nft-minter.ts b/src/nft-minter.ts new file mode 100644 index 0000000..8ab6cec --- /dev/null +++ b/src/nft-minter.ts @@ -0,0 +1,424 @@ +import { + Address, + AddressValue, + BigUIntValue, + BooleanValue, + ContractCallPayloadBuilder, + ContractFunction, + IAddress, + StringValue, + TokenIdentifierValue, + Transaction, + U64Value +} from '@multiversx/sdk-core/out'; +import { Minter } from './minter'; +import dataNftLeaseAbi from './abis/data-nft-lease.abi.json'; + +export class NftMinter extends Minter { + constructor(env: string, contractAddress: string, timeout: number = 10000) { + super(env, contractAddress, dataNftLeaseAbi, timeout); + } + + /** + * Creates an initialize contract transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param collectionName The name of the NFT collection + * @param tokenTicker The ticker of the NFT collection + * @param mintLimit(seconds)- The mint limit between mints + * @param requireMintTax - A boolean value to set if the mint tax is required or not + * @param options - If `requireMintTax` is true, the `options` object must contain the `taxTokenIdentifier` and `taxTokenAmount` + */ + initializeContract( + senderAddress: IAddress, + collectionName: string, + tokenTicker: string, + mintLimit: number, + requireMintTax: boolean, + options: { + taxTokenIdentifier: string; + taxTokenAmount: number; + } + ): Transaction { + let data; + if (requireMintTax && options) { + data = new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('initializeContract')) + .addArg(new StringValue(collectionName)) + .addArg(new StringValue(tokenTicker)) + .addArg(new BigUIntValue(mintLimit)) + .addArg(new BooleanValue(requireMintTax)) + .addArg(new TokenIdentifierValue(options.taxTokenIdentifier)) + .addArg(new BigUIntValue(options.taxTokenAmount)) + .build(); + } else { + data = new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('initializeContract')) + .addArg(new StringValue(collectionName)) + .addArg(new StringValue(tokenTicker)) + .addArg(new BigUIntValue(mintLimit)) + .addArg(new BooleanValue(requireMintTax)) + .build(); + } + + const initializeContractTx = new Transaction({ + value: 50000000000000000, + data: data, + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + return initializeContractTx; + } + + /** + * Creates a updateAttributes transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param tokenIdentiifer The token identifier of the data nft to update attributes + * @param nonce The nonce of the token to update attributes + * @param attributes The new attributes to update + * @param quantity The quantity of the token to update attributes (default: 1) + */ + updateAttributes( + senderAddress: IAddress, + tokenIdentiifer: string, + nonce: number, + attributes: { + dataMarshalUrl: string; + dataStreamUrl: string; + dataPreviewUrl: string; + creator: IAddress; + title: string; + description: string; + }, + quantity = 1 + ): Transaction { + const updateAttributesTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('ESDTNFTTransfer')) + .addArg(new TokenIdentifierValue(tokenIdentiifer)) + .addArg(new U64Value(nonce)) + .addArg(new U64Value(quantity)) + .addArg(new AddressValue(this.contract.getAddress())) + .addArg(new StringValue('updateAttributes')) + .addArg(new StringValue(attributes.dataMarshalUrl)) + .addArg(new StringValue(attributes.dataStreamUrl)) + .addArg(new StringValue(attributes.dataPreviewUrl)) + .addArg(new AddressValue(attributes.creator)) + .addArg(new StringValue(attributes.title)) + .addArg(new StringValue(attributes.description)) + .build(), + receiver: senderAddress, + gasLimit: 12000000, + sender: senderAddress, + chainID: this.chainID + }); + return updateAttributesTx; + } + + /** + * Creates a setLocalRoles transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + */ + setLocalRoles(senderAddress: IAddress): Transaction { + const setLocalRolesTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('setLocalRoles')) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + return setLocalRolesTx; + } + + /** + * Creates a setTransferRoles transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param address The address to set the transfer roles + */ + setTransferRole(senderAddress: IAddress, address: IAddress): Transaction { + const setTransferRolesTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('setTransferRole')) + .addArg(new AddressValue(address)) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + return setTransferRolesTx; + } + + /** + * Creates an unsetTransferRoles transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param address The address to unset the transfer roles + */ + unsetTransferRole(senderAddress: IAddress, address: IAddress): Transaction { + const unsetTransferRolesTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('unsetTransferRole')) + .addArg(new AddressValue(address)) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + return unsetTransferRolesTx; + } + + /** Creates a pause transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + */ + pauseContract(senderAddress: IAddress): Transaction { + const pauseContractTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('setIsPaused')) + .addArg(new BooleanValue(true)) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + + return pauseContractTx; + } + + /** Creates a unpause transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + */ + unpauseContract(senderAddress: IAddress): Transaction { + const unpauseContractTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('setIsPaused')) + .addArg(new BooleanValue(false)) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + + return unpauseContractTx; + } + + /** Creates a set mint tax transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param is_required A boolean value to set if the mint tax is required or not + */ + setMintTaxIsRequired( + senderAddress: IAddress, + is_required: boolean + ): Transaction { + const setMintTaxIsRequiredTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('setTaxIsRequired')) + .addArg(new BooleanValue(is_required)) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + + return setMintTaxIsRequiredTx; + } + + /** Creates a set mint tax transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param tokenIdentifier The token identifier of the token to set the mint tax + * @param tax The tax to set for the token + */ + setMintTax( + senderAddress: IAddress, + tokenIdentifier: string, + tax: number + ): Transaction { + const setMintTaxTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('setAntiSpamTax')) + .addArg(new TokenIdentifierValue(tokenIdentifier)) + .addArg(new BigUIntValue(tax)) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + return setMintTaxTx; + } + + /** Creates a set mint tax transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param is_enabled A boolean value to set if whitelist is enabled or not + */ + setWhitelistIsEnabled( + senderAddress: IAddress, + is_enabled: boolean + ): Transaction { + const setWhitelistIsEnabledTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('setWhiteListEnabled')) + .addArg(new BooleanValue(is_enabled)) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + return setWhitelistIsEnabledTx; + } + + /** Creates a whitelist transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param addresses The addresses to whitelist + */ + + whitelist( + senderAddress: IAddress, + addresses: string[], + gasLimit = 0 + ): Transaction { + const whitelistTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('setWhiteListSpots')) + .setArgs( + addresses.map((address) => new AddressValue(new Address(address))) + ) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000 + gasLimit, + sender: senderAddress, + chainID: this.chainID + }); + return whitelistTx; + } + + /** Creates a delist transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param addresses The addresses to delist + */ + delist( + senderAddress: IAddress, + addresses: string[], + gasLimit: 0 + ): Transaction { + const delistTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('delist')) + .setArgs( + addresses.map((address) => new AddressValue(new Address(address))) + ) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000 + gasLimit, + sender: senderAddress, + chainID: this.chainID + }); + return delistTx; + } + + /** Creates a set mint time limit transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param timeLimit(seconds) The time limit to set between mints + */ + setMintTimeLimit(senderAddress: IAddress, timeLimit: number): Transaction { + const setMintTimeLimitTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('setMintTimeLimit')) + .addArg(new BigUIntValue(timeLimit)) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + return setMintTimeLimitTx; + } + + /** Sets a new administrator for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param newAdministrator The address of the new administrator + */ + setAdministrator( + senderAddress: IAddress, + newAdministrator: IAddress + ): Transaction { + const setAdministratorTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('setAdministrator')) + .addArg(new AddressValue(newAdministrator)) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + return setAdministratorTx; + } + + /** Sets the claim address for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param claimsAddress The claims address + */ + setClaimsAddress( + senderAddress: IAddress, + claimsAddress: IAddress + ): Transaction { + const setClaimsAddressTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('setClaimsAddress')) + .addArg(new AddressValue(claimsAddress)) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + return setClaimsAddressTx; + } + + /** Creates a claim royalties transaction for the contract + * @param senderAddress The address of the sender, must be the admin of the contract + * @param tokenIdentifier The token identifier of the token to claim royalties + * @param nonce The nonce of the token to claim royalties (default: 0 for ESDT) + */ + claimRoyalties( + senderAddress: IAddress, + tokenIdentifier: string, + nonce = 0 + ): Transaction { + const claimRoyaltiesTx = new Transaction({ + value: 0, + data: new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('claimRoyalties')) + .addArg(new TokenIdentifierValue(tokenIdentifier)) + .addArg(new BigUIntValue(nonce)) + .build(), + receiver: this.contract.getAddress(), + gasLimit: 10000000, + sender: senderAddress, + chainID: this.chainID + }); + return claimRoyaltiesTx; + } +} diff --git a/src/sft-minter.ts b/src/sft-minter.ts new file mode 100644 index 0000000..28a049b --- /dev/null +++ b/src/sft-minter.ts @@ -0,0 +1,69 @@ +import { + AddressValue, + IAddress, + ResultsParser, + TokenIdentifierValue +} from '@multiversx/sdk-core/out'; +import { Minter } from './minter'; +import { + EnvironmentsEnum, + itheumTokenIdentifier, + minterContractAddress +} from './config'; +import { MinterRequirements } from './interfaces'; +import dataNftMinterAbi from './abis/datanftmint.abi.json'; + +export class SftMinter extends Minter { + constructor(env: string, timeout: number = 10000) { + super( + env, + minterContractAddress[env as EnvironmentsEnum], + dataNftMinterAbi, + timeout + ); + } + + /** + * Retrieves the minter smart contract requirements for the given user + * @param address the address of the user + * @param taxToken the tax token to be used for the minting (default = `ITHEUM` token identifier based on the {@link EnvironmentsEnum}) + */ + async viewMinterRequirements( + address: IAddress, + taxToken = itheumTokenIdentifier[this.env as EnvironmentsEnum] + ): Promise { + const interaction = this.contract.methodsExplicit.getUserDataOut([ + new AddressValue(address), + new TokenIdentifierValue(taxToken) + ]); + const query = interaction.buildQuery(); + const queryResponse = await this.networkProvider.queryContract(query); + const endpointDefinition = interaction.getEndpoint(); + const { firstValue, returnCode } = new ResultsParser().parseQueryResponse( + queryResponse, + endpointDefinition + ); + if (returnCode.isSuccess()) { + const returnValue = firstValue?.valueOf(); + const requirements: MinterRequirements = { + antiSpamTaxValue: returnValue.anti_spam_tax_value.toNumber(), + contractPaused: returnValue.is_paused, + maxRoyalties: returnValue.max_royalties.toNumber(), + minRoyalties: returnValue.min_royalties.toNumber(), + maxSupply: returnValue.max_supply.toNumber(), + mintTimeLimit: returnValue.mint_time_limit.toNumber(), + lastUserMintTime: returnValue.last_mint_time, + userWhitelistedForMint: returnValue.is_whitelisted, + contractWhitelistEnabled: returnValue.whitelist_enabled, + numberOfMintsForUser: returnValue.minted_per_user.toNumber(), + totalNumberOfMints: returnValue.total_minted.toNumber(), + addressFrozen: returnValue.frozen, + frozenNonces: returnValue.frozen_nonces.map((v: any) => v.toNumber()) + }; + return requirements; + } else { + throw new Error('Could not retrieve minter contract requirements'); + // throw new ErrContractQuery('Could not retrieve requirements'); + } + } +} diff --git a/src/utils.ts b/src/utils.ts index 959034a..b85938d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -111,6 +111,7 @@ export function validateSpecificParamsViewData(params: { mvxNativeAuthMaxExpirySeconds?: number | undefined; mvxNativeAuthOrigins?: string[] | undefined; fwdHeaderMapLookup?: any; + nestedIdxToStream?: number | undefined; _fwdHeaderMapLookupMustContainBearerAuthHeader?: boolean | undefined; _mandatoryParamsList: string[]; // a pure JS fallback way to validate mandatory params, as typescript rules for mandatory can be bypassed by client app }): { @@ -310,6 +311,32 @@ export function validateSpecificParamsViewData(params: { } } + // nestedIdxToStream test + let nestedIdxToStreamValid = true; + + if ( + params.nestedIdxToStream !== undefined || + params._mandatoryParamsList.includes('nestedIdxToStream') + ) { + nestedIdxToStreamValid = false; + + const nestedIdxToStreamToInt = + params.nestedIdxToStream !== undefined + ? parseInt(params.nestedIdxToStream.toString(), 10) + : null; + + if ( + nestedIdxToStreamToInt !== null && + !isNaN(nestedIdxToStreamToInt) && + nestedIdxToStreamToInt >= 0 + ) { + nestedIdxToStreamValid = true; + } else { + validationMessages += + '[nestedIdxToStream needs to be a number more than 0]'; + } + } + if ( !signedMessageValid || !signableMessageValid || @@ -318,7 +345,8 @@ export function validateSpecificParamsViewData(params: { !fwdHeaderKeysIsValid || !fwdHeaderMapLookupIsValid || !mvxNativeAuthMaxExpirySecondsValid || - !mvxNativeAuthOriginsIsValid + !mvxNativeAuthOriginsIsValid || + !nestedIdxToStreamValid ) { allPassed = false; } @@ -452,7 +480,7 @@ export function validateSpecificParamsMint(params: { typeof params.royalties === 'number' && !(params.royalties % 1 != 0) && // modulus checking. (10 % 1 != 0) EQ false, (10.5 % 1 != 0) EQ true, params.royalties >= 0 && - params.royalties <= 50 + params.royalties <= 5000 ) { royaltiesValid = true; } else { @@ -545,3 +573,11 @@ export function checkStatus(response: Response) { throw new ErrFetch(response.status, response.statusText); } } + +export function checkStatus(response: Response) { + if (!(response.status >= 200 && response.status <= 299)) { + throw new Error( + `Response returned non-success HTTP code. status = ${response?.status} statusText = ${response?.statusText}` + ); + } +}