About how to write and publish a smart contract in the Telegram Open Network (TON)

About how to write and publish a smart contract in TON

What is this article about?

In the article, I will talk about how I participated in the first (of two) Telegram blockchain competition, did not take a prize, and decided to record the experience in the article so that it does not sink into oblivion and, perhaps, helps someone.

Since I didn’t want to write abstract code, but to do something working, for the article I wrote an instant lottery smart contract and a website that shows smart contract data directly from TON without using intermediate storage.

The article will be useful for those who want to make their first smart contract in TON, but do not know where to start.

Using the example of a lottery, I will go from setting up the environment to publishing a smart contract, interacting with it, and writing a site for receiving and publishing data.

About participation in the competition

Last October, Telegram announced a blockchain competition with new languages Fift ΠΈ FunC. It was necessary to choose to write any of the five proposed smart contracts. I figured it would be nice to do something out of the ordinary, learn a language and do something, even if I don't have to write anything else in the future. Plus, the topic is constantly on hearing.

It is worth saying that I had no experience in developing smart contracts.

I planned to participate until the very end, as long as it turns out and then write a review article, but I immediately failed at the first one. I wrote wallet with multi-signature on FunC and he generally worked. Took as a basis smart contract on Solidity.

At that time, I thought that this was definitely enough to take at least some prize-winning place. As a result, about 40 out of 60 participants became winners and I was not among them. In general, this is nothing terrible, but one thing strained me. At the time of the announcement of the results, a review with a test for my contract had not been done, I asked the participants in the chat if there were anyone else who did not have it, there were none.

Apparently, having paid attention to my messages, two days later the judges published a comment and I still did not understand, they accidentally missed my smart contract during the judging, or simply considered that it was so bad that it did not need comment. I asked a question on the page, but did not receive a response. Although who judged is not a secret, I considered it superfluous to write personal messages.

A lot of time was spent on understanding, so it was decided to write an article. Since there is not much information yet, the article will help save time for everyone interested.

The concept of smart contracts in TON

Before you write something, you need to figure out which side to approach this thing at all. Therefore, now I will tell you what parts the system consists of. More precisely, what parts do you need to know in order to write at least some kind of working contract.

We will focus on writing a smart contract and working with TON Virtual Machine (TVM), Fift ΠΈ FunC, so the article is more like a description of the development of a conventional program. We will not dwell on how the platform itself works here.

Generally about how it works TVM and language Fift there is good official documentation. During my participation in the competition and now during the writing of the current contract, I often turned to her.

The main language in which smart contracts are written is FunC. There is no documentation on it at the moment, so in order to write something, you need to study examples of smart contracts from the official repository and the language implementation itself there, plus you can watch examples of smart contracts for the past two contests. Links at the end of the article.

Suppose we have already written a smart contract for FunC, after that we compile the code into Fift assembler.

The compiled smart contract remains to be published. To do this, you need to write a function for Fift, which will take the smart contract code and some other parameters as input, and the output will be a file with the extension .boc (which means "bag of cells"), and, depending on how we write it, a private key and an address that is generated based on the smart contract code. Grams can already be sent to a smart contract address that has not yet been published.

To publish a smart contract in TON received .boc the file will need to be sent to the blockchain using a light client (more on that below). But before publishing, you need to transfer grams to the generated address, otherwise the smart contract will not be published. After publication, it will be possible to interact with a smart contract by sending messages to it from outside (for example, using a light client) or from within (for example, one smart contract sends a message to another inside TON).

After we understand how the code is published, it becomes easier further. We roughly know what we want to write and how our program will work. And while writing, we are looking for how it is already implemented in existing smart contracts, or we look into the implementation code Fift ΠΈ FunC in the official repository, or look in the official documentation.

Very often I searched for keywords in the Telegram chat where all the participants of the contest and Telegram employees, including, gathered, it so happened that during the contest everyone gathered there and started discussing Fift and FunC. Link at the end of the article.

It's time to move from theory to practice.

Preparing the environment for working with TON

Everything that will be described in the article I did on MacOS and double-checked in a clean Ubuntu 18.04 LTS on Docker.

The first thing to do is download and install lite-client with which you can send requests to TON.

The instructions on the official website describe the installation process in some detail and clearly and omit some details. Here we follow the instructions along the way, installing the missing dependencies. I did not compile each project myself and installed from the official Ubuntu repository (on MacOS I used brew).

apt -y install git 
apt -y install wget 
apt -y install cmake 
apt -y install g++ 
apt -y install zlib1g-dev 
apt -y install libssl-dev 

Once all dependencies are installed, you can install lite-client, Fift, FunC.

First, we clone the TON repository along with the dependencies. For convenience, we will do everything in a folder ~/TON.

cd ~/TON
git clone https://github.com/ton-blockchain/ton.git
cd ./ton
git submodule update --init --recursive

The repository also stores implementations Fift ΠΈ FunC.

Now we are ready to build the project. Repository code cloned into folder ~/TON/ton. In ~/TON create a folder build and collect the project in it.

mkdir ~/TON/build 
cd ~/TON/build
cmake ../ton

Since we are going to write a smart contract, we need not only lite-clientbut Fift с FunC, so we compile everything. Not a fast process so we are waiting.

cmake --build . --target lite-client
cmake --build . --target fift
cmake --build . --target func

Next, download the configuration file, which contains data about the node to which lite-client will connect.

wget https://test.ton.org/ton-lite-client-test1.config.json

Making the first requests in TON

Now let's run lite-client.

cd ~/TON/build
./lite-client/lite-client -C ton-lite-client-test1.config.json

