Scripts
Create a Project

Creating A Project

This section details how to create a project using the ers-scripts repo.

Set Up

If you are trying to execute a transaction it is assumed that you have also read the Set Up section. If you are just looking for an example read on

To be able to create a project you first need to specify the Ethereum account you want to call addProject, note that this address will be set as the "owner" of the project's ProjectRegistrar however that can be changed post-creation. To do this, you must input the private key of the account you wish to use into the .env file. Additionally, you need to specify the projetPublicKey for the project, this public key is used to sign all tsmCertificates. The .env file should look something like this:

...
GOERLI_PROJECT_OWNER_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
GOERLI_PROJECT_PUBLIC_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
...
MAINNET_PROJECT_OWNER_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
MAINNET_PROJECT_PUBLIC_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Replace the stand-in private keys (pulled from Hardhat) with the private key you wish to use. Make sure that you are using the correct private key for the network you are deploying to.

Defining Script Inputs

The inputs to the project creation script are more complex than simply inputting on a command line can allow. To get around this we use a JSON file to define the inputs to the script. The params file is located at task_params/projectCreation.json and it should look something like this:

{
    "name": "",
    "chipDataLocation": "",
    "manufacturerValidationLocation": "task_outputs/addManufacturerEnrollment/",
    "tokenUriRoot": "ipfs://tokenUriRoot/",
    "lockinPeriod": 0,
    "serviceId": ""
}

If the file does not exist yet you can create it by calling: cp task_params/projectCreation.default.json task_params/projectCreation.json

To fields in the file are defined as such:

  • name: ERS name for the project
  • chipDataLocation: This should be the location of a local file that has an array containing the chip data for the project, see below for the definition of the chipData object.
  • manufacturerValidationLocation: Either local location of manufacturer validation file or an IPFS folder containing the ManufacturerValidation information for chips in the project
  • tokenUriRoot: IPFS URI root for the token URIs of the chips in the project
  • lockinPeriod: Timestamp after which user can change the primary service associated with chip (see Understanding Services for more information on services)
  • serviceId: ID of the service being initially tied to the chips in the project

In order to create a valid chipData array it must adhere to the ChipInfo definition:

{
    "chipId": "",
    "pk2": "",
    "enrollmentId": ""
}

This data is either fetchable from the Manufacturer Enrollment data posted to IPFS as part of the manufacturer enrollment process or can be created locally by scanning the chips and associating their chipId with the enrollmentId of that chip.

Executing the Script

The script to create a project is located in tasks/projectCreation.ts. To execute the script run:

yarn createProject --network [network] --post [true/false]

Where:

  • post indicate whether to post the resulting project files to IPFS. These files are used to help users claim the chip, it's recommended to have this set to true for any project that will be used by the public. Note that you must have an NFT_STORAGE_API_KEY set (see Setup)

When the script is run the first step is using lib-ers to create the project's merkle tree:

    const ersInstance: ERS = await createERSInstance(hre);
 
    // Create merkle tree
    const chipInfo: ChipInfo[] = JSON.parse(fs.readFileSync(params.chipDataLocation, 'utf-8'));
    await ersInstance.projectCreation.createTSMMerkleTree(
      chipInfo,
      params.tokenUriRoot,
      params.lockinPeriod,
      params.serviceId
    );

Next we use the projectPublicKey to create tsmCertificates and custodyProofs for each chip. This requires scanning each chip to sign the custodyProofs:

    async function createCertificates(): Promise<any> {
      const certSigner = await hre.ethers.getSigner(projectPublicKey);
      const tsmCertificates: string[] = [];
      for (let i = 0; i < chipInfo.length; i++) {
        const packedCert = ethers.utils.solidityPack(["address"], [chipInfo[i].chipId]);
        tsmCertificates.push(await certSigner.signMessage(ethers.utils.arrayify(packedCert)));
      }
 
      const custodyProofs: string[] = new Array<string>(chipInfo.length);
      const message = ethers.utils.solidityPack(["address"], [projectPublicKey]);     
      for (let i = 0; i < chipInfo.length; i++) {
        const sig = await getChipSigWithGateway(gate, message);
        const index = chipInfo.findIndex(item => item.chipId == sig.etherAddress);
        
        if (index == -1) {
          console.log(`Could not find chipId ${sig.etherAddress} in chipInfo`);
          i -= 1;
          continue
        };
 
        if (custodyProofs[index]) {
          console.log(`ChipId ${chipInfo[i].chipId} already has an ownership proof`);
          i -= 1;
          continue
        }
 
        console.log(`Ownership proof created for chipId: ${chipInfo[i].chipId}`)
        custodyProofs[index] = sig.signature.ether;
      }
      return { tsmCertificates, custodyProofs };
    }

