development-cycle

In the previous blogpost we saw how one can write a smart contract for the Aeternity blockchain from scratch. Of course, smart contracts would not be so useful if it weren’t easy to deploy them to the blockchain. The Aeternity team has built the Forgae framework to make developers' lives easier. Forgae has a command line interface to setup new Aeternity projects, compile contracts, run unit tests, deploy compiled contracts, and start a local Aeternity node for development. In this post, we will go through the basic usage of Forgae to complete our development cycle for the Certification story.

Install and Setup

First, we need to install Node.js and its build tool npm. On macOS, we can use brew to install Node.js and npm to install Forgae:

brew install node
npm i -g forgae

Secondly, we need to install Docker, since Forgae is using the Docker setup to spin up a local node. Installation instructions can be found on the Docker website.

Now, we need to create a project folder and run the initialization command:

mkdir ae-certification
cd ae-certification
forgae init

This should print a result message, which may look like this:

===== ForgAE was successfully initialized! =====

The result of this command is a number of folders and files to quickly get started with Aeternity app development:

├── contracts
├── deployment
├── docker
├── docker-compose.yml
├── node_modules
├── package-lock.json
├── package.json
└── test

As we can see, Forgae creates a Node.js project with:

It also generates a demo script and a smart contract, but we will remove them and create new ones for our Certification project. Clean the contracts folder and create two contracts based on the source code of the previous post. The contracts folder should now look like this:

contracts
├── ExamVerifier.aes
└── Examinee.aes

The initial setup is done!

Compiling Contracts

Compilation is done through the Aeternity node. That means we need to start a local node first:

forgae node

Once the Aeternity node is successfully started, we can compile our two contracts:

forgae compile

It should print the resulting (long) byte code. Make sure there are no errors in the terminal output.

Writing Tests in Mocha

mocha-js-logo

Our smart contracts have some business logic which we need to test. We will write integration tests to check how both contracts are working with each other after being deployed to the local Aeternity node. Forgae is using the Mocha JavaScript framework to run the tests.

First, clean the test folder and create a new file with the name examineeTest.js and the code below:

const Ae = require('@aeternity/aepp-sdk').Universal;
const Crypto = require('@aeternity/aepp-sdk').Crypto;
const {Universal} = require('@aeternity/aepp-sdk');
const Util = require('../scripts/util');

const minGrade = 1;
const maxGrade = 5;
const minGradeForFoundation = 2;
const oracleFee = 0;

describe('Examinee Contract', () => {
  let client;
  let examineeContract;
  let examVerifierContract;

  before(async () => {
    client = await Ae({
      url: "http://localhost:3001/",
      internalUrl: "http://localhost:3001/internal/",
      keypair: {
        publicKey: wallets[0].publicKey,
        secretKey: wallets[0].secretKey
      },
      nativeMode: true,
      networkId: "ae_devnet"
    });

    examVerifierContract = await Util.deployContract(client, `(${oracleFee})`,
        './contracts/ExamVerifier.aes', oracleFee);
  });

  async function deployExamineeContract(client, examVerifier,
    maxPtsPerTraining = 10, minPtsForAdvanced = 70) {

    const examVerifierAddress = Util.keyToAddress(examVerifier.address);
    const identHash = Crypto.sha256hash("alexander");
    const args = `("${identHash}", ${examVerifierAddress}, ${maxPtsPerTraining},
        ${minPtsForAdvanced}, ${minGradeForFoundation}, ${oracleFee})`;

    return Util.deployContract(client, args, './contracts/Examinee.aes', oracleFee);
  }

  // see second part further
})

The Aeternity team provides a JavaScript SDK which is used in the tests as well. At first, we import several SDK modules and our own utility script for contract deployment. Then, we start with a standard clause of Mocha to define a test. The first function before prepares the state of this test: an Aeternity client to communicate with the node and an instance of the ExamVerifier contract. Finally, we define a function to deploy an Examinee contract. As we can see, this test compiles and deploys contracts on its own and later calls methods and checks contract properties.

The main part of the test looks as follows:

// second part 
  it('may request the homework if min amount of points is collected 
    and foundationLevel is completed',
    async () => {
    //given
    const points = 10;
    const trainingDate = 1551433652;
    const trainingProvider = Util.keyToAddress(wallets[1].publicKey);
    const t1 = `("Blockchain training", ${trainingDate}, 
        ${trainingProvider}, "Nakomoto", ${points})`;
    const contract = await deployExamineeContract(client, 
        examVerifierContract, points, points);

    //when/then
    await addTrainingAndAssert(t1, points, contract);

    //when/then
    await canRequestHomeworkAndAssert(contract, false);

    //given
    const examPoints = 80;
    const params = `(${trainingDate}, ${examPoints})`;
    //when
    await contract.call('setFoundationLevel',
            { args: params, value: oracleFee })
            .catch(e => Util.logAndThrowError(client, e));
    //when/then
    const grade = await gradeExamAndAssert(contract, params);

    //when
    const retrieveExamGrade = await contract.call('retrieveExamGrade')
        .then(g => g.decode('option(int)'));
    //then
    assert.equal(retrieveExamGrade.value[1].value, grade);

    //when/then
    const canRequestHomework = grade <= minGradeForFoundation;
    await canRequestHomeworkAndAssert(contract, canRequestHomework);
  });

  async function addTrainingAndAssert(training, expectedPoints, contract) {
    //when
    const addTraining = await contract.call('addTraining', { args: training })
        .catch(e => Util.logAndThrowError(client, e));

    //then
    const totalPoints = await addTraining.decode('int');
    assert.equal(totalPoints.value, expectedPoints);
  };

  async function canRequestHomeworkAndAssert(contract, expected) {
    //when
    let canRequestHomework = await contract.call('canRequestHomework');
    //then
    let canRequestHomeworkRes = await canRequestHomework.decode('bool');
    assert.equal(canRequestHomeworkRes.value, expected);
  };

  async function gradeExamAndAssert(contract, params) {
    //when
    const contractAddress = Util.keyToAddress(contract.address);
    const gradeExam = await examVerifierContract.call('gradeExam',
        { args: `(${contractAddress})`})
        .then(g => g.decode('option(int)'));

    // below line of code is quite bad. It expects an array and value in its index 1. 
    // I could not find better way to access an option value
    const grade = gradeExam.value[1].value;
    //then
    assert.isAtLeast(grade, minGrade, "Too small grade returned");
    assert.isAtMost(grade, maxGrade, "Too big grade returned");
    return grade;
  };
})