If the build was successful, then after launch you will see the light client connection log to the node.

[ 1][t 2][1582054822.963129282][lite-client.h:201][!testnode]   conn ready
[ 2][t 2][1582054823.085654020][lite-client.cpp:277][!testnode] server version is 1.1, capabilities 7
[ 3][t 2][1582054823.085725069][lite-client.cpp:286][!testnode] server time is 1582054823 (delta 0)
...

You can run the command help and see what commands are available.

help

Let's list the commands that we will use in this article.

list of available commands:
last    Get last block and state info from server
sendfile <filename> Load a serialized message from <filename> and send it to server
getaccount <addr> [<block-id-ext>]  Loads the most recent state of specified account; <addr> is in [<workchain>:]<hex-or-base64-addr> format
runmethod <addr> [<block-id-ext>] <method-id> <params>...   Runs GET method <method-id> of account <addr> with specified parameters

last ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ послСдний созданный Π±Π»ΠΎΠΊ с сСрвСра. 

sendfile <filename> отправляСт Π² TON Ρ„Π°ΠΉΠ» с сообщСниСм, ΠΈΠΌΠ΅Π½Π½ΠΎ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ этой ΠΊΠΎΠΌΠ°Π½Π΄Ρ‹ публикуСтся смарт-ΠΊΠΎΠ½Ρ‚Ρ€Π°ΠΊΡ‚ ΠΈ запрсосы ΠΊ Π½Π΅ΠΌΡƒ. 

getaccount <addr> Π·Π°Π³Ρ€ΡƒΠΆΠ°Π΅Ρ‚ Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π΅ состояниС смарт-ΠΊΠΎΠ½Ρ‚Ρ€Π°ΠΊΡ‚Π° с ΡƒΠΊΠ°Π·Π°Π½Π½Ρ‹ΠΌ адрСсом. 

runmethod <addr> [<block-id-ext>] <method-id> <params>  запускаСт get-ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ смартконтракта. 

Now we are ready to write the contract itself.

implementation

Idea

As I wrote above, the smart contract that we are writing is a lottery.

Moreover, this is not a lottery in which you need to buy a ticket and wait an hour, a day or a month, but an instant one in which the user transfers to the address of the contract N grams, and instantly gets back 2 * N grams or loses. We will make the probability of winning about 40%. If there are not enough grams for payment, then we will consider the transaction as a replenishment.

Moreover, it is important that the bets can be seen in real time and in a convenient way, so that the user can immediately understand whether he won or lost. Therefore, you need to make a website that will show the rates and the result directly from TON.

Writing a smart contract

For convenience, I made code highlighting for FunC, the plugin can be found and installed in the Visual Studio Code search, if you suddenly want to add something, then I posted the plugin in the public domain. Also, someone previously made a plugin for working with Fift, you can also install it and find it in VSC.

Immediately create a repository where we will commit intermediate results.

To make our life easier, we will write a smart contract and test it locally until it is ready. Only after that we will publish it in TON.

The smart contract has two external methods that can be accessed. First, recv_external() this function is executed when a request to the contract comes from the outside world, that is, not from TON, for example, when we ourselves form a message and send it via lite-client. Second, recv_internal() this is when, inside the TON itself, any contract refers to ours. In both cases, you can pass parameters to the function.

Let's start with a simple example that will work if published, but does not contain any functional load.

() recv_internal(slice in_msg) impure {
    ;; TODO: implementation 
}

() recv_external(slice in_msg) impure {
    ;; TODO: implementation  
}

Here it is necessary to explain what slice. All stored data in TON Blockchain is a collection TVM cell or simply cell, such a cell can store up to 1023 bits of data and up to 4 references to other cells.

TVM cell slice or slice is part of the existing cell is used for its parsing, it will be clear further. The main thing for us is that we can transfer to a smart contract slice and, depending on the type of message, process the data in recv_external() or recv_internal().

impure β€” a keyword that indicates that the function changes the smart contract data.

Save the contract code in lottery-code.fc and compile.

~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc 

The value of the flags can be viewed using the command

~/TON/build/crypto/func -help

We have compiled Fift assembler code in lottery-compiled.fif:

// lottery-compiled.fif

"Asm.fif" include
// automatically generated from `/Users/rajymbekkapisev/TON/ton/crypto/smartcont/stdlib.fc` `./lottery-code.fc` 
PROGRAM{
  DECLPROC recv_internal
  DECLPROC recv_external
  recv_internal PROC:<{
    //  in_msg
    DROP    // 
  }>
  recv_external PROC:<{
    //  in_msg
    DROP    // 
  }>
}END>c

It can be run locally, for this we will prepare the environment.

Note that the first line connects Asm.fif, this is code written in Fift for Fift assembler.

Since we want to run and test the smart contract, we will create a file locally lottery-test-suite.fif and copy the compiled code there, replacing the last line in it, which writes the smart contract code into a constant codeto then pass it to the virtual machine:

"TonUtil.fif" include
"Asm.fif" include

PROGRAM{
  DECLPROC recv_internal
  DECLPROC recv_external
  recv_internal PROC:<{
    //  in_msg
    DROP    // 
  }>
  recv_external PROC:<{
    //  in_msg
    DROP    // 
  }>
}END>s constant code

While it seems clear, now let's add the code that we will use to launch TVM to the same file.

0 tuple 0x076ef1ea , // magic
0 , 0 , // actions msg_sents
1570998536 , // unix_time
1 , 1 , 3 , // block_lt, trans_lt, rand_seed
0 tuple 100000000000000 , dictnew , , // remaining balance
0 , dictnew , // contract_address, global_config
1 tuple // wrap to another tuple
constant c7

