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.json b/package.json index 268ac23..b2facad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@itheum/sdk-mx-data-nft", - "version": "1.1.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", diff --git a/src/datanft.ts b/src/datanft.ts index 15471f4..9e47cc1 100644 --- a/src/datanft.ts +++ b/src/datanft.ts @@ -14,7 +14,8 @@ import { createNftIdentifier, numberToPaddedHex, parseDataNft, - validateSpecificParamsViewData + validateSpecificParamsViewData, + checkStatus } from './utils'; import minterAbi from './abis/datanftmint.abi.json'; import { NftType, ViewDataReturnType } from './interfaces'; @@ -267,6 +268,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; @@ -277,6 +279,7 @@ export class DataNft { fwdHeaderMapLookup?: { [key: string]: any; }; + nestedIdxToStream?: number; }): Promise { DataNft.ensureNetworkConfigSet(); if (!this.dataMarshal) { @@ -298,6 +301,7 @@ export class DataNft { fwdAllHeaders: p.fwdAllHeaders, fwdHeaderKeys: p.fwdHeaderKeys, fwdHeaderMapLookup: p.fwdHeaderMapLookup, + nestedIdxToStream: p.nestedIdxToStream, _mandatoryParamsList: ['signedMessage', 'signableMessage'] }); @@ -354,6 +358,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}`; @@ -375,6 +383,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 || '' @@ -396,6 +418,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[]; @@ -406,6 +429,7 @@ export class DataNft { fwdHeaderKeys?: string; fwdAllHeaders?: boolean; stream?: boolean; + nestedIdxToStream?: number; }): Promise { try { // S: run any format specific validation @@ -416,6 +440,7 @@ export class DataNft { fwdHeaderMapLookup: p.fwdHeaderMapLookup, fwdAllHeaders: p.fwdAllHeaders, stream: p.stream, + nestedIdxToStream: p.nestedIdxToStream, _fwdHeaderMapLookupMustContainBearerAuthHeader: true, _mandatoryParamsList: [ 'mvxNativeAuthOrigins', @@ -466,6 +491,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' && @@ -495,6 +524,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/utils.ts b/src/utils.ts index 4b39857..354e4b8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -102,6 +102,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 }): { @@ -301,6 +302,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 || @@ -309,7 +336,8 @@ export function validateSpecificParamsViewData(params: { !fwdHeaderKeysIsValid || !fwdHeaderMapLookupIsValid || !mvxNativeAuthMaxExpirySecondsValid || - !mvxNativeAuthOriginsIsValid + !mvxNativeAuthOriginsIsValid || + !nestedIdxToStreamValid ) { allPassed = false; } @@ -532,3 +560,11 @@ export async function checkUrlIsUp(url: string, expectedHttpCodes: number[]) { ); } } + +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}` + ); + } +}