Next we need to pre-calculate the projectRegistrarAddress since this is required to create the project enrollment files posted to IPFS. This is doable because we use CREATE2 in the contracts, and can be done using lib-ers:

    ersInstance.projectCreation.calculateProjectRegistrarAddress(projectOwner, ersInstance.projectCreation.tsmTree.getHexRoot())

Now we have everything we need to create and post the project enrollment files to IPFS:

    function _generateProjectEnrollmentFiles(
      ers: ERS,
      certificates: string[],
      custodyProof: string[]
    ): File[] {
      let projectEnrollmentFiles: File[] = [];
      for (let i = 0; i < ers.projectCreation.treeData.length; i++) {
        const chipData = ers.projectCreation.treeData[i];
        let chipValidationInfo: TSMMerkleProofInfo = {
          tsmIndex: BigNumber.from(i),
          serviceId: chipData.primaryServiceId,
          lockinPeriod: chipData.lockinPeriod,
          tokenUri: chipData.tokenUri,
          tsmProof: ers.projectCreation.tsmTree.getProof(i, chipData),
        };
  
        let projectEnrollment: ProjectEnrollmentIPFS = {
          enrollmentId: chipData.enrollmentId,
          projectRegistrar: projectRegistrarAddress,
          TSMMerkleInfo: chipValidationInfo,
          tsmCertificate: certificates[i],
          custodyProof: custodyProof[i]
        };
        projectEnrollmentFiles.push(new File([JSON.stringify(projectEnrollment)], `${chipData.chipId}.json`.toLowerCase(), { type: 'application/json' }));
      }
    
      return projectEnrollmentFiles;
    }

Finally, we need three last pieces of data to create the project:

  1. Chip ownership proof - a proving chip is selected and used to sign the chipOwnershipProof
    const chainId = BigNumber.from(await hre.getChainId());
    const packedMsg = ethers.utils.solidityPack(["uint256", "address"], [chainId, projectOwner]);
    const response = await getChipSigWithGateway(gate, packedMsg);
 
    const chipOwnershipProof = response.signature.ether;
  1. Project ownership proof - signature of the projectRegistrarAddress by the projectPublicKey
    async function createProjectOwnershipMessage(projectRegistrarAddress: Address): Promise<string> {
      const signer = hre.ethers.provider.getSigner(projectPublicKey);
      const chainId = BigNumber.from(await signer.getChainId());
 
      const packedMsg = ethers.utils.solidityPack(["uint256", "address"], [chainId, projectRegistrarAddress]);
 
      return signer.signMessage(ethers.utils.arrayify(packedMsg));
    }
  1. Proving chip manufacturer validation is grabbed from the manufacturerValidationLocation
    async function getProvingChipManufacturerValidation(chip: Address): Promise<ManufacturerEnrollmentIPFS> {
      if (params.manufacturerValidationLocation.slice(0, 5) == 'https') {
        return (await axios.get(params.manufacturerValidationLocation + `${provingChip.toLowerCase()}.json`)).data;
      } else {
        return JSON.parse(
          fs.readFileSync(params.manufacturerValidationLocation + `${provingChip}.json`, 'utf-8')
        );
      }
    }

Finally we can put all these pieces together and submit the addProject transaction:

      await rawTx({
        from: projectOwner,
        to: enrollmentManagerAddress,
        data: enrollmentManager.interface.encodeFunctionData(
          "addProject",
          [
            projectOwner,
            chipValidationDataUri,                              // URI of the project enrollment files
            calculateLabelHash(params.name),                    // Calculated label hash using defined name
            ersInstance.projectCreation.tsmTree.getHexRoot(),
            projectPublicKey,
            provingChip,
            chipClaimInfo.TSMMerkleInfo,                        // TSMMerkleInfo of the proving chip
            provingChipManufacturerInfo.validationInfo,
            chipOwnershipProof,
            projectOwnershipProof
          ]
        )
      });