0 constant recv_internal // to run recv_internal() 
-1 constant recv_external // to invoke recv_external()

Π’ c7 we write the context, that is, the data with which the TVM (or network state) will be launched. Even during the competition, one of the developers showed how to create c7 and I copied. In this article, we may need to change rand_seed since the generation of a random number depends on it and does not change, the same number will be returned each time.

recv_internal ΠΈ recv_external constants with a value of 0 and -1 will be responsible for calling the appropriate functions in the smart contract.

We are now ready to create the first test for our empty smart contract. For clarity, for now, we will add all the tests to the same file. lottery-test-suite.fif.

Let's create a variable storage and write an empty cell, this will be the storage of the smart contract.

message this is the message that we will send to the smart contact from the outside. Let's make it empty for now.

variable storage 
<b b> storage ! 

variable message 
<b b> message ! 

After we have prepared the constants and variables, we start TVM using the command runvmctx and pass the created parameters to the input.

message @ 
recv_external 
code 
storage @ 
c7 
runvmctx 

As a result, we will succeed this intermediate code to Fift.

Now we can run the resulting code.

export FIFTPATH=~/TON/ton/crypto/fift/lib // выполняСм ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π· для удобства 
~/TON/build/crypto/fift -s lottery-test-suite.fif 

The program should work without errors and in the output we will see the execution log:

execute SETCP 0
execute DICTPUSHCONST 19 (xC_,1)
execute DICTIGETJMPZ
execute DROP
execute implicit RET
[ 3][t 0][1582281699.325381279][vm.cpp:479]     steps: 5 gas: used=304, max=9223372036854775807, limit=9223372036854775807, credit=0

Great, we have written the first working version of the smart contract.

Now we need to add functionality. Let us first deal with the messages that come from the outside world to recv_external()

The developer himself chooses the message format that the contract can accept.

But usually

  • First, we want to protect our contract from the outside world and make it so that only the owner of the contract can send external messages to it.
  • secondly, when we send a valid message to TON, we want it to happen exactly once and when the same message is sent again, the smart contract rejects it.

Therefore, in almost every contract, these two problems are solved, since our contract accepts external messages, we also need to take care of this.

We'll do it in reverse order. First, we solve the problem with repetition, if the contract has already received such a message and processed it, then it will not execute it a second time. And then we will solve the problem so that only a certain circle of people can send messages to the smart contract.

There are different ways to solve the problem with repeated messages. We'll do it this way. In the smart contract, we initialize the counter of received messages with an initial value of 0. In each message to the smart contract, we will add the current value of the counter. If the value of the counter in the message does not match the value in the smart contract, then we do not process it, if it does, then we process and increase the counter in the smart contract by 1.

We return to lottery-test-suite.fif and add the second test to it. Let's send an invalid number, the code should throw an exception. For example, let's say 166 is stored in the contract data, and we will send 165.

<b 166 32 u, b> storage !
<b 165 32 u, b> message !

message @ 
recv_external 
code 
storage @ 
c7 
runvmctx

drop 
exit_code ! 
."Exit code " exit_code @ . cr 
exit_code @ 33 - abort"Test #2 Not passed"

Let's run.

 ~/TON/build/crypto/fift -s lottery-test-suite.fif 

And we will see that the test is executed with an error.

[ 1][t 0][1582283084.210902214][words.cpp:3046] lottery-test-suite.fif:67: abort": Test #2 Not passed
[ 1][t 0][1582283084.210941076][fift-main.cpp:196]      Error interpreting file `lottery-test-suite.fif`: error interpreting included file `lottery-test-suite.fif` : lottery-test-suite.fif:67: abort": Test #2 Not passed

At this stage lottery-test-suite.fif should look like here to register:.

Now let's add the counter logic to the smart contract in lottery-code.fc.

() recv_internal(slice in_msg) impure {
    ;; TODO: implementation 
}

() recv_external(slice in_msg) impure {
    if (slice_empty?(in_msg)) {
        return (); 
    }
    int msg_seqno = in_msg~load_uint(32);
    var ds = begin_parse(get_data());
    int stored_seqno = ds~load_uint(32);
    throw_unless(33, msg_seqno == stored_seqno);
}

Π’ slice in_msg lies the message we're sending.

The first thing we do is check if there is data in the message, if not, then we just exit.

Next, we parse the message. in_msg~load_uint(32) loads number 165, 32-bit unsigned int from the transmitted message.

Next, we load 32 bits from the smart contract storage. We check that the loaded number matches the passed one, if not, we throw an exception. In our case, since we're passing in a mismatch, an exception should be thrown.

Now let's compile.

~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc 

Copy the resulting code to lottery-test-suite.fif, not forgetting to replace the last line.

Checking that the test passes:

~/TON/build/crypto/fift -s lottery-test-suite.fif

Here you can see the corresponding commit with current results.

Note that it is inconvenient to constantly copy the compiled smart contract code to the test file, so we will write a script that will write the code to a constant for us, and we will simply connect the compiled code to our tests using "include".

Create a file in the project folder build.sh with the following content.

#!/bin/bash

~/TON/build/crypto/func -SPA -R -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc

Let's make it executable.

chmod +x ./build.sh

Now, it is enough to run our script to compile the contract. But besides that, we need to write it into a constant code. So we will create a new file lotter-compiled-for-test.fif, which we will include in the file lottery-test-suite.fif.

Let's add code to sh script that will simply duplicate the compiled file in lotter-compiled-for-test.fif and change the last line in it.

