Building FreeTON dapps with Locklift

I'm happy to present you the Locklift package - it's a development environment for smart contract developers. It's inspired by the Truffle and Hardhat Ethereum frameworks and provides roughly similar functionality. By using Locklift you can

  • Build your Solidity smart contracts (generate code with compiler, generate TVM with TVM-linker, and encode it into base64 for better UX)
  • Test your contracts with Mocha JS
  • Run arbitrary scripts
  • Manage your networks (main, dev, local, etc)

Getting started

Install Locklift as usual NPM package:

$ npm install -g locklift

Initialize new Locklift project directory with predefined configuration and some sample code:

$ locklift init --path my-new-freeton-dapp
New Locklift project initialized in my-new-freeton-dapp

Customizing your configuration

At the root of your project, you can find the

locklift.config.js

It stores the configuration for your Locklift project. It's an entry point for using Locklift - each command use some data from the configuration. Let's see how it looks like

$ cat locklift.config.js 
module.exports = {
  compiler: {
    // Specify path to your TON-Solidity-Compiler
    path: '/usr/bin/solc-ton',
  },
  linker: {
    // Path to your TVM Linker
    path: '/usr/bin/tvm_linker',
  },
  networks: {
    // You can use TON labs graphql endpoints or local node
    local: {
      ton_client: {
        // See the TON client specification for all available options
        network: {
          server_address: 'http://localhost/',
        },
      },
      // This giver is default local-node giver
      giver: {
        address: '0:841288ed3b55d9cdafa806807f02a0ae0c169aa5edfe88a789a6482429756a94',
        abi: { "ABI version": 1, "functions": [ { "name": "constructor", "inputs": [], "outputs": [] }, { "name": "sendGrams", "inputs": [ {"name":"dest","type":"address"}, {"name":"amount","type":"uint64"} ], "outputs": [] } ], "events": [], "data": [] },
        key: '',
      },
      // Use tonos-cli to generate your phrase
      // !!! Never commit it in your repos !!!
      keys: {
        phrase: '',
        amount: 20,
      }
    },
  },
};

Most of the parameters speak for themselves. Let me clarify the meaning of some of them:

  • Replace the path to your compiler and linker if it differs from the default
  • By default, the only local network is specified, but you can add as many as you need, such as dev or main for dev and main networks.
  • The ton_client field stores the settings which will be used for initializing the TonClient from the https://github.com/tonlabs/ton-client-js SDK.
  • Customize the giver object, for example, you can set the giver keys if your smart contract expects a specific msg.pubkey()
  • Specify the keys, so your keys will remain the same. By default, a new mnemonic is generated for each run.

Writing and building contract

You can find the sample contract in the contracts/ folder. Use this folder to place all your smart contracts.

$ cat contracts/Sample.sol 
pragma solidity >= 0.6.0;
pragma AbiHeader expire;
pragma AbiHeader pubkey;


contract Sample {
    uint16 static _nonce;

    uint state;

    event StateChange(uint _state);

    constructor(uint _state) public {
        tvm.accept();

        setState(_state);
    }

    function setState(uint _state) public {
        tvm.accept();
        state = _state;

        emit StateChange(_state);
    }

    function getDetails()
        external
        view
    returns (
        uint _state
    ) {
        return state;
    }
}

This sample contract stores some integer state variable and allows you to read it with a special function. Additionally, it has static _nonce which is filled with random integer, so the address turns out to be new every run.

Run the following command from the Locklift project root, to build the contracts:

$ locklift build --config locklift.config.js 
Found 1 sources
Building contracts/Sample.sol
Compiled contracts/Sample.sol
Linked contracts/Sample.sol

Running tests

The tests by default are placed in the test/ folder. It's a regular Mocha test, which expects the pre-configured locklift object:

$ cat test/sample-test.js 
const { expect } = require('chai');


let Sample;
let sample;

const getRandomNonce = () => Math.random() * 64000 | 0;


describe('Test Sample contract', async function() {
  describe('Contracts', async function() {
    it('Load contract factory', async function() {
      Sample = await locklift.factory.getContract('Sample');
      
      expect(Sample.code).not.to.equal(undefined, 'Code should be available');
      expect(Sample.abi).not.to.equal(undefined, 'ABI should be available');
    });
    
    it('Deploy contract', async function() {
      this.timeout(20000);

      const [keyPair] = await locklift.keys.getKeyPairs();
      
      sample = await locklift.giver.deployContract({
        contract: Sample,
        constructorParams: {
          _state: 123
        },
        initParams: {
          _nonce: getRandomNonce(),
        },
        keyPair,
      });
  
      expect(sample.address).to.be.a('string')
        .and.satisfy(s => s.startsWith('0:'), 'Bad future address');
    });

    it('Interact with contract', async function() {
      await sample.run({
        method: 'setState',
        params: { _state: 111 },
      });

      const response = await sample.call({
        method: 'getDetails',
        params: {},
      });

      expect(response.toNumber()).to.be.equal(111, 'Wrong state');
    });
  });
});

Let's run the local network and see how the test works:

$ docker run --rm -d --name local-node -p80:80 -e USER_AGREEMENT=yes tonlabs/local-node
$ locklift test --config locklift.config.js --network local


  Test Sample contract
    Contracts
      ✓ Load contract factory
      ✓ Deploy contract (2181ms)
      ✓ Interact with contract (1038ms)


  3 passing (3s)

As you see, you're specifying the network parameter. By using it, you run easily switch to any network, specified in your configuration.

Account support

As you probably know, TON has internal and external messages. Let's say you expect the user to call your contract method with an internal message. Usually, you need to create some wallet-like contract, which has a method to receive (TvmCell payload, address receiver) to send an internal message to the receiver smart contract. In Locklift this contract is called Account and available out of the box:

const [keyPair] = await locklift.keys.getKeyPairs();

const Account = await locklift.factory.getAccount();

const owner = await locklift.giver.deployContract({
    contract: Account,
    constructorParams: {},
    initParams: {
	    _randomNonce: getRandomNonce(),
    },
    keyPair,
}, locklift.utils.convertCrystal(30, 'nano'));


const tx = await owner.runTarget({
    contract: someTargetContract,
    method: 'callMe',
    params: {
    	some_integer: 1
    },
});

console.log(`Transaction (external -> owner -> target): ${tx.transaction.id}`);

Using contracts from another module

Let's say you have another project, which provides some smart contracts, and you want to use these contracts without duplicating them. To do this, just install this additional project with NPM and use relative paths to the smart contract:

pragma ton-solidity ^0.39.0;
pragma AbiHeader expire;
pragma AbiHeader pubkey;


import './../node_modules/some-additional-module/contracts/ParentContract.sol';


contract DaughterContract is ParentContract {
	...
}

I hope this package will inspire you to build new awesome projects on the FreeTON!   If you have any questions or suggestions about the Locklift, please create the issue on Github. See you soon!