Getting Started with Lamington

Let's launch with Lamington!

Goal

At the end of this guide, you will have:

  • A simple smart contract with one action, only callable by one account
  • A JavaScript unit test suite which tests the business logic around who is able to call the action
  • Optional: Instructions on how to use TypeScript for the unit testing suite we previously wrote in JavaScript
  • An understanding of what exactly Lamington is doing for you, and how you can leverage it to its fullest

Prerequisites

Follow the instructions on the Install Lamington page to install Lamington globally. Once you've done that, you're good to get started!

We'll start from an empty folder.

Step 1: Add a package.json file

We need to have a package.json file so we can manage our NPM dependencies for the project. Let's create a placeholder package.json file which can be edited later if needed.

$ npm init -y

This command is a standard NPM command (which is documented here if you want more information about it).

Step 2: Initialise Lamington's Configuration

Ok, now time to get Lamington ready to go:

$ lamington init
$ npm i

You'll see that the init command created a folder structure for you, and added lamington to the devDependencies of the project, which is why we then run npm i, so that Lamington will be around to be called from your tests later.

Step 3: Add a simple contract to the contracts folder

Now we can create our smart contract. Because of how Lamington works, this contract could actually live anywhere in the structure of the application, so if having a contracts/ folder doesn't work for you and you'd rather it live at eos/stuff/things/, don't worry, but we'll put it there for this example.

Create contracts/example.cpp as follows:

#include <eosio/eosio.hpp>

class[[eosio::contract("example")]] example : public eosio::contract
{
 public:
 using contract::contract;

 [[eosio::action]] void dothing()
 {
  require_auth(_self);

  eosio::print("Success!");
 };
};

If you're new to EOS smart contract development, there are a few key things to note:

  • The naming of dothing() is important. It has to be a valid EOS name, meaning:
    • The only allowed characters are lowercase letters, a period, and numbers up to 5 (or 6 in some situations, but a good rule of thumb is to just treat 5 as the max).
    • A maximum length of 12 characters
    • This means that do_thing() is not valid, and doThing() is also not valid.
    • This is because EOS names are encoded into a uint64_t, so there are only so many bits for characters. You can see the implementation of EOS names here if you want to learn more.
  • require_auth(_self) requires the sender of the transaction to be the account that deployed the contract. This is an easy way on EOS to implement owner-only actions.
  • eosio::print() is an easy way to debug your EOS smart contracts.

Step 4: Add a test file

There are several ways to add tests for Lamington to run:

  • Lamington will load any JavaScript or TypeScript files under a test or spec directory in your project root.
  • Lamington will load any JavaScript or TypeScript files anywhere in the project that end in .test.js or .test.ts.
  • Lamington will load any JavaScript or TypeScript files anywhere in the project that end in .spec.js or .spec.ts.

If you want to tweak this behaviour, you can ignore files using the exclude configuration value in .lamingtonrc.

So let's create a file called contracts/example.test.js:

const {
 AccountManager,
 ContractDeployer,
 assertMissingAuthority
} = require("lamington");

describe("Example Contract", function() {
 let contract;
 let account1;

 before(async function() {
  account1 = await AccountManager.createAccount();
  });

 beforeEach(async function() {
  contract = await ContractDeployer.deploy("contracts/example");
  });

 it("allows the owner to call dothing()", async function() {
  await contract.dothing();
 });

 it("should throw when calling dothing() from another account", async function() {
  await assertMissingAuthority(contract.dothing({ from: account1 }));
 });
});

This is standard Mocha code, so you can use whatever you like from their docs and you can add Chai or another assertion library if you like.

So why are we running ContractDeployer.deploy("contracts/example") before every test? Since EOS doesn't let us roll back easily, this lets us run tests in a "clean" environment, which means a fresh copy of the contract is deployed for every single test.

Step 5: Run the Tests

Still with me? Great! Let's run those tests:

$ lamington test

If you haven't run Lamington before, it's now going to build a Docker image for you, (which takes a few minutes) then it will run the tests you just created. You should see that the tests pass!

Optional: Use TypeScript for the Tests

Ok, so what if we want to use TypeScript? That's easy, we only need to do a few things:

  1. Rename contracts/example.test.js to contracts/example.test.ts.
  2. Install types for Mocha:
    $ npm i --save-dev @types/mocha
  3. Add a tsconfig.js file to the root of the project. Here's an example:
    {
     "compilerOptions": {
      "target": "es5",
      "module": "commonjs",
      "strict": true,
      "noImplicitAny": true,
      "lib": ["es6", "es2015", "dom"]
     },
     "exclude": ["node_modules"]
    }
  4. Update the code in the tests to leverage the types that Lamington generates for you from your ABIs:
    import {
     Account,
     AccountManager,
     ContractDeployer,
     assertMissingAuthority
    } from "lamington";
    import { Example } from "./example";

    describe("Example Contract", function() {
     let contract: Example;
     let account1: Account;

     before(async function() {
      account1 = await AccountManager.createAccount();
      });

     beforeEach(async function() {
      contract = await ContractDeployer.deploy("contracts/example");
      });

     it("allows the owner to call dothing()", async function() {
      await contract.dothing();
     });

     it("should throw when calling dothing() from another account", async function() {
      await assertMissingAuthority(contract.dothing({ from: account1 }));
     });
    });

Now you have full autocomplete in VSCode when you call actions on your contracts, and your tests are type safe!

VSCode autocomplete for smart contract actions

Summary

By now you've likely realised some of the things that Lamington can do for you:

  • It manages a Docker container with EOS and the Contract Development Toolkit (CDT) in it for you.
  • It builds your contracts.
  • It creates typed interfaces for your contracts from ABI files.
  • It includes a bunch of helper methods that help with smart contract testing. See the full list here.
  • It can be configured to ignore certain tests.

I hope you've found this helpful. We're always keen to talk to new EOS devs, so please feel free to join our Slack below!

Join us on Slack

Want to link up with the core dev team and other EOS devs? Invites are open, join us!