# copy and change for test 
cp lottery-compiled.fif lottery-compiled-for-test.fif
sed '$d' lottery-compiled-for-test.fif > test.fif
rm lottery-compiled-for-test.fif
mv test.fif lottery-compiled-for-test.fif
echo -n "}END>s constant code" >> lottery-compiled-for-test.fif

Now, to check, run the resulting script and we will generate a file lottery-compiled-for-test.fifwhich we will include in our lottery-test-suite.fif

Π’ lottery-test-suite.fif remove the contract code and add the line "lottery-compiled-for-test.fif" include.

Run tests to see if they pass.

~/TON/build/crypto/fift -s lottery-test-suite.fif

Great, now to automate the launch of the tests, let's create a file test.sh, which will first execute build.shand then run tests.

touch test.sh
chmod +x test.sh

We write inside

./build.sh 

echo "nCompilation completedn"

export FIFTPATH=~/TON/ton/crypto/fift/lib
~/TON/build/crypto/fift -s lottery-test-suite.fif

We make the test.sh and run to make sure the tests work.

chmod +x ./test.sh
./test.sh

We check that the contract is compiled and the tests are executed.

Great, now on startup test.sh the tests will be compiled and run immediately. Here is a link to commit.

Okay, before we continue let's do one more thing for convenience.

Let's create a folder build where we will store the copied contract and its clone written into a constant lottery-compiled.fif, lottery-compiled-for-test.fif. We will also create a folder test where will the test file be stored lottery-test-suite.fif and potentially other support files. Link to related changes.

Let's continue developing the smart contract.

Next should be a test that checks that the message is received and the counter is updated in the store when we send the correct number. But we will do that later.

Now let's think about what data structure and what data should be stored in a smart contract.

I will describe everything that we have.

`seqno` 32-Ρ… Π±ΠΈΡ‚Π½ΠΎΠ΅ Ρ†Π΅Π»ΠΎΠ΅ ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ΅ число счСтчик. 

`pubkey` 256-Ρ‚ΠΈ Π±ΠΈΡ‚Π½ΠΎΠ΅ Ρ†Π΅Π»ΠΎΠ΅ ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ΅ число ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡, с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ³ΠΎ, ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΡΡ‚ΡŒ подпись ΠΎΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½Π½ΠΎΠ³ΠΎ ΠΈΠ·Π²Π½Π΅ сообщСния, ΠΎ Ρ‡Π΅ΠΌ Π½ΠΈΠΆΠ΅. 

`order_seqno` 32-Ρ… Π±ΠΈΡ‚Π½ΠΎΠ΅ Ρ†Π΅Π»ΠΎΠ΅ ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ΅ число Ρ…Ρ€Π°Π½ΠΈΡ‚ счСтчик количСства ставок. 

`number_of_wins` 32-Ρ… Π±ΠΈΡ‚Π½ΠΎΠ΅ Ρ†Π΅Π»ΠΎΠ΅ ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ΅ число Ρ…Ρ€Π°Π½ΠΈΡ‚  количСство ΠΏΠΎΠ±Π΅Π΄. 

`incoming_amount` Ρ‚ΠΈΠΏ Π΄Π°Π½Π½Ρ‹Ρ… Gram (ΠΏΠ΅Ρ€Π²Ρ‹Π΅ 4 Π±ΠΈΡ‚Π° ΠΎΡ‚Π²Π΅Ρ‡Π°Π΅Ρ‚ Π·Π° Π΄Π»ΠΈΠ½Ρƒ), Ρ…Ρ€Π°Π½ΠΈΡ‚ ΠΎΠ±Ρ‰Π΅Π΅ количСство Π³Ρ€Π°ΠΌΠΎΠ², ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π±Ρ‹Π»ΠΈ ΠΎΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½Ρ‹ Π½Π° ΠΊΠΎΠ½Ρ‚Ρ€Ρ‚Π°ΠΊΡ‚. 

`outgoing_amount` ΠΎΠ±Ρ‰Π΅Π΅ количСство Π³Ρ€Π°ΠΌΠΎΠ², ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ Π±Ρ‹Π»ΠΎ ΠΎΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ побСдитСлям. 

`owner_wc` Π½ΠΎΠΌΠ΅Ρ€ Π²ΠΎΡ€ΠΊΡ‡Π΅ΠΉΠ½Π°, 32-Ρ… Π±ΠΈΡ‚Π½ΠΎΠ΅ (Π² Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… мСстах написано, Ρ‡Ρ‚ΠΎ 8-ΠΌΠΈ Π±ΠΈΡ‚Π½ΠΎΠ΅) Ρ†Π΅Π»ΠΎΠ΅ число. Π’ Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚ всСго Π΄Π²Π° -1 ΠΈ 0. 

`owner_account_id` 256-Ρ‚ΠΈ Π±ΠΈΡ‚Π½ΠΎΠ΅ Ρ†Π΅Π»ΠΎΠ΅ ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ΅ число, адрСс ΠΊΠΎΠ½Ρ‚Ρ€Π°ΠΊΡ‚Π° Π² Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΌ Π²ΠΎΡ€ΠΊΡ‡Π΅ΠΉΠ½Π΅. 

`orders` пСрСмСнная Ρ‚ΠΈΠΏΠ° ΡΠ»ΠΎΠ²Π°Ρ€ΡŒ, Ρ…Ρ€Π°Π½ΠΈΡ‚ послСдниС Π΄Π²Π°Π΄Ρ†Π°Ρ‚ΡŒ ставок. 

The next step is to write two functions. Let's call the first pack_state(), which will pack the data for subsequent storage in the smart contract storage. The second one, we'll call unpack_state() will read and return data from storage.

