
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:
- a
contracts
folder containing smart contracts in Sophia - a Docker Compose file to start a local Aeternity node
- a
test
folder containing tests in JavaScript - a
deployment
folder containing deployment scripts in JavaScript
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

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:
- Deploy Examinee contract
- Add training and assert points
- Assert that examinee cannot access the homework module yet
- Set Foundation Level exam for examinee
- Trigger ExamVerifier contract to calculate exam grade and store it in the blockchain
- Retrieve exam grade from the blockchain and store it in the Examinee contract
- Assert that the examinee can request the homework module now
There are several important points about Aeternity tests and its JavaScript SDK:
- There is a global array of 10 wallets which can be used to simulate different user addresses in the network.
- Communication is based on the JavaScript
Promise
API and is thus fully asynchronous.await
can be used to observe results. - 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. - 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.