Skip to content

Commit

Permalink
feat/metadata-field-hip-766 (#64)
Browse files Browse the repository at this point in the history
Signed-off-by: michalrozekariane <[email protected]>
Signed-off-by: kacper-koza-arianelabs <[email protected]>
Co-authored-by: kacper-koza-arianelabs <[email protected]>
  • Loading branch information
1 parent d34d09f commit f9ee518
Show file tree
Hide file tree
Showing 19 changed files with 1,072 additions and 607 deletions.
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This package includes all sorts of tooling for the Hedera NFT ecosystem. Some ad
- **PinataService:** Utilizes Pinata Cloud to pin files to IPFS, providing an `ipfs://` URL upon successful upload. Includes metadata and options customization.
- **NftStorageService:** Integrates with the NFT.storage API to upload files directly to IPFS and returns an `ipfs://` URL. It supports dynamic API key usage based on a provided list.
- **MockStorageService:** A mock storage service for testing purposes, returning predefined URLs.
15. **Collection Metadata Validation** Provides a possibility to validate the collection metadata of a collection against the [HIP-766 metadata schema](https://hips.hedera.com/hip/hip-766). This method can handle both direct HTTP(S) URLs or IPFS CIDs as input. If an IPFS CID is provided without a full URL, the method will attempt to use an IPFS gateway to retrieve the metadata. The IPFS gateway parameter is optional; if not specified and required (i.e., when a CID is provided without a full URL), the method will throw an error indicating that the IPFS gateway is required. This ensures that the metadata conforms to the standards required for NFT collections on the Hedera network. The validator provides a thorough check, highlighting any errors or missing fields in the collection metadata structure.

## Table of Contents

Expand All @@ -48,7 +49,7 @@ This package includes all sorts of tooling for the Hedera NFT ecosystem. Some ad
- **Package: [Prepare Metadata Objects From CSV Rows](#prepare-metadata-objects-from-csv-rows)**
- **Package: [Upload Service - with Node.js features](#upload-service)**
- **Package: [File Storage Services](#file-storage-services)**
- **[Changes in browser bundle](#changes-in-browser-bundle)**
- **Package: [Collection Metadata Validation](#collection-metadata-validation)**
- **[Questions, contact us, or improvement proposals?](#questions-or-improvement-proposals)**
- **[Support](#Support)**
- **[Contributing](#Contributing)**
Expand Down Expand Up @@ -1841,6 +1842,39 @@ Listed features utilized in browser environment will throw an error.

After downloading the repo run `npm run build` to build the SDK.

## Collection Metadata Validation

The `validateCollectionMetadata` validates the metadata of a collection against the [HIP-766 metadata schema](https://hips.hedera.com/hip/hip-766) using a specified IPFS gateway to retrieve the metadata. This method ensures that the metadata conforms to the standards required for NFT collections on the Hedera network, offering a comprehensive check that highlights any errors or missing fields in the metadata structure.

### Usage

```ts
const collectionMetadataValidationResult = await validateCollectionMetadata(metadataURL, ipfsGateway);
```

### Parameters

- `metadataURL`: The URL or CID pointing to the metadata file. This can be a direct HTTP(S) URL or an IPFS CID. If only a CID is provided, an IPFS gateway URL must be specified unless the CID is already included in a full URL format.
- `ipfsGateway`: Optional. Specifies the IPFS gateway URL to be used for resolving a CID to an HTTP URL. If not provided, and a CID without a full URL is used, the method will throw an error indicating the necessity for an IPFS gateway.

### Output

This method returns an object that contains:

- `isValid`: A boolean flag indicating whether the metadata conforms to the HIP-766 metadata schema.
- `errors`: An array of strings detailing any issues found during the validation process. This array is empty if no errors are present.

### Example result

```ts
type ValidationResult = {
isValid: boolean;
errors: string[];
};
```

---

## Questions or Improvement Proposals

Please create an issue or PR on [this repository](https://github.com/hashgraph/hedera-nft-sdk). Make sure to join the [Hedera Discord server](https://hedera.com/discord) to ask questions or discuss improvement suggestions.
Expand Down
1,187 changes: 595 additions & 592 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions src/helpers/decode-metadata-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,23 @@
import { dictionary } from '../utils/constants/dictionary';
import { errorToMessage } from './error-to-message';

export const decodeMetadataUrl = (encodedMetadata: string, ipfsGateway?: string): string => {
let decodedUrl = '';
export const decodeMetadataURL = (metadataURL: string, ipfsGateway?: string, isMetadataURLBase64?: boolean): string => {
let decodedURL = '';
try {
decodedUrl = atob(encodedMetadata);
decodedURL = isMetadataURLBase64 ? atob(metadataURL) : metadataURL;
} catch (error) {
throw new Error(errorToMessage(error));
}

if (!decodedUrl.startsWith('https://') && !decodedUrl.startsWith('http://') && !ipfsGateway) {
if (!decodedURL.startsWith('https://') && !decodedURL.startsWith('http://') && !ipfsGateway) {
throw new Error(dictionary.errors.ipfsGatewayRequired);
}

if (decodedUrl.startsWith('ipfs://') && ipfsGateway) {
decodedUrl = decodedUrl.replace('ipfs://', ipfsGateway);
} else if (!decodedUrl.startsWith('https://') && !decodedUrl.startsWith('http://') && ipfsGateway) {
decodedUrl = `${ipfsGateway}${decodedUrl}`;
if (decodedURL.startsWith('ipfs://') && ipfsGateway) {
decodedURL = decodedURL.replace('ipfs://', ipfsGateway);
} else if (!decodedURL.startsWith('https://') && !decodedURL.startsWith('http://') && ipfsGateway) {
decodedURL = `${ipfsGateway}${decodedURL}`;
}

return decodedUrl;
return decodedURL;
};
4 changes: 2 additions & 2 deletions src/helpers/uri-decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
*
*/
import { NFTDetails, DecodedMetadata } from '../types/nfts';
import { decodeMetadataUrl } from './decode-metadata-url';
import { decodeMetadataURL } from './decode-metadata-url';

export const uriDecoder = (nfts: NFTDetails | NFTDetails[], ipfsGateway?: string): DecodedMetadata[] => {
const nftsArray = Array.isArray(nfts) ? nfts : [nfts];
const decodedMetadataArray: DecodedMetadata[] = nftsArray.map((nft: NFTDetails) => {
const decodedNFTMetadata = decodeMetadataUrl(nft.metadata, ipfsGateway);
const decodedNFTMetadata = decodeMetadataURL(nft.metadata, ipfsGateway, true);

return {
metadata: decodedNFTMetadata,
Expand Down
63 changes: 63 additions & 0 deletions src/hip766-collection-metadata-validator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*-
*
* Hedera NFT SDK
*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import axios from 'axios';
import { decodeMetadataURL } from '../helpers/decode-metadata-url';
import { CollectionMetadataSchema } from '../utils/validation-schemas/hip766-collection-metadata-schema';
import { validateObjectWithSchema, validationMetadataErrorOptions } from '../helpers/validate-object-with-schema';
import { ValidationError } from '../utils/validation-error';
import { errorToMessage } from '../helpers/error-to-message';
import { dictionary } from '../utils/constants/dictionary';

export const validateCollectionMetadata = async (
metadataURL: string,
ipfsGateway?: string
): Promise<{ errors: string[]; isValid: boolean }> => {
const errors: string[] = [];

if (metadataURL.length === 0) {
errors.push(dictionary.validation.uriIsRequired);
return { errors, isValid: false };
}

const decodedUri = decodeMetadataURL(metadataURL, ipfsGateway);

try {
const response = await axios.get(decodedUri, { responseType: 'json' });
const metadata = response.data;

try {
validateObjectWithSchema(CollectionMetadataSchema, metadata, validationMetadataErrorOptions);
} catch (error) {
if (error instanceof ValidationError) {
errors.push(...error.errors);
} else {
errors.push(errorToMessage(error));
}
}
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(`${dictionary.errors.failedToFetchMetadata}: ${error.message}`);
} else {
errors.push(dictionary.errors.unhandledError);
}
}

return { errors, isValid: errors.length === 0 };
};
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export type {
MetadataOnChainObjects,
} from './types/hip412-validator';
export { PrivateKey } from '@hashgraph/sdk';

export { HederaNFTSDK } from './nftSDKFunctions';
export { FeeFactory } from './feeFactory';
export { TokenMetadataValidator } from './token-metadata-validator';
Expand Down
11 changes: 9 additions & 2 deletions src/nftSDKFunctions/create-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const createCollectionFunction = async ({
autoRenewAccountPrivateKey,
autoRenewPeriod,
memo,
metadata,
}: CreateCollectionType): Promise<string> => {
validatePropsForCreateCollection({
collectionName,
Expand All @@ -49,6 +50,7 @@ export const createCollectionFunction = async ({
autoRenewAccountPrivateKey,
autoRenewPeriod,
memo,
metadata,
});

const treasuryAccountId = treasuryAccount ? treasuryAccount : client.getOperator()!.accountId;
Expand Down Expand Up @@ -85,6 +87,10 @@ export const createCollectionFunction = async ({
transaction = transaction.setPauseKey(keys?.pause);
}

if (keys?.metadataKey) {
transaction = transaction.setMetadataKey(keys?.metadataKey);
}

if (maxSupply) {
transaction = transaction.setSupplyType(TokenSupplyType.Finite);
transaction = transaction.setMaxSupply(maxSupply);
Expand All @@ -110,8 +116,9 @@ export const createCollectionFunction = async ({
transaction = transaction.setTokenMemo(memo);
}

if (keys?.metadataKey) {
transaction = transaction.setMetadataKey(keys?.metadataKey);
if (metadata) {
const encodedMetadata = new TextEncoder().encode(metadata);
transaction = transaction.setMetadata(encodedMetadata);
}

transaction = transaction.freezeWith(client);
Expand Down
3 changes: 3 additions & 0 deletions src/nftSDKFunctions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export class HederaNFTSDK {
autoRenewAccountPrivateKey,
autoRenewPeriod,
memo,
metadata,
}: {
collectionName: string;
collectionSymbol: string;
Expand All @@ -97,6 +98,7 @@ export class HederaNFTSDK {
autoRenewAccountPrivateKey?: PrivateKey;
autoRenewPeriod?: number;
memo?: string;
metadata?: string;
}) {
return createCollectionFunction({
client: this.client,
Expand All @@ -113,6 +115,7 @@ export class HederaNFTSDK {
autoRenewAccountPrivateKey,
autoRenewPeriod,
memo,
metadata,
});
}

Expand Down
19 changes: 19 additions & 0 deletions src/test/e2e/calculate-rarity-from-on-chain-data-e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*-
*
* Hedera NFT SDK
*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { LONG_E2E_TIMEOUT, MIRROR_NODE_DELAY } from '../__mocks__/consts';
import { calculateRarityFromOnChainData } from '../../rarity';
import { LINK_TO_JSON_OBJECT_WITHOUT_ERRORS, nftSDK, operatorPrivateKey } from './e2e-consts';
Expand Down
17 changes: 17 additions & 0 deletions src/test/e2e/create-collection/create-collection-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,21 @@ describe('createCollectionFunction e2e', () => {
},
LONG_E2E_TIMEOUT
);

it(
'creates a collection with collecton metadata URL provided',
async () => {
const tokenId = await nftSDK.createCollection({
collectionName: 'token_with_collection_metadata',
collectionSymbol: 'TWCM',
treasuryAccountPrivateKey: secondPrivateKey,
treasuryAccount: secondAccountId,
metadata: 'www.metadata-url-example.com',
});

const tokenInfo = await getTokenInfo(tokenId, nftSDK.client);
expect(tokenInfo.metadata).toBeDefined();
},
LONG_E2E_TIMEOUT
);
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*-
*
* Hedera NFT SDK
*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { TokenMetadataValidator } from '../../../token-metadata-validator';
import {
nftSDK,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*-
*
* Hedera NFT SDK
*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { TokenMetadataValidator } from '../../../token-metadata-validator';
import {
nftSDK,
Expand Down
Loading

0 comments on commit f9ee518

Please sign in to comment.