_ pack_state(int seqno, int pubkey, int order_seqno, int number_of_wins, int incoming_amount, int outgoing_amount, int owner_wc, int owner_account_id, cell orders) inline_ref {
    return begin_cell()
            .store_uint(seqno, 32)
            .store_uint(pubkey, 256)
            .store_uint(order_seqno, 32)
            .store_uint(number_of_wins, 32)
            .store_grams(incoming_amount)
            .store_grams(outgoing_amount)
            .store_int(owner_wc, 32)
            .store_uint(owner_account_id, 256)
            .store_dict(orders)
            .end_cell();
}

_ unpack_state() inline_ref {
    var ds = begin_parse(get_data());
    var unpacked = (ds~load_uint(32), ds~load_uint(256), ds~load_uint(32), ds~load_uint(32), ds~load_grams(), ds~load_grams(), ds~load_int(32), ds~load_uint(256), ds~load_dict());
    ds.end_parse();
    return unpacked;
}

We add these two functions at the beginning of the smart contract. It turns out this intermediate result.

To save the data, you will need to call the built-in function set_data() and it will write data from pack_state() in the smart contract storage.

cell packed_state = pack_state(arg_1, .., arg_n); 
set_data(packed_state);

Now that we have convenient functions for writing and reading data, we can move on.

We need to check that the incoming message is signed by the owner of the contract (well, or another user who has access to the private key).

When we publish a smart contract, we can initialize it with the data we need in the store, which will be saved for future use. We will write the public key there so that we can verify that the signature of the incoming message was made by the corresponding private key.

Before proceeding, let's create a private key and write it to test/keys/owner.pk. To do this, start Fift in interactive mode and run four commands.

`newkeypair` гСнСрация ΠΏΡƒΠ±Π»ΠΈΡ‡Π½ΠΎΠ³ΠΎ ΠΈ ΠΏΡ€ΠΈΠ²Π°Ρ‚Π½ΠΎΠ³ΠΎ ΠΊΠ»ΡŽΡ‡Π° ΠΈ запись ΠΈΡ… Π² стСк. 

`drop` удалСния ΠΈΠ· стСка Π²Π΅Ρ€Ρ…Π½Π΅Π³ΠΎ элСмСнта (Π² Π΄Π°Π½Π½ΠΎΠΌ случаС ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡)  

`.s` просто ΠΏΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ‡Ρ‚ΠΎ Π»Π΅ΠΆΠΈΡ‚ Π² стСкС Π² Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚ 

`"owner.pk" B>file` запись ΠΏΡ€ΠΈΠ²Π°Ρ‚Π½ΠΎΠ³ΠΎ ΠΊΠ»ΡŽΡ‡Π° Π² Ρ„Π°ΠΉΠ» с ΠΈΠΌΠ΅Π½Π΅ΠΌ `owner.pk`. 

`bye` Π·Π°Π²Π΅Ρ€ΡˆΠ°Π΅Ρ‚ Ρ€Π°Π±ΠΎΡ‚Ρƒ с Fift. 

Let's create a folder keys inside a folder test and write the private key there.

mkdir test/keys
cd test/keys
~/TON/build/crypto/fift -i 
newkeypair
 ok
.s 
BYTES:128DB222CEB6CF5722021C3F21D4DF391CE6D5F70C874097E28D06FCE9FD6917 BYTES:DD0A81AAF5C07AAAA0C7772BB274E494E93BB0123AA1B29ECE7D42AE45184128 
drop 
 ok
"owner.pk" B>file
 ok
bye

We see the file in the current folder owner.pk.

We remove the public key from the stack, when we need it we can get it from the private one.

Now we need to write the signature verification. Let's start with a test. First, we read the private key from the file using the function file>B and write it to a variable owner_private_key, then using the function priv>pub convert the private key to public and write the result to owner_public_key.

variable owner_private_key
variable owner_public_key 

"./keys/owner.pk" file>B owner_private_key !
owner_private_key @ priv>pub owner_public_key !

We need both keys.

We initialize the smart contract storage with arbitrary data in the same sequence as in the function pack_state()and write to a variable storage.

variable owner_private_key
variable owner_public_key 
variable orders
variable owner_wc
variable owner_account_id

"./keys/owner.pk" file>B owner_private_key !
owner_private_key @ priv>pub owner_public_key !
dictnew orders !
0 owner_wc !
0 owner_account_id !

<b 0 32 u, owner_public_key @ B, 0 32 u, 0 32 u, 0 Gram, 0 Gram, owner_wc @ 32 i, owner_account_id @ 256 u,  orders @ dict, b> storage !

Next, we will compose a signed message, it will contain only the signature and the value of the counter.

First, we create the data that we want to transfer, then we sign it with a private key, and finally we form a signed message.

variable message_to_sign
variable message_to_send
variable signature
<b 0 32 u, b> message_to_sign !
message_to_sign @ hashu owner_private_key @ ed25519_sign_uint signature !
<b signature @ B, 0 32 u, b> <s  message_to_send !  

As a result, the message that we will send to the smart contract is written to the variable message_to_send, about functions hashu, ed25519_sign_uint you can read in the Fift documentation.

And we call again to run the test.

message_to_send @ 
recv_external 
code 
storage @
c7
runvmctx

Like this the test file should look like at this stage.

Let's run the test and it will fail, so we will change the smart contract so that it can receive messages of this format and verify the signature.

First, we read 512 bits of the signature from the message and write it to a variable, then we read 32 bits of the counter variable.

Since we have a function for reading data from the smart contract storage, we will use it.