In this test, we perform several calls to simulate a user scenario. To summarize the steps:

  1. Deploy Examinee contract
  2. Add training and assert points
  3. Assert that examinee cannot access the homework module yet
  4. Set Foundation Level exam for examinee
  5. Trigger ExamVerifier contract to calculate exam grade and store it in the blockchain
  6. Retrieve exam grade from the blockchain and store it in the Examinee contract
  7. Assert that the examinee can request the homework module now

There are several important points about Aeternity tests and its JavaScript SDK:

  1. There is a global array of 10 wallets which can be used to simulate different user addresses in the network.
  2. Communication is based on the JavaScript Promise API and is thus fully asynchronous. await can be used to observe results.
  3. Returned values are encoded, and thus need to be decoded using decode in order to compare them with test variables. The same applies to error message. They need to be decoded into a human-readable form.
  4. If we need to pass a contract address as a parameter to another contract, we will need to properly format it. An example can be found in the Util.keyToAddress function.

Although our integration tests are not exhaustive, they cover the main use case. Real production applications would require more tests and perhaps separate tests for each contract.

Running Tests

Once the tests are prepared, we can run them using the command below:

forgae test

It should print the standard Mocha messages saying whether the tests are passing.

===== Starting Tests =====

  Examinee Contract
    ✓ may request the homework if min amount of points is 
        collected and foundationLevel is completed (24083ms)

  1 passing (26s)

One important thing to mention is that the JavaScript SDK will sometimes give misleading error messages. Occasionally, it may not be easy to find an error in the test code or even an error in the contract code. One can use debugging techniques, like commenting out parts of the code, to understand which line causes an error.

Deployment Scripts

Forgae provides deployment scripts to easily publish smart contracts to a specific Aeternity network. Clean the deployment folder and create a new file deploy.js with the following content:

const Ae = require('@aeternity/aepp-sdk').Universal;
const Crypto = require('@aeternity/aepp-sdk').Crypto;
const Deployer = require('forgae').Deployer;
const Util = require('../scripts/util');

const gasLimit = 200000;
const oracleFee = 5;

const deploy = async (network, privateKey) => {
  const deployer = new Deployer(network, privateKey)

  const maxPtsPerTraining = 10;
  const minPtsForAdvanced = 70;
  const minGradeForFoundation = 2;
  const examVerifierAddress = await deployExamVerifier(deployer);
  const opts = { ttl: 100, amount: oracleFee};
  const identHash = Crypto.sha256hash("alexander");

  const args = `("${identHash}", ${examVerifierAddress}, ${maxPtsPerTraining},
    ${minPtsForAdvanced}, ${minGradeForFoundation}, ${oracleFee})`;

  const c = await deployer.deploy("./contracts/Examinee.aes", gasLimit, args, opts);
  console.log("Examinee address: " + c.address + ", network: " + network);
};

async function deployExamVerifier(deployer) {
  const examVerifier = await deployer.deploy(
    './contracts/ExamVerifier.aes', gasLimit, `(${oracleFee})`);
  return Util.decodeContractAddress(examVerifier);
}

module.exports = {
  deploy
};

The above code looks similar to the test code which we have written earlier. First, we import a couple of Aeternity SDK and Forgae modules, as well as our own script for contract deployment. The main deployment instructions are in the deploy function. Basically, we create an instance of Deployer and deploy both contracts with it. Finally, we print the Examinee contract address and the name of the network to which we deployed.

Note that the deployment script takes the network as a parameter as well as the private key of the account. The latter is required to sign the contract creation transactions and is accountable for deployment expenses. The network name and the key can be passed as options to the deploy command:

forgae deploy -n https://sdk-mainnet.aepps.com  -s <secretKey>

We have finished our development cycle for the Certification story including smart contracts implementation, integration testing and deployment. Now we can stop the local node.

forgae node --stop

Summary

The Forgae framework covers the full cycle of the development flow. It is very helpful to have such a tool from the very beginning of an application development for any blockchain network. In case you know the Truffle framework from the Ethereum community, you may find many similarities in how Forgae works. This allows us to be quite productive in application development for Aeternity. For more information on different Forgae options and examples see the Aeternity tutorials.

There is one more part, which we have skipped so far: the UI for our Certification story. We briefly saw that Aeternity provides a JavaScript SDK. We already used it in some scripts. In general, the JavaScript SDK allows us to build Aeternity web applications so that an application can communicate directly from a web browser with the Aeternity node JSON API. We will cover that part in another post.

TAGS