diff --git a/src/datanft.ts b/src/datanft.ts index 15471f4..7df3728 100644 --- a/src/datanft.ts +++ b/src/datanft.ts @@ -11,6 +11,7 @@ import { networkConfiguration } from './config'; import { + checkStatus, createNftIdentifier, numberToPaddedHex, parseDataNft, @@ -18,13 +19,13 @@ import { } from './utils'; import minterAbi from './abis/datanftmint.abi.json'; import { NftType, ViewDataReturnType } from './interfaces'; -// import { -// ErrDataNftCreation, -// ErrDecodeAttributes, -// ErrFailedOperation, -// ErrAttributeNotSet, -// ErrNetworkConfig -// } from './errors'; +import { + ErrAttributeNotSet, + ErrDataNftCreate, + ErrDecodeAttributes, + ErrFetch, + ErrNetworkConfig +} from './errors'; export class DataNft { readonly tokenIdentifier: string = ''; @@ -60,6 +61,11 @@ export class DataNft { * @param env 'devnet' | 'mainnet' | 'testnet' */ static setNetworkConfig(env: string) { + if (!(env in EnvironmentsEnum)) { + throw new ErrNetworkConfig( + `Invalid environment: ${env}, Expected: 'devnet' | 'mainnet' | 'testnet'` + ); + } this.env = env; this.networkConfiguration = networkConfiguration[env as EnvironmentsEnum]; this.apiConfiguration = apiConfiguration[env as EnvironmentsEnum]; @@ -82,20 +88,19 @@ export class DataNft { dataNftTokenIdentifier[this.env as EnvironmentsEnum], token.nonce ); + const response = await fetch(`${this.apiConfiguration}/nfts/${identifier}`); + + checkStatus(response); + const data: NftType = await response.json(); try { const dataNft = parseDataNft(data); return dataNft; - } catch (error) { - throw error; - // if (error instanceof Error) { - // throw new ErrDataNftCreation(error); - // } else { - // throw ErrDataNftCreation; - // } + } catch (error: any) { + throw new ErrDataNftCreate('Response could not be parsed'); } } @@ -118,24 +123,22 @@ export class DataNft { nonce ) ); - try { - const response = await fetch( - `${this.apiConfiguration}/nfts?identifiers=${identifiers.join( - ',' - )}&withSupply=true` - ); - const data: NftType[] = await response.json(); + const response = await fetch( + `${this.apiConfiguration}/nfts?identifiers=${identifiers.join( + ',' + )}&withSupply=true` + ); + + checkStatus(response); + const data: NftType[] = await response.json(); + + try { const dataNfts = data.map((value) => parseDataNft(value)); return dataNfts; - } catch (error) { - throw error; - // if (error instanceof Error) { - // throw new ErrDataNftCreation(error); - // } else { - // throw ErrDataNftCreation; - // } + } catch (error: any) { + throw new ErrDataNftCreate('Response could not be parsed'); } } @@ -160,13 +163,8 @@ export class DataNft { parseNft(payload as NftType); return dataNfts; } - } catch (error) { - throw error; - // if (error instanceof Error) { - // throw new ErrDataNftCreation(error); - // } else { - // throw ErrDataNftCreation; - // } + } catch (error: any) { + throw new ErrDataNftCreate('Response could not be parsed'); } } @@ -195,9 +193,8 @@ export class DataNft { description: decodedAttributes['description'].toString(), title: decodedAttributes['title'].toString() }; - } catch (error) { - throw error; - // throw ErrDecodeAttributes; + } catch (error: any) { + throw new ErrDecodeAttributes(error.message); } } @@ -211,22 +208,17 @@ export class DataNft { identifier = dataNftTokenIdentifier[this.env as EnvironmentsEnum] ): Promise { this.ensureNetworkConfigSet(); - try { - const res = await fetch( - `${this.apiConfiguration}/accounts/${address}/nfts?size=10000&collections=${identifier}&withSupply=true` - ); - const data = await res.json(); - const dataNfts: DataNft[] = this.createFromApiResponseOrBulk(data); - return dataNfts; - } catch (error) { - throw error; - // if (error instanceof Error) { - // throw new ErrFailedOperation(this.ownedByAddress.name, error); - // } else { - // throw ErrFailedOperation; - // } - } + const res = await fetch( + `${this.apiConfiguration}/accounts/${address}/nfts?size=10000&collections=${identifier}&withSupply=true` + ); + + checkStatus(res); + + const data = await res.json(); + const dataNfts: DataNft[] = this.createFromApiResponseOrBulk(data); + + return dataNfts; } /** @@ -235,28 +227,22 @@ export class DataNft { async getMessageToSign(): Promise { DataNft.ensureNetworkConfigSet(); if (!this.dataMarshal) { - throw new Error('No data marshal set for getMessageToSign'); - // throw new ErrAttributeNotSet('dataMarshal'); - } - try { - const res = await fetch( - `${this.dataMarshal}/preaccess?chainId=${ - DataNft.networkConfiguration.chainID == 'D' - ? 'ED' - : DataNft.networkConfiguration.chainID - }` - ); - const data = await res.json(); - - return data.nonce; - } catch (error) { - throw error; - // if (error instanceof Error) { - // throw new ErrFailedOperation(this.getMessageToSign.name, error); - // } else { - // throw ErrFailedOperation; - // } + throw new ErrAttributeNotSet('dataMarshal'); } + + const res = await fetch( + `${this.dataMarshal}/preaccess?chainId=${ + DataNft.networkConfiguration.chainID == 'D' + ? 'ED' + : DataNft.networkConfiguration.chainID + }` + ); + + checkStatus(res); + + const data = await res.json(); + + return data.nonce; } /** @@ -280,8 +266,7 @@ export class DataNft { }): Promise { DataNft.ensureNetworkConfigSet(); if (!this.dataMarshal) { - throw new Error('No data marshal set for viewData'); - // throw new ErrAttributeNotSet('dataMarshal'); + throw new ErrAttributeNotSet('dataMarshal'); } const signResult = { signature: '', @@ -510,10 +495,7 @@ export class DataNft { private static ensureNetworkConfigSet() { if (!this.env || !this.apiConfiguration) { - throw new Error( - 'Network configuration is not set. Call setNetworkConfig static method before calling any method that requires network configuration.' - ); - // throw ErrNetworkConfig; + throw new ErrNetworkConfig(); } } } diff --git a/src/errors.ts b/src/errors.ts index 0556be6..1ec6864 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,72 +1,76 @@ -/** - * The base class for exceptions (errors). - */ -export class Err extends Error { - inner: Error | undefined = undefined; - - public constructor(message: string, inner?: Error) { - super(message); - this.inner = inner; +export class ErrNetworkConfig extends Error { + public constructor(message?: string) { + super( + message || + 'Network configuration is not set. Call setNetworkConfig static method before calling any method that requires network configuration.' + ); } +} - /** - * Returns a pretty, friendly summary for the error or for the chain of errors (if appropriate). - */ - summary(): any[] { - let result = []; - - result.push({ name: this.name, message: this.message }); - - let inner: any = this.inner; - while (inner) { - result.push({ name: inner.name, message: inner.message }); - inner = inner.inner; - } - - return result; +export class ErrInvalidArgument extends Error { + public constructor(message: string) { + super(`Invalid argument: ${message}`); } } -export class ErrNetworkConfig extends Err { - public constructor() { +export class ErrBadType extends Error { + public constructor(name: string, type: any, value?: any, context?: string) { super( - 'Network configuration is not set. Call setNetworkConfig static method before calling any method that requires network configuration.' + `Bad type of "${name}": ${value}. Expected type: ${type}. Context: ${context}` ); } } -export class ErrDataNftCreation extends Err { - public constructor(inner?: Error) { - super(`Could not create DataNft:`, inner); +export class ErrDataNftCreate extends Error { + public constructor(message?: string) { + super(`Failed to create Data NFT: ${message}`); } } -export class ErrDecodeAttributes extends Err { - public constructor() { - super('Could not decode attributes'); +export class ErrFetch extends Error { + public constructor(status: number, message: string) { + super(`Fetch error with status code: ${status} and message: ${message}`); } } -export class ErrAttributeNotSet extends Err { - public constructor(attributeName: string) { - super(`Attribute ${attributeName} is not set`); +export class ErrDecodeAttributes extends Error { + public constructor(message?: string) { + super(`Failed to decode attributes: ${message}`); } } -export class ErrArgumentNotSet extends Err { - public constructor(argumentName: string, message: string) { - super(`Argument ${argumentName} is not set. ${message}`); +export class ErrAttributeNotSet extends Error { + public constructor(attribute: string) { + super(`Attribute "${attribute}" is not set`); } } -export class ErrFailedOperation extends Err { - public constructor(methodName: string, inner?: Error) { - super(`Function failed: ${methodName}:`, inner); +export class ErrContractQuery extends Error { + public constructor(method: string, message?: string) { + super(`Failed to query contract: Method: ${method} : ${message}`); } } -export class ErrContractQuery extends Err { +export class ErrParamValidation extends Error { public constructor(message: string) { - super(`Contract query failed: ${message}`); + super(`Params have validation issues : ${message}`); + } +} + +export class ErrFailedOperation extends Error { + public constructor(method: string, message?: string) { + super(`Failed to perform operation: ${method} : ${message}`); + } +} + +export class ErrMissingTrait extends Error { + public constructor(trait: string) { + super(`Missing trait: ${trait}`); + } +} + +export class ErrMissingValueForTrait extends Error { + public constructor(trait: string) { + super(`Missing value for trait: ${trait}`); } } diff --git a/src/marketplace.ts b/src/marketplace.ts index a3c5b38..df29383 100644 --- a/src/marketplace.ts +++ b/src/marketplace.ts @@ -26,7 +26,7 @@ import { import dataMarketAbi from './abis/data_market.abi.json'; import { MarketplaceRequirements, Offer } from './interfaces'; import { parseOffer } from './utils'; -// import { ErrContractQuery } from './errors'; +import { ErrContractQuery, ErrNetworkConfig } from './errors'; export class DataNftMarket { readonly contract: SmartContract; @@ -40,6 +40,11 @@ export class DataNftMarket { * @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'` + ); + } this.env = env; const networkConfig = networkConfiguration[env as EnvironmentsEnum]; this.chainID = networkConfig.chainID; @@ -87,7 +92,10 @@ export class DataNftMarket { ); return offers; } else { - return []; + throw new ErrContractQuery( + 'viewAddressListedOffers', + returnCode.toString() + ); } } @@ -122,7 +130,10 @@ export class DataNftMarket { ); return offers; } else { - return []; + throw new ErrContractQuery( + 'viewAddressPagedOffers', + returnCode.toString() + ); } } @@ -145,7 +156,10 @@ export class DataNftMarket { const returnValue = firstValue?.valueOf(); return returnValue.toNumber(); } else { - return 0; + throw new ErrContractQuery( + 'viewAddressTotalOffers', + returnCode.toString() + ); } } @@ -171,7 +185,10 @@ export class DataNftMarket { ); return offers; } else { - return []; + throw new ErrContractQuery( + 'viewAddressCancelledOffers', + returnCode.toString() + ); } } @@ -199,7 +216,7 @@ export class DataNftMarket { ); return offers; } else { - return []; + throw new ErrContractQuery('viewPagedOffers', returnCode.toString()); } } @@ -248,7 +265,7 @@ export class DataNftMarket { ); return offers; } else { - return []; + throw new ErrContractQuery('viewOffers', returnCode.toString()); } } @@ -281,10 +298,7 @@ export class DataNftMarket { }; return requirements; } else { - throw new Error('Error while retrieving the marketplace requirements'); - // throw new ErrContractQuery( - // 'Error while retrieving the marketplace requirements' - // ); + throw new ErrContractQuery('viewRequirements', returnCode.toString()); } } @@ -304,9 +318,7 @@ export class DataNftMarket { const returnValue = firstValue?.valueOf(); return new U8Value(returnValue).valueOf().toNumber(); } - - throw new Error('Error while retrieving the number of offers'); - // throw new ErrContractQuery('Error while retrieving the number of offers'); + throw new ErrContractQuery('viewNumberOfOffers', returnCode.toString()); } /** @@ -326,10 +338,7 @@ export class DataNftMarket { return new U64Value(returnValue).valueOf().toNumber(); } - throw new Error('Error while retrieving the last valid offer id'); - // throw new ErrContractQuery( - // 'Error while retrieving the last valid offer id' - // ); + throw new ErrContractQuery('viewLastValidOfferId', returnCode.toString()); } /** @@ -348,10 +357,10 @@ export class DataNftMarket { const returnValue = firstValue?.valueOf(); return new BooleanValue(returnValue).valueOf(); } else { - throw new Error('Error while retrieving the contract pause state'); - // throw new ErrContractQuery( - // 'Error while retrieving the contract pause state' - // ); + throw new ErrContractQuery( + 'viewContractPauseState', + returnCode.toString() + ); } } diff --git a/src/minter.ts b/src/minter.ts index 710b13c..4f979ce 100644 --- a/src/minter.ts +++ b/src/minter.ts @@ -28,15 +28,18 @@ import { MinterRequirements } from './interfaces'; import { NFTStorage } from 'nft.storage'; import { File } from '@web-std/file'; import { + checkStatus, checkTraitsUrl, checkUrlIsUp, validateSpecificParamsMint } from './utils'; -// import { -// ErrArgumentNotSet, -// ErrContractQuery, -// ErrFailedOperation -// } from './errors'; +import { + ErrBadType, + ErrContractQuery, + ErrFailedOperation, + ErrNetworkConfig, + ErrParamValidation +} from './errors'; export class DataNftMinter { readonly contract: SmartContract; @@ -51,6 +54,11 @@ export class DataNftMinter { * @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'` + ); + } this.env = env; const networkConfig = networkConfiguration[env as EnvironmentsEnum]; this.imageServiceUrl = imageService[env as EnvironmentsEnum]; @@ -114,8 +122,10 @@ export class DataNftMinter { }; return requirements; } else { - throw new Error('Could not retrieve minter contract requirements'); - // throw new ErrContractQuery('Could not retrieve requirements'); + throw new ErrContractQuery( + 'viewMinterRequirements', + returnCode.toString() + ); } } @@ -135,10 +145,10 @@ export class DataNftMinter { const returnValue = firstValue?.valueOf(); return new BooleanValue(returnValue).valueOf(); } else { - throw new Error('Error while retrieving the contract pause state'); - // throw new ErrContractQuery( - // 'Error while retrieving the contract pause state' - // ); + throw new ErrContractQuery( + 'viewContractPauseState', + returnCode.toString() + ); } } @@ -247,27 +257,15 @@ export class DataNftMinter { }); if (!allPassed) { - throw new Error(`Params have validation issues = ${validationMessages}`); - // throw new ErrFailedOperation( - // this.mint.name, - // new Error(`params have validation issues = ${validationMessages}`) - // ); + throw new ErrParamValidation(validationMessages); } // E: run any format specific validation... // deep validate all mandatory URLs - try { - await checkUrlIsUp(dataStreamUrl, [200, 403]); - await checkUrlIsUp(dataPreviewUrl, [200]); - await checkUrlIsUp(dataMarshalUrl + '/health-check', [200]); - } catch (error) { - throw error; - // if (error instanceof Error) { - // throw new ErrFailedOperation(this.mint.name, error); - // } else { - // throw new ErrFailedOperation(this.mint.name); - // } - } + + await checkUrlIsUp(dataStreamUrl, [200, 403]); + await checkUrlIsUp(dataPreviewUrl, [200]); + await checkUrlIsUp(dataMarshalUrl + '/health-check', [200]); let imageOnIpfsUrl: string; let metadataOnIpfsUrl: string; @@ -277,13 +275,12 @@ export class DataNftMinter { if (!imageUrl) { if (!nftStorageToken) { - throw new Error( + throw new ErrBadType( + 'nftStorageToken', + 'string', + nftStorageToken, 'NFT Storage token is required when not using custom image and traits' ); - // throw new ErrArgumentNotSet( - // 'nftStorageToken', - // 'NFT Storage token is required when not using custom image and traits' - // ); } const { image, traits } = await this.createFileFromUrl( `${this.imageServiceUrl}/v1/generateNFTArt?hash=${dataNftHash}`, @@ -302,11 +299,12 @@ export class DataNftMinter { metadataOnIpfsUrl = metadataIpfsUrl; } else { if (!traitsUrl) { - throw new Error('Traits URL is required when using custom image'); - // throw new ErrArgumentNotSet( - // 'traitsUrl', - // 'Traits URL is required when using custom image' - // ); + throw new ErrBadType( + 'traitsUrl', + 'string', + traitsUrl, + 'Traits URL is required when using custom image' + ); } await checkTraitsUrl(traitsUrl); @@ -374,29 +372,21 @@ export class DataNftMinter { body: JSON.stringify({ dataNFTStreamUrl }) }; - try { - const res = await fetch(`${dataMarshalUrl}/generate`, requestOptions); - const data = await res.json(); + const res = await fetch(`${dataMarshalUrl}/generate`, requestOptions); + const data = await res.json(); - if (data && data.encryptedMessage && data.messageHash) { - return { - dataNftHash: data.messageHash, - dataNftStreamUrlEncrypted: data.encryptedMessage - }; - } else { - throw new Error('Issue with data marshal generate payload'); - // throw new ErrFailedOperation(this.dataNFTDataStreamAdvertise.name); - } - } catch (error) { - throw error; - // if (error instanceof Error) { - // throw new ErrFailedOperation( - // this.dataNFTDataStreamAdvertise.name, - // error - // ); - // } else { - // throw new ErrFailedOperation(this.dataNFTDataStreamAdvertise.name); - // } + checkStatus(res); + + if (data && data.encryptedMessage && data.messageHash) { + return { + dataNftHash: data.messageHash, + dataNftStreamUrlEncrypted: data.encryptedMessage + }; + } else { + throw new ErrFailedOperation( + 'dataNFTDataStreamAdvertise', + 'Invalid response from Data Marshal' + ); } } @@ -412,9 +402,8 @@ export class DataNftMinter { }); const dir = [image, traits]; res = await nftstorage.storeDirectory(dir); - } catch (error) { - throw error; - // throw new ErrFailedOperation(this.storeToIpfs.name); + } catch (error: any) { + throw new ErrFailedOperation('storeToIpfs', error.message); } return { imageOnIpfsUrl: `https://ipfs.io/ipfs/${res}/image.png`, diff --git a/src/utils.ts b/src/utils.ts index 4b39857..959034a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,13 @@ import BigNumber from 'bignumber.js'; import { DataNft } from './datanft'; import { NftEnumType, NftType, Offer } from './interfaces'; +import { + ErrBadType, + ErrFetch, + ErrInvalidArgument, + ErrMissingTrait, + ErrMissingValueForTrait +} from './errors'; export function numberToPaddedHex(value: BigNumber.Value) { let hex = new BigNumber(value).toString(16); @@ -63,12 +70,14 @@ export async function checkTraitsUrl(traitsUrl: string) { const traitsResponse = await fetch(traitsUrl); const traits = await traitsResponse.json(); + checkStatus(traitsResponse); + if (!traits.description) { - throw new Error('Traits description is empty'); + throw new ErrMissingTrait(traits.description); } if (!Array.isArray(traits.attributes)) { - throw new Error('Traits attributes must be an array'); + throw new ErrMissingTrait(traits.attributes); } const requiredTraits = ['Creator', 'Data Preview URL']; @@ -80,13 +89,13 @@ export async function checkTraitsUrl(traitsUrl: string) { (attribute: any) => attribute.trait_type === requiredTrait ) ) { - throw new Error(`Missing required trait: ${requiredTrait}`); + throw new ErrMissingTrait(requiredTrait); } } for (const attribute of traitsAttributes) { if (!attribute.value) { - throw new Error(`Empty value for trait: ${attribute.trait_type}`); + throw new ErrMissingValueForTrait(attribute.trait_type); } } } @@ -527,8 +536,12 @@ export async function checkUrlIsUp(url: string, expectedHttpCodes: number[]) { const response = await fetch(url); if (!expectedHttpCodes.includes(response.status)) { - throw new Error( - `URL needs to return a 200 OK response code (or a 403 Forbidden error code is also allowed for protected Data Streams). url : ${url}, actual HTTP status: ${response.status}` - ); + throw new ErrFetch(response.status, response.statusText); + } +} + +export function checkStatus(response: Response) { + if (!(response.status >= 200 && response.status <= 299)) { + throw new ErrFetch(response.status, response.statusText); } } diff --git a/tests/datanft.test.ts b/tests/datanft.test.ts index 40d8732..342b1a2 100644 --- a/tests/datanft.test.ts +++ b/tests/datanft.test.ts @@ -5,12 +5,10 @@ describe('Data NFT test', () => { test('#test not setting network config', async () => { try { await DataNft.createFromApi({ nonce: 62 }); - } catch (error) { - if (error instanceof Error) { - expect(error.message).toBe( - 'Network configuration is not set. Call setNetworkConfig static method before calling any method that requires network configuration.' - ); - } + } catch (error: any) { + expect(error.message).toBe( + 'Network configuration is not set. Call setNetworkConfig static method before calling any method that requires network configuration.' + ); } }); @@ -26,12 +24,24 @@ describe('Data NFT test', () => { signableMessage: new SignableMessage({ message: Buffer.from('test') }), stream: true }); - } catch (error) { - if (error instanceof Error) { - expect(error.message).toBe( - 'Network configuration is not set. Call setNetworkConfig static method before calling any method that requires network configuration.' - ); - } + } catch (error: any) { + expect(error.message).toBe( + 'Network configuration is not set. Call setNetworkConfig static method before calling any method that requires network configuration.' + ); + } + }); + + test('#test bad input on createFromApi', async () => { + try { + DataNft.setNetworkConfig('devnet'); + await DataNft.createFromApi({ + nonce: 62, + tokenIdentifier: 'DATANFTFT3-d0978a' + }); + } catch (error: any) { + expect(error.message).toBe( + 'Fetch error with status code: 404 and message: Not Found' + ); } }); @@ -44,7 +54,6 @@ describe('Data NFT test', () => { const nonceToSign = await dataNft.getMessageToSign(); expect(typeof nonceToSign).toBe('string'); - const nft = await DataNft.createFromApi({ nonce: 62, tokenIdentifier: 'DATANFTFT3-d0978e' diff --git a/tests/traits-check.test.ts b/tests/traits-check.test.ts index 4f40f59..7f0d8c9 100644 --- a/tests/traits-check.test.ts +++ b/tests/traits-check.test.ts @@ -15,8 +15,8 @@ describe('Traits strucutre test', () => { await checkTraitsUrl( 'https://ipfs.io/ipfs/bafybeicbmpiehja5rjk425ol4rmrorrg5xh62vcbeqigv3zjcrfk4rtggm/metadata.json' ); - } catch (error) { - expect(error).toStrictEqual(Error('Missing required trait: Creator')); + } catch (error: any) { + expect(error.message).toBe('Missing trait: Creator'); } }, 100000); });