Further check of the counter transferred with storage and verification of the signature. If something does not match, then we throw an exception with the corresponding code.

var signature = in_msg~load_bits(512);
var message = in_msg;
int msg_seqno = message~load_uint(32);
(int stored_seqno, int pubkey, int order_seqno, int number_of_wins, int incoming_amount, int outgoing_amount, int owner_wc, int owner_account_id, cell orders) = unpack_state();
throw_unless(33, msg_seqno == stored_seqno);
throw_unless(34, check_signature(slice_hash(in_msg), signature, pubkey));

Relevant commit here.

Let's run the tests and see that the second test fails. For two reasons, the lack of bits in the message and the lack of bits in the storage, so the code crashes when parsing. We need to add the signature of the message we are sending and copy the storage from the last test.

In the second test, we will add a message signature and change the storage of the smart contract. Like this looks like a file with tests at the moment.

Let's write the fourth test, in which we will send a message signed by someone else's private key. Let's create another private key and save it to a file not-owner.pk. Let's sign the message with this private key. Let's run the tests and make sure all the tests pass. commit at this moment.

Now we can finally move on to implementing the smart contract logic.
Π’ recv_external() we will receive two types of messages.

Since our contract will accumulate the losses of the players, this money must be transferred to the creator of the lottery. The wallet address of the lottery creator is written to the vault when the contract is created.

Just in case, we need the ability to change the address to which to send the gram of the losers. We should also be able to send grams from the lottery to the address of the owner.

Let's start with the first one. Let's first write a test that will check that after sending the message, the smart contract saved the new address in the storage. Note that in the message, in addition to the counter and the new address, we also send action 7-bit non-negative integer, depending on it, we will choose how to process the message in the smart contract.

<b 0 32 u, 1 @ 7 u, new_owner_wc @  32 i, new_owner_account_id @ 256 u, b> message_to_sign !

In the test, you can see how the de-realization of the smart contract storage takes place storage in Fift. Variable deserialization is described in the Fift documentation.

Link to the commit with added test.

Let's run the test and see if it fails. Now let's add logic to change the address of the lottery owner.

In the smart contract, we continue to parse message, read in action. Recall that we have two action: change address and send grams.

Then we read the new address of the contract owner and save it to the storage.
We run the tests and see that the third test fails. Crashes due to the fact that the contract now additionally parses 7 bits from the message, which are missing in the test. Let's add a non-existent message to the message action. Let's run the tests and see that everything passes. Here commit for changes. Great.

Now let's write the logic for sending the specified number of grams to the previously saved address.

Let's write a test first. We will write two tests, one when the balance is not enough, the second when everything should pass successfully. Tests can be viewed in this commit.

Now let's add some code. First, let's write two helper methods. The first get method is to find out the current balance of the smart contract.

int balance() inline_ref method_id {
    return get_balance().pair_first();
}

And the second one is for sending grams to another smart contract. I completely copied this method from another smart contract.

() send_grams(int wc, int addr, int grams) impure {
    ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
    cell msg = begin_cell()
    ;;  .store_uint(0, 1) ;; 0 <= format indicator int_msg_info$0 
    ;;  .store_uint(1, 1) ;; 1 <= ihr disabled
    ;;  .store_uint(1, 1) ;; 1 <= bounce = true
    ;;  .store_uint(0, 1) ;; 0 <= bounced = false
    ;;  .store_uint(4, 5)  ;; 00100 <= address flags, anycast = false, 8-bit workchain
        .store_uint (196, 9)
        .store_int(wc, 8)
        .store_uint(addr, 256)
        .store_grams(grams)
        .store_uint(0, 107) ;; 106 zeroes +  0 as an indicator that there is no cell with the data.
        .end_cell(); 
    send_raw_message(msg, 3); ;; mode, 2 for ignoring errors, 1 for sender pays fees, 64 for returning inbound message value
}

Let's add these two methods to the smart contract and write the logic. First, we parse the number of grams from the message. Then we check the balance, if not enough we throw an exception. If everything is fine, then we send grams to the saved address and update the counter.

int amount_to_send = message~load_grams();
throw_if(36, amount_to_send + 500000000 > balance());
accept_message();
send_grams(owner_wc, owner_account_id, amount_to_send);
set_data(pack_state(stored_seqno + 1, pubkey, order_seqno, number_of_wins, incoming_amount, outgoing_amount, owner_wc, owner_account_id, orders));

Like this looks like a smart contract at the moment. Let's run the tests and make sure they pass.

By the way, a commission is debited for a processed message from a smart contract every time. In order for the smart contract messages to execute the request, after basic checks, you need to call accept_message().

Now let's deal with internal messages. In fact, we will only accept grams and send back a double amount to the player if he wins and a third to the owner if he loses.

Let's write a simple test first. To do this, we need a test address of the smart contract from which we send grams to the smart contract.

The smart contract address consists of two numbers, a 32-bit integer responsible for the workchain and a 256-bit non-negative integer unique account number in this workchain. For example, -1 and 12345, this address will be saved to a file.

I copied the address saving function from TonUtil.fif.

// ( wc addr fname -- )  Save address to file in 36-byte format
{ -rot 256 u>B swap 32 i>B B+ swap B>file } : save-address

Let's take a look at how the function works, this will give an understanding of how Fift works. Launch Fift in interactive mode.

~/TON/build/crypto/fift -i 

First we push -1, 12345 and the name of the future file "sender.addr" onto the stack:

-1 12345 "sender.addr" 

The next step is to execute the function -rot, which shifts the stack so that the unique number of the smart contract is at the top of the stack:

