Create a Collection

Overview

The package is designed to help projects or developers create, manage, and deploy collections. It also facilitates the creation of a marketplace, interaction with xNFTs, and payment for DePIN resources, amongst others.

This documentation provides the required application programming interface (API) calls to create your collection of xNFTs. It is divided into two sections:

  1. The types and explanation of types used in a collection data.

  2. Guides for creating collections for static and custom applications.


Prerequisites

You will need to install & set up Skynet SDK. If not done, please refer to Skynet.

Types

Below is an interface that contains all the data structures necessary for your collection:

export interface NetworkSettings {
 protocol: string;
 containerPort: number;
 accessPort: number;
 addHostUrl: boolean;
 hostUrl: string;
}

The interface NetworkSettings is designed to configure your network settings, which you will call later within the interface collectionData. The properties within NetworkSettings are:

  • protocol: It holds the value of your network protocol and can be HTTPS, TCP, etc.

  • containerPort: This represents your network port number.

  • accessPort: The value representing your port number through which external clients can access your services if hosted inside a container.

  • addHostUrl: Indicates if the host URL should be added to the network configuration.

  • hostURL: The base URL hosting your service.


export interface AttribVariableParam {
   name: string;
   condition: string;
   conditionDescription?: string;
   defaultValue?: string;
}

This interface handles the creation of the attribute variable parameter within the collection you create. AttribVariableParam gets called with collectionData interface.


collectionData Interface
export interface collectionData { 
id: string; 
address: string, 
collectionName: string; 
collectionDescription: string; 
collectionSize: number; 
encrypt: boolean; 
license: boolean; 
licenseFee: string; 
royalty: string; 
category: string; 
percentageMonth: string; 
storageType: string; 
applicationType: null | string; 
tags: string; 
imageName: string; 
imageTag: string; 
privateImageRegistry: string; 
privateImageUsername: string; 
privateImagePassword: string; 
softwareLock: boolean; 
noDeployment: boolean; 
privateImage: boolean; 
status: boolean; 
mintStatus: string; 
mintCost: string; 
collectionCost: string; 
netWorkSetting?: NetworkSettings[]; 
environmentVariables?: string; 
stateFullSets?: boolean; 
args?: string; 
attributeVariableParam: AttribVariableParam; 
cpuRange: string; 
bandwidthRange: string; 
storageRange: string; 
nftMintedList?: number[]; 
commands?: string; 
instanceType: 'cpuStandard' | 'cpuIntensive' | 'gpuStandard'; dataPersistence?: boolean;
mountVolume?: string; 
subnetType?: number; 
subnetCount?: number; 
customAppStorageType?: string; 
collectionImage: null | File; 
nftImage: null | File; 
nftId: null | string; 
limitedEdition: boolean; 
staticFile: File[]; 
bundle: any; 
marketplaceList: boolean; 
publicMint: boolean; 
verified: boolean; 
paymentMade: boolean; 
} 

The collectionData interface defines the structure of a collection within the package. Below is an explanation of all the parameters on the interface:

Parameters
  • id: It is an auto-generated ID for your collection and can be left blank.

  • address: is the collection creator’s wallet address.

  • collectionName: The name you give your collection, which will be used as a metadata name for your NFT.

  • collectionDescription: Description of the collection you are creating which will be used for collection description in the marketplace.

  • collectionSize: The maximum number of NFTs that can be minted in the collection.

  • encrypt: This is used for static files.

  • license: This is the wallet address you use to receive fees whenever your NFT gets resold in the marketplace.

  • licenseFee: Fees for the license.

  • royalty: Royalty can be referred from any NFT collection.

  • category: Category of your collection, used in the marketplace to sort collections based on filters.

  • percentageMonth: Monthly percentage rate.

  • storageType: This could be files, videos, or folders, each with having its own encryption and launcher.

  • applicationType: Custom or static application.

  • tags: Used for search in the marketplace.

  • imageName: Name of the docker image.

  • imageTag: Tag for the docker image.

  • privateImageRegistry: Used for custom applications if your docker image is private.

  • privateImageUsername: Username for your private image registry.

  • privateImagePassword: Password for your private image registry.

  • softwareLock: Indicates if the software’s version or name cannot be updated.

  • noDeployment: Indicates if deployment is disabled.

  • privateImage: Indicates if the image is private.

  • status: This is updated by workers and shows static application creation status.

  • mintStatus: Minting status of your collection.

  • mintCost: This can be updated based on the price the collection owners want to collect per mint. However, mint costs $1 by default.

  • collectionCost: The cost it takes to create a static and custom application; for a static application, this is based on the static file size, and $1 for a custom application.

  • netWorkSetting: Used for custom applications.

  • environmentVariables: This is used for custom applications.

  • stateFullSets: Indicates if the collection has stateful sets; this is optional.

  • args: Additional arguments; this is optional.

  • attributeVariableParam: Attribute variable parameters.

  • cpuRange: CPU range required.

  • bandwidthRange: Bandwidth range needed for your collection.

  • storageRange: Storage range required for your application.

  • nftMintedList: List of minted NFTs; this is optional.

  • commands: Commands associated with the collection; this is optional.

  • instanceType: Type of instance (cpuStandard, cpuIntensive, gpuStandard).

  • dataPersistence: Indicates if data persistence is enabled; this is optional.

  • mountVolume: Volume mount; this is optional.

  • subnetType: Type of subnet; this is optional.

  • subnetCount: Number of subnets; this is optional.

  • customAppStorageType: Custom application storage type; this is optional.

  • collectionImage: Image file for the collection or null.

  • nftImage: Image file for the NFT or null.

  • nftId: ID of the NFT or null.

  • limitedEdition: Indicates if your collection is a limited edition.

  • staticFile: Static files associated with your collection.

  • bundle: Bundle data.

  • marketplaceList: Indicates if your collection is listed on the marketplace.

  • publicMint: Indicates if your collection is publicly mintable.

  • verified: Indicates if your collection is verified.

  • paymentMade: Indicates if payment has been made.


