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
-
Create build folder
mkdir build cd build
-
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 ..
-
Build func & fift
make func fift cd crypto
-
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
- 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
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();
}
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";
From npm
yarn add ton-stdlib
Include from node-modules:
#include "../../node_modules/ton-stdlib/func/stdlib.fc";
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);
}
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();
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);
Message is structured that way:
- 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)
- Source address (2 zero bits, address is none)
- Destination address
- Message metadata
- 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()
);
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();
}
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();
}
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
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.
- Get some Sandbox coins
- Install dependencies
yarn add ton-core ton qrcode-terminal qs
yarn add -D @types/qs @types/qrcode-terminal
- 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)
});
- Run script & scan qr code in the Sandbox wallet
yarn ts-node manage/deploy-sandbox.ts
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);
})()
Congratulations
You've set up environment, built your first contract on TON and tried it in the sandbox network 🚀
Further reading
- Learn FunC in Y minutes
- ton.org docs
- Smart-contract guidelines
- Examples: nft & ft contracts
- Examples: PoW giver contract
- Examples: wallet v4 contract
- tblkch.pdf
Top comments (3)
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 ?
can you recommend some roadmap for learning TON?
¯_(ツ)_/¯