"sender.addr" -1 12345

256 u>B converts a 256-bit non-negative integer to bytes.

"sender.addr" -1 BYTES:0000000000000000000000000000000000000000000000000000000000003039

swap swaps the top two elements of the stack.

"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 -1

32 i>B converts a 32-bit integer to bytes.

"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 BYTES:FFFFFFFF

B+ connects two sequences of bytes.

 "sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF

Again swap.

BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF "sender.addr" 

And finally, the bytes are written to the file B>file. After that, our stack is empty. Stopping Fift. File created in current folder sender.addr. Move the file to the created folder test/addresses/.

Let's write a simple test that will send grams to a smart contract. Here is the commit.

Now let's deal with the logic of the lottery.

The first thing we do is check the message bounced or not if bounced, then we ignore it. bounced means that the contract will return grams if some error occurs. We will not return grams if an error occurs, we will not.

We check if the balance is less than half a gram, then we simply accept the message and ignore it.

Next, we parse the address of the smart contract from which the message came.

We read the data from the storage and then delete the old bets from the history if there are more than twenty of them. For convenience, I wrote three additional functions pack_order(), unpack_order(), remove_old_orders().

Then we look if the balance is not enough for the payout, then we consider that this is not a bet, but a replenishment and save the replenishment in orders.

Then finally the essence of the smart contract.

First, if the player has lost, we save it to the history of bets and if the amount is more than 3 grams, we send 1/3 to the owner of the smart contract.

If the player won, then we send a double amount to the player's address and then save the information about the bet in the history.

() recv_internal(int order_amount, cell in_msg_cell, slice in_msg) impure {
    var cs = in_msg_cell.begin_parse();
    int flags = cs~load_uint(4);  ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
    if (flags & 1) { ;; ignore bounced
        return ();
    }
    if (order_amount < 500000000) { ;; just receive grams without changing state 
        return ();
    }
    slice src_addr_slice = cs~load_msg_addr();
    (int src_wc, int src_addr) = parse_std_addr(src_addr_slice);
    (int stored_seqno, int pubkey, int order_seqno, int number_of_wins, int incoming_amount, int outgoing_amount, int owner_wc, int owner_account_id, cell orders) = unpack_state();
    orders = remove_old_orders(orders, order_seqno);
    if (balance() < 2 * order_amount + 500000000) { ;; not enough grams to pay the bet back, so this is re-fill
        builder order = pack_order(order_seqno, 1, now(), order_amount, src_wc, src_addr);
        orders~udict_set_builder(32, order_seqno, order);
        set_data(pack_state(stored_seqno, pubkey, order_seqno + 1, number_of_wins, incoming_amount + order_amount, outgoing_amount, owner_wc, owner_account_id, orders));
        return ();
    }
    if (rand(10) >= 4) {
        builder order = pack_order(order_seqno, 3, now(), order_amount, src_wc, src_addr);
        orders~udict_set_builder(32, order_seqno, order);
        set_data(pack_state(stored_seqno, pubkey, order_seqno + 1, number_of_wins, incoming_amount + order_amount, outgoing_amount, owner_wc, owner_account_id, orders));
        if (order_amount > 3000000000) {
            send_grams(owner_wc, owner_account_id, order_amount / 3);
        }
        return ();
    }
    send_grams(src_wc, src_addr, 2 * order_amount);
    builder order = pack_order(order_seqno, 2, now(), order_amount, src_wc, src_addr);
    orders~udict_set_builder(32, order_seqno, order);
    set_data(pack_state(stored_seqno, pubkey, order_seqno + 1, number_of_wins + 1, incoming_amount, outgoing_amount + 2 * order_amount, owner_wc, owner_account_id, orders));
}

That's it. Relevant commit.

Now it remains simple, let's make get methods so that we can get information about the state of the contract from the outside world (in fact, read the data from the smart contract storage).

Add Get Methods. We will write about how to receive information about a smart contract below.

I also forgot to add the code that will process the very first request that occurs when the smart contract is published. Relevant commit. And further corrected bug with sending 1/3 of the amount to the owner's account.

The next step is to publish the smart contract. Let's create a folder requests.

I took the publication code as a basis simple-wallet-code.fc ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ You can find in the official repository.

What is worth paying attention to. We form the storage of the smart contract and the entry message. After this, the address of the smart contract is generated, that is, the address is known even before publication in TON. Then you need to send a few grams to this address, and only after that you need to send a file with the smart contract itself, since the network takes a commission for storing the smart contract and operations in it (validators that store and execute smart contracts). The code can be viewed here.

Next, we execute the publication code and get lottery-query.boc file and smart contract address.

~/TON/build/crypto/fift -s requests/new-lottery.fif 0

Don't forget to save the generated files: lottery-query.boc, lottery.addr, lottery.pk.

Among other things, in the execution logs we will see the address of the smart contract.

new wallet address = 0:044910149dbeaf8eadbb2b28722e7d6a2dc6e264ec2f1d9bebd6fb209079bc2a 
(Saving address to file lottery.addr)
Non-bounceable address (for init): 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
Bounceable address (for later access): kQAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KpFY

For the sake of interest, we will make a request to TON

$ ./lite-client/lite-client -C ton-lite-client-test1.config.json 
getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd

And we will see that the account with this address is empty.

account state is empty

We send to the address 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd 2 Gram and after a few seconds we execute the same command. To send grams I use official wallet, and you can ask for test grams from someone in the chat, which I will talk about at the end of the article.

> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd

Looks for an uninitialized (state:account_uninit) a smart contract with this address and a balance of 1 nanograms.