Upload Static APP

Encrypt & Upload Files to IPFS for a Static Application

You can also create a collection for a static application by calling the package API. Follow these steps to create a collection for your static application.

Step 1: Upload your static file to AWS S3

To upload your file to AWS S3, you need to get an access key and a session key by calling this API:

const getCredentials = async (skynet: SkyMainBrowser, address: string) => {
  const signature = await skynet.appManager.getUrsulaAuth();

  if (signature.success) {
    try {
      const response = await fetch(
        `${process.env.NEXT_PUBLIC_STUDIO_SERVICE_URL!}/getAwsCredentials`,
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            userAuthPayload: signature.data,
            createdAt: Date.now(),
          }),
        }
      );

      if (response.ok) {
        const responseData = await response.json();
        return {
          success: true,
          data: responseData,
        };
      } else {
        return {
          success: false,
          data: Error("Unexpected error occurred"),
        };
      }
    } catch (error) {
      return {
        success: false,
        data: Error("Unexpected error occurred"),
      };
    }
  } else {
    return {
      success: false,
      data: Error("Failed to get signature"),
    };
  }
};

Here is what the code does:

  • It authenticates you and receives a signature, and if it succeeds, it sends a POST request to the specified endpoint, /getAwsCredentials.

  • After the request succeeds or passes, you get a temporary credential.


After getting your access and session key, upload the file to AWS S3:

const uploadFileToS3 = async (
  files: File[],
  bucketName: string,
  skynet: SkyMainBrowser,
  onProgressCallback?: ProgressCallback
): Promise<UploadResult> => {
  try {
    const filesArray: string[] = [];
    const credentials = await getCredentials(skynet);
    if (!credentials.success) {
      return {
        success: false,
        message: credentials.data
      }
    }

    const s3Client = new S3Client({
      region: "us-east-1",
      apiVersion: "2006-03-01",
      credentials: {
        accessKeyId: credentials.data.Credentials.AccessKeyId,
        secretAccessKey: credentials.data.Credentials.SecretAccessKey,
        sessionToken: credentials.data.Credentials.SessionToken,
        expiration: credentials.data.Credentials.Expiration
      }
    });

    for (const file of files) {
      const fileName = Date.now().toString() + file.name;
      const fileSize = file.size;
      const totalParts = Math.ceil(fileSize / PART_SIZE);
      
      const createParams = {
        Bucket: bucketName,
        Key: fileName,
      };
      
      const uploadIdResponse = await createMultipartUpload(s3Client, new CreateMultipartUploadCommand(createParams));

      const uploadId = uploadIdResponse.UploadId;

      const uploadPromises: Promise<any>[] = [];

      for (let partNumber = 0; partNumber < totalParts; partNumber++) {
        const startByte = partNumber * PART_SIZE;
        const endByte = Math.min(startByte + PART_SIZE, fileSize);
        const partParams = {
          Bucket: bucketName,
          Key: fileName,
          PartNumber: partNumber + 1,
          UploadId: uploadId,
          Body: file.slice(startByte, endByte),
        };
        uploadPromises.push(uploadPart(s3Client, new UploadPartCommand(partParams)));
      }

      const responses: any = await allProgress(uploadPromises,
        (p: any) => {
          if (onProgressCallback) {
            onProgressCallback(`${p.toFixed(2)}`);
          }
        });

      const parts = responses.map((response: any, index: number) => ({
        PartNumber: index + 1,
        ETag: response.ETag,
      }));
     
      const completeParams = {
        Bucket: bucketName,
        Key: fileName,
        UploadId: uploadId,
        MultipartUpload: {
          Parts: parts,
        },
      };
      
      await completeMultipartUpload(s3Client, new CompleteMultipartUploadCommand(completeParams));
      filesArray.push(fileName);
    }

    return {
      success: true,
      message: "Files uploaded successfully",
      filesArray
    };
  } catch (error: any) {
    return {
      success: false,
      message: error.message || "Failed to upload files"
    };
  }
};

 const onProgress: ProgressCallback = (fileProgress) => {
          console.log(parseFloat(fileProgress));
          };
        
 const response = await uploadFileToS3(
          staticFile, // pass the static file uploaded by the user
          "fx-temp-store",
          skynet,
          onProgress,
        );
 if(!respose.success){
 return;
 }

