DEV Community

Dan Volkov
Dan Volkov

Posted on • Updated on

Introduction to FunC: how to start developing on TON?

I've started developing in TON last year. At the beginning there were just a few articles and a bit of docs at ton.org. Now its ecosystem is growing, so many can find this blockchain interesting. In this article I want to share my experience with beginners through a guide with some useful examples.

We will build & deploy a simple "Hello world" contract.

Overview

TON is PoS blockchain based on the actor model, interaction between contracts can be done only by messages, unlike EVM-powered chains, where you can call other contracts during computations.

Any data stored in TON blockchain is stored in cells. Cells can contain up to 1023 bits and 4 references to other cells. Cell itself is immutable, to read from it you need to create a slice. Generally speaking, slices are read-only cell representation. To create a new cell here comes another type - builder. Builder is the opposite of slice: write-only cell representation, which can be converted to cell. Cells can be serialised to Bag Of Cells (BoC). BoC will contain the root cell and the whole referenced cells' tree.

Let's speak about tools. Now there are two languages:

  • fift - low-level language, has interpreter, so can be used for scripting
  • func - high-level imperative language, compiled to fift

...and one more thing: Tact language, designed specifically for the beginners, is under development.
In this article we'll focus only on FunC, but if you want, you may read about Fift yourself.

Installing build tools

The best way (at the time of writing) to install actual versions of tools is to compile them yourself.

UPDATE: now you can download precompiled binaries from official ton-blockchain/ton releases.

Building func & fift from source

Requirements

  • cmake ([For MacOS] XCode build Tools will be ok)
  • make ([For MacOS] XCode build Tools will be ok)
  • openssl ([For MacOS] can be installed via brew install openssl )
  • Clone TON monorepo and fetch submodules
git clone https://github.com/ton-blockchain/ton
cd ton
git submodule init
git submodule update
Enter fullscreen mode Exit fullscreen mode
  1. Create build folder

    mkdir build
    cd build
    
  2. Run cmake to create build environment

    # make sure that OPENSSL_ROOT_DIR is set
    # For MacOS users: export OPENSSL_ROOT_DIR=/usr/local/opt/openssl/
    cmake ..
    
  3. Build func & fift

    make func fift
    cd crypto
    
  4. Install binaries to library directories or add them to PATH

    cp fift /usr/local/bin/fift
    cp func /usr/local/bin/func
    

Now we are ready to start!

Creating a project

I suggest using the Node.js environment for FunC projects, but feel free to use any other environment you want. Also, I suppose My common project structure is:

├── build
│   └── ...build results
├── manage
│   └── deploy.ts
├── contracts
│   └── hello-world.fc
├── test
│   └── ...tests
├── build.sh
├── jest.config.js
├── package.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode
  • build folder contains built result.
  • manage folder contains scripts for deploy, dump smart contract data from blockchain, etc.
  • test folder contains tests.

To init Node.js project with Typescript:

yarn init
yarn add -D typescript ts-node @types/node
yarn run tsc --init
Enter fullscreen mode Exit fullscreen mode

Creating smart-contract

A bit of theory: smart-contract in TON can handle two types of messages: internal & external. Internal messages are sent from one contract to another, externals - from the internet to contract. Another way to interact with contract is to call get methods. Get methods are used for computing some helpful information from the current contract's state and executed offchain. They are marked with the method_id keyword.

So, the contract consists of two entrypoints, recv_internal and recv_external. Also, contracts can contain as many get methods as you want.

Our contract will respond to any internal message with Hello, world! in comment and count how many messages were handled. Counter will be accessible through the get method.

This is hello-world.fc structure:

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    return ();
}

() recv_external(slice in_msg) impure {
    ;; Do not accept external messages
    throw(0xffff);
}

int counter() method_id {
    return 1;
}

slice owner() method_id {
    return null();
}
Enter fullscreen mode Exit fullscreen mode

We have three methods: recv_internal for handling internal messages, recv_external for handling external messages (rejects any external message) and counter get method.

Installing stdlib.fc

There are two ways to include stdlib.fc: copy to project sources or install via yarn/npm.
From sources
Download stdlib.fc and copy to the src/contracts. Then, include it:

#include "stdlib.fc";
Enter fullscreen mode Exit fullscreen mode

From npm

yarn add ton-stdlib
Enter fullscreen mode Exit fullscreen mode

Include from node-modules:

#include "../../node_modules/ton-stdlib/func/stdlib.fc";
Enter fullscreen mode Exit fullscreen mode

Sending message

Let's modify our recv_internal to send a message back to the user.

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    slice cs = in_msg_full.begin_parse();
    cs~skip_bits(4); ;; skip flags

    slice sender_address = cs~load_msg_addr();

    ;; Send message
    var message = begin_cell()
        .store_uint(0x10, 6)
        .store_slice(sender_address)
        .store_coins(0)
        .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
        .store_uint(0, 32)
        .store_slice("Hello, world!")
        .end_cell();

    send_raw_message(message, 64);
}
Enter fullscreen mode Exit fullscreen mode

At first, we should get the sender address. According to the TL-b scheme, we should skip 4 bits (scheme tag & flags), after goes sender address.

slice cs = in_msg_full.begin_parse();
cs~skip_bits(4);

slice sender_address = cs~load_msg_addr();
Enter fullscreen mode Exit fullscreen mode

Then, we know the sender address and can send a response.

;; Send message
var message = begin_cell()
    .store_uint(0x10, 6)         ;; flags
    .store_slice(sender_address) ;; destination
    .store_coins(0)              ;; amount 
    .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; metadata
    .store_uint(0, 32)
    .store_slice("Hello, world!") ;; payload
    .end_cell();

send_raw_message(message, 64);
Enter fullscreen mode Exit fullscreen mode

Message is structured that way:

  1. 4 bits of flags
    • internal message tag (1 zero bit)
    • ihr_disabled (Instant Hypercube Routing, learn more at tblkch.pdf)
    • bounce (can the receiving part bounce a message?)
    • bounced (means that receiving part failed, so the message returned to contract)
  2. Source address (2 zero bits, address is none)
  3. Destination address
  4. Message metadata
  5. Content

The message cell layout better described here.

We are sending message with flag 64, it means that the amount increased by the remaining value of the inbound message.

You can find more about send_raw_message flags here.

Message will be sent after computations, because transactions are split into different phases: storage, credit, compute, action and bounce. Phases described at the brief TVM overview.

Updating counter

After sending a message we need to update the counter.

var cs = get_data().begin_parse();
var counter = cs~load_uint(32);

set_data(
    begin_cell()
        .store_uint(counter + 1, 32)
        .store_slice(cs)
    .end_cell()
);
Enter fullscreen mode Exit fullscreen mode

At first, we are getting the old counter from the contract's data. Then, we store the new data with the updated counter.

Adding get-methods

counter should return just single integer with the current state of counter, owner should return owner.

int counter() method_id {
    var data = get_data().begin_parse();
    return data~load_uint(32);
}

slice owner() method_id {
    var data = get_data().begin_parse();
    data~skip_bits(32);
    return data~load_msg_addr();
}
Enter fullscreen mode Exit fullscreen mode

Result

#include "../../node_modules/ton-stdlib/func/stdlib.fc";

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    slice cs = in_msg_full.begin_parse();
    cs~skip_bits(4);

    slice sender_address = cs~load_msg_addr();

    ;; Send message
    var message = begin_cell()
        .store_uint(0x10, 6) ;; 010000
        .store_slice(sender_address)
        .store_coins(0)
        .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
        .store_uint(0, 32)
        .store_slice("Hello, world!")
        .end_cell();

    send_raw_message(message, 64);

    ;; Update counter
    var cs = get_data().begin_parse();
    var counter = cs~load_uint(32);

    set_data(
        begin_cell()
            .store_uint(counter + 1, 32)
            .store_slice(cs)
        .end_cell()
    );
}

() recv_external(slice in_msg) impure {
    throw(0xffff);
}

int counter() method_id {
    var data = get_data().begin_parse();
    return data~load_uint(32);
}

slice owner() method_id {
    var data = get_data().begin_parse();
    data~skip_bits(32);
    return data~load_msg_addr();
}
Enter fullscreen mode Exit fullscreen mode

Writing build.sh

To deploy & test contract we need to compile & serialise it to BoC. It can be done by single command:

func -Wbuild/hello-world.boc src/contracts/hello-world.fc | fift >> /dev/null
Enter fullscreen mode Exit fullscreen mode

At first, we build hello-world.fc using func, with the flag -W. Flag -W appends Fift code for serialisation of the resulting contract to BoC. func emits fift code that is further passed to fift interpreter. Then, we forward interpreter's stdout to /dev/null to avoid messy outputs in the console.

Deploying

There are several ways to deploy contract: via toncli, via fift & lite-client, via tonweb, via ton, etc.

In this guide I we will deploy contract to the sandbox network using Tonhub Sandbox and ton library.

  1. Get some Sandbox coins
  2. Install dependencies
yarn add ton-core ton qrcode-terminal qs
yarn add -D @types/qs @types/qrcode-terminal
Enter fullscreen mode Exit fullscreen mode
  1. Add manage/deploy-sandbox.ts script
import { Cell, Builder, storeStateInit, contractAddress, StateInit, toNano, beginCell, Address } from 'ton-core';
import { readFileSync } from 'fs';
import qs from 'qs';
import qrcode from 'qrcode-terminal';

// Create a data cell similar to the initial contract state
const dataCell = beginCell()
    .storeUint(0, 32)                        // counter
    .storeAddress(Address.parse('INSERT_ADDRESS'))
    .endCell();

// Load code from build
const codeCell = Cell.fromBoc(readFileSync('./build/hello-world.boc'))[0];

// Calculate address from code & data
const address = contractAddress(0, {
    code: codeCell,
    data: dataCell
});

// Prepare init message
const initCell = new Builder();

storeStateInit({
    code: codeCell,
    data: dataCell,
})(initCell);

// Encode link to deploy contract
let link = 'https://test.tonhub.com/transfer/' + address.toString({ testOnly: true }) + '?' + qs.stringify({
    text: 'Deploy contract',
    amount: toNano(1).toString(10),
    init: initCell.asCell().toBoc({ idx: false }).toString('base64')
});

console.log('Address: ' + address.toString({ testOnly: true }));

qrcode.generate(link, { small: true }, (code) => {
     console.log(code)
});
Enter fullscreen mode Exit fullscreen mode
  1. Run script & scan qr code in the Sandbox wallet
yarn ts-node manage/deploy-sandbox.ts
Enter fullscreen mode Exit fullscreen mode

Testing contract

Send any amount to the address you've got from the deployment step and you will get "Hello, world!" back. Check the transactions in the sandbox explorer.

Getting counter & owner

Create a .ts file in the manage folder and name it like dump-sandbox.ts. Replace address to yours. Create TonClient with a sandbox endpoint and call get methods.

import { TonClient } from 'ton'
import { Address } from 'ton-core'

let address = Address.parse('YOUR CONTRACT ADDRESS');

(async () => {
    // Create sandbox API client
    const client = new TonClient({
        endpoint: 'https://sandbox.tonhubapi.com/jsonRPC'
    });

    // Call get method and log the result
    let counter = await client.runMethod(address, 'counter');
    console.log('Counter', parseInt(counter.stack[0][1], 16));

    let owner = await client.runMethod(address, 'owner');
    let ownerCell = Cell.fromBoc(Buffer.from(owner.stack[0][1].bytes, 'base64'))[0];
    let ownerAddress = ownerCell.beginParse().loadAddress()?.toFriendly({ testOnly: true });
    console.log('Address: ', ownerAddress);
})()
Enter fullscreen mode Exit fullscreen mode

Congratulations

You've set up environment, built your first contract on TON and tried it in the sandbox network 🚀

Further reading

Top comments (3)

Collapse
 
durmusgulbahar profile image
durmusgulbahar

I do not understand, what should i do after build ton. Where should i create my project folder and how to use ton compiler . with ton-compiler ?

Collapse
 
_romancooper profile image
Roman Bondar

can you recommend some roadmap for learning TON?

Collapse
 
dvlkv profile image
Dan Volkov
  1. Learn what is TON and it's basic concepts. Can recommend TonSpace.
  2. Try to create your first contract.
  3. Learn more about TON & TVM from whitepapers and source code.
  4. Practice a lot

¯_(ツ)_/¯