account state is (account
  addr:(addr_std
    anycast:nothing workchain_id:0 address:x044910149DBEAF8EADBB2B28722E7D6A2DC6E264EC2F1D9BEBD6FB209079BC2A)
  storage_stat:(storage_info
    used:(storage_used
      cells:(var_uint len:1 value:1)
      bits:(var_uint len:1 value:103)
      public_cells:(var_uint len:0 value:0)) last_paid:1583257959
    due_payment:nothing)
  storage:(account_storage last_trans_lt:3825478000002
    balance:(currencies
      grams:(nanograms
        amount:(var_uint len:4 value:2000000000))
      other:(extra_currencies
        dict:hme_empty))
    state:account_uninit))
x{C00044910149DBEAF8EADBB2B28722E7D6A2DC6E264EC2F1D9BEBD6FB209079BC2A20259C2F2F4CB3800000DEAC10776091DCD650004_}
last transaction lt = 3825478000001 hash = B043616AE016682699477FFF01E6E903878CDFD6846042BA1BFC64775E7AC6C4
account balance is 2000000000ng

Now let's publish the smart contract. Let's start lite-client and execute.

> sendfile lottery-query.boc
[ 1][t 2][1583008371.631410122][lite-client.cpp:966][!testnode] sending query from file lottery-query.boc
[ 3][t 1][1583008371.828550100][lite-client.cpp:976][!query]    external message status is 1 

Let's check that the contract is published.

> last
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd

Among other things, we get

  storage:(account_storage last_trans_lt:3825499000002
    balance:(currencies
      grams:(nanograms
        amount:(var_uint len:4 value:1987150999))
      other:(extra_currencies
        dict:hme_empty))
    state:(account_active

We see that account_active.

Relevant commit with changes here.

Now let's create requests to interact with the smart contract.

More precisely, we will leave the first one for changing the address as an independent work, and we will do the second one for sending grams to the owner’s address. In fact, we will need to do the same as in the test for sending grams.

This is the message we will send to the smart contract, where msg_seqno 165, action 2 and 9.5 grams to send.

<b 165 32 u, 2 7 u, 9500000000 Gram, b>

Don't forget to sign the message with your private key lottery.pk, which was generated earlier when creating a smart contract. Here is the relevant commit.

Getting information from a smart contract using get methods

Now let's look at how to run smart contract get methods.

Run lite-client and run the get methods we wrote.

$ ./lite-client/lite-client -C ton-lite-client-test1.config.json
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd balance
arguments:  [ 104128 ] 
result:  [ 64633878952 ] 
...

Π’ result contains the value returned by the function balance() from our smart contract.
We will do the same for several more methods.

> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd get_seqno
...
arguments:  [ 77871 ] 
result:  [ 1 ] 

We will request the history of rates.

> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd get_orders
...
arguments:  [ 67442 ] 
result:  [ ([0 1 1583258284 10000000000 0 74649920601963823558742197308127565167945016780694342660493511643532213172308] [1 3 1583258347 4000000000 0 74649920601963823558742197308127565167945016780694342660493511643532213172308] [2 1 1583259901 50000000000 0 74649920601963823558742197308127565167945016780694342660493511643532213172308]) ] 

We will use lite-client and get methods to display information about the smart contract on the site.

Display smart contract data on the website

I have written a simple Python website to show smart contract data in a convenient way. Here I will not dwell on it in detail and publish the site in one commit.

Requests to TON are made from Python through lite-client. For convenience, the site is packaged in Docker and published on Google Cloud. Link.

Trying

Now let's try to send grams there for replenishment from purse. We will send 40 grams. And let's make a couple of bets for clarity. We see that the site shows the history of bets, the current winning percentage and other useful information.

We seethat we won the first, lost the second.

Afterword

The article turned out to be much longer than I expected, maybe it could have been shorter, or maybe just for a person who knows nothing about TON and wants to write and publish a smart contract that is not the easiest to interact with. Perhaps some things could be explained more simply.

Perhaps some points in the implementation could have been done more efficiently and elegantly, but then the preparation of the article would have taken even more time. It is also possible that I made a mistake somewhere or did not understand something, so if you are doing something serious, you need to rely on the official documentation or the official repository with the TON code.

It should be noted that since TON itself is still in the active development stage, there may be changes that will break any of the steps in this article (which happened while I was writing, I have already corrected it), but the general approach is unlikely to change.

I will not talk about the future of TON. Perhaps the platform will become something big and we should take the time to study it and carve a niche with our products now.

There is also Libra from Facebook, which has a larger potential audience of users than TON. I know almost nothing about Libra, judging by the activity forum there is much more activity than in the TON community. Although the developers and the TON community are more like an underground, which is also cool.

references

  1. Official documentation on TON: https://test.ton.org
  2. Official TON repository: https://github.com/ton-blockchain/ton
  3. Official wallet for different platforms: https://wallet.ton.org
  4. The smart contract repository from this article: https://github.com/raiym/astonished
  5. Link to the smart contract website: https://ton-lottery.appspot.com
  6. Repository for extension for Visual Studio Code for FunC: https://github.com/raiym/func-visual-studio-plugin
  7. Chat about TON in Telegram, which helped a lot to figure it out at the initial stage. I think it will not be a mistake if I say that there is everyone who wrote something for TON. You can also ask for test grams there. https://t.me/tondev_ru
  8. Another chat about TON in which I found useful information: https://t.me/TONgramDev
  9. The first stage of the competition: https://contest.com/blockchain
  10. Second round of the competition: https://contest.com/blockchain-2

Source: habr.com

Add a comment