// add aws file key in collection form
 formData.staticFile = response.filesArray

Here are util functions for uploading files to AWS S3:

async function createMultipartUpload(s3Client: S3Client, uploadParams: CreateMultipartUploadCommand) {
  return s3Client.send(uploadParams);
}

async function completeMultipartUpload(s3Client: S3Client, uploadParams: CompleteMultipartUploadCommand) {
  return s3Client.send(uploadParams);
}

With this, you can upload the files using the credentials you get in return; every time you upload, you need to call this API to get the keys, as the keys have an expiration period. So remember you have to call this API anytime you want to upload a file to refresh the credentials.

You need to save the file key in staticFile in collectionData, which will be processed. After you upload your file and have made the payment to create a custom application, the file gets downloaded and encrypted. After it is encrypted, it gets uploaded to IPFS and pinned to IPNS; this IPNS link will be stored in the contract when a new SkyID is minted from your collection.

You can upload any file to AWS S3. For example, if it is a video you wish to upload, indicate so as the key of storageType, else it should be files.

Step 2: Update the staticFile Key

After updating the staticFile key, you should upload the collection to the API call made in the previous section.

Unlike custom applications, the cost of creating a collection for the static application is based on file size and not fixed.

How to Create a Collection for a Custom Application

Creating a collection for a custom application is one of the use cases for which you can use the package; this guide outlines the API calls to create a collection for a custom application.

The following code shows how to use the Skynet SDK to get a signature and send a POST request to create a collection:

const signature = await skyBrowser.appManager.getUrsulaAuth(); 
if(!signature.success){
   // show error.
   return;
}
const response = await fetch(
  `${process.env.NEXT_PUBLIC_STUDIO_SERVICE_URL!}/createCollection`, 
  {
    method: "POST", 
    headers: { 
      "Content-Type": "application/json", 
    }, 
    body: JSON.stringify({ 
      ...formData, 
      collectionImage: collectionBannerUpload ? collectionBannerUpload.data : null, 
      nftImage: nftImageUpload ? nftImageUpload.data : null, 
      userAuthPayload: signature.data, 
      createdAt: Date.now(), 
    }), 
  }
);

In the above code:

  • CollectionImage is a URL of the collection image. (optional)

  • NftImage is a URL that is used in the metadata of the SkyID and as a SkyID Image. (required)

  • FormData is the data required to create a collection. Refer to the type here.

  • The first line runs an authentication using the appManager object, and the function getUrsulaAuth validates the address stored in formData.

  • The second line sends a POST request to the link specified by process.env.NEXT_PUBLIC_STUDIO_SERVICE_URL, for example, https://fxservice-n1063-marvel.stackos.io, is concatenated by /createCollection and sends it to a specified endpoint for creating a collection.

  • The request body, a JSON string, contains several properties from the collectionData interface explained earlier that contain the data of the collection you are creating.

Implementing this API call will create a collection for a custom application.

You must pay a fixed cost of $1 to create a collection for a custom application.


To calculate what it will cost you, use:

const totalCost = 1 + (value[0].size * 
parseFloat(process.env.NEXT_PUBLIC_STORAGE_COST || "0.0000000037"));

Where the cost per byte of file size is 0.0000000037 and where value[0].size refers to the size of the file getting stored.

Make Payment

A collection requires a minimum fee of 1$ to encrypt the static file and then upload the file to IPFS.

export const makePayment = async (skynet: SkyMainBrowser, collectionId: string, price: number = 1) => {
    try {
        const etherContractService: any = skynet.contractService;
        const balance = await etherContractService.signer.getBalance();
        const userBalance = ethers.utils.formatEther(balance);
        
        if (parseFloat(userBalance) < price) {
            return {
                success: false,
                data: Error("Insufficient Balance")
            }
        }
        
        const payvalue = ethers.utils.parseEther(price)
        
        const patmentMade = await skynet.contractService.callContractWrite(skynet.contractService.OrderPayment.createCollection(
            stringToHex(collectionId),
            1,
            1, // mint price
            {
                value: payvalue
            }
        ))
        

        if (patmentMade?.success) {
            return {
                success: true,
                data: collectionId
            }
        } else {
            return patmentMade
        }
    } catch (e) {
        return {
            success: false,
            data: e
        }
    }
}

const totalCost = 1 + (staticFile.size * parseFloat(process.env.NEXT_PUBLIC_STORAGE_COST || "0.0000000037"));

await makePayment(skynet, collectionId, parseFloat(totalCost));

The above code calculates the cost and also makes the payment for uploading the file. After the payment is made, the collection will become active, and then you can mint the SkyIDs from the collection created.

Last updated