How to Write and Publish a Smart Contract in TON
What Is This Article About?
In this article, I will share my experience participating in the first (of two) Telegram blockchain competition, not winning a prize, and deciding to document my experience so it doesn't fade away and possibly help someone else.
Since I didn't want to write abstract code, but instead create something functional, I wrote a smart contract for an instant lottery and a website that displays the smart contract data directly from TON without using intermediate storage.
This article will be useful for those who want to create their first smart contract in TON but don't know where to start.
Using the lottery as an example, I will guide you from setting up the environment to publishing the smart contract, interacting with it, and writing a website to retrieve and display the data.
About Participating in the Competition
In October of last year, Telegram announced a blockchain competition with new languages Fift and FunC. Participants were required to choose any of the five proposed smart contracts. I thought it would be great to engage in something unusual, learn the language, and create something, even if in the future I don't have to write anything else. Plus, the topic is always current.
I should mention that I had no prior experience in smart contract development.
I planned to participate until the end, as long as I could, and then write a review article, but I failed right at the beginning. I with multi-signature on FunC and it generally worked. I used as a basis .
At that moment, I thought this would definitely be enough to secure at least some prize. In the end, about 40 out of 60 participants were prize-winners, and I was not among them. Overall, it's not a big deal, but one thing bothered me. At the time of the result announcement, there was no review with tests done for my contract; I asked the participants in the chat if there was anyone else who didn't have it, and no one did.
Apparently noticing my messages two days later, the judges published a comment, and I still did not understand whether they accidentally overlooked my smart contract during the judging or simply considered it so poor that it did not require comment. I asked a question on the page, but received no response. Although who judged is not a secret, I considered sending personal messages unnecessary.
A considerable amount of time was spent understanding, so it was decided to write an article. Since there isn't much information yet, the article will help save time for all interested parties.
The Concept of Smart Contracts in TON
Before writing anything, it is essential to understand how to approach this topic. Therefore, I will explain which components the system consists of — specifically, which components need to be known to write at least some functioning contract.
We will focus on writing a smart contract and working with TON Virtual Machine (TVM), Fift and FunC, so the article resembles a description of developing a regular program. We will not dwell on how the platform itself works here.
In general, regarding how TVM and the language Fift have good official documentation. During my participation in the competition and now while writing the current contract, I often referred to it.
The primary language used to write smart contracts is FunC. There is currently no documentation on it, so to write something, one must study examples of smart contracts from the official repository and the language's implementation there, plus one can look at examples of smart contracts from the past two competitions. Links will be provided at the end of the article.
Suppose we have already written a smart contract in FunC, after which we compile the code into Fift assembly.
The compiled smart contract needs to be published. For this, a function should be written in Fift, which will accept the smart contract code and some other parameters as input, and the output will be a file with the extension .boc (which stands for "bag of cells"), and, depending on how we write it, a private key and an address generated based on the smart contract code. Grams can already be sent to the address of the smart contract that has not yet been published.
To publish a smart contract in TON, the obtained .boc The file will need to be sent to the blockchain using a light client (about which more 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 the smart contract by sending it messages from outside (for example, through the light client) or from inside (for example, one smart contract sends a message to another within TON).
Once we understood how to publish the code, things get easier. We roughly know what we want to write and how our program will work. During writing, we look for how this has already been implemented in existing smart contracts, or we check the implementation code. Fift and FunC in the official repository, or we look in the official documentation.
I often searched by keywords in the Telegram chat where all the participants of the competition and Telegram staff gathered; it turned out that during the competition everyone was indeed there and started discussing Fift and FunC. The link is at the end of the article.
It's time to move from theory to practice.
Preparing the environment for working with TON
Everything described in the article was done on MacOS, and I double-checked it on a clean Ubuntu 18.04 LTS on Docker.
The first thing to do is to download and install lite-client with which you can send requests to TON.
The instructions on the official website describe the installation process quite clearly and comprehensively, skipping some details. Here we follow the instructions while 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 After all dependencies are installed, you can install lite-client, Fift, FunC.
First, we clone the TON repository along with dependencies. For convenience, we will do everything in the folder ~\/TON.
cd ~\/TON
git clone https:\/\/github.com\/ton-blockchain\/ton.git
cd .\/ton
git submodule update --init --recursiveThe repository also contains implementations Fift and FunC.
Now we are ready to build the project. The repository code is cloned into the folder ~\/TON\/ton. In ~\/TON we create the folder build and build the project in it.
mkdir ~\/TON\/build
cd ~\/TON\/build
cmake ..\/tonSince we are going to write a smart contract, we need not only lite-client, but also Fift with FunC, so we compile everything. It's not a fast process, so we wait.
cmake --build . --target lite-client
cmake --build . --target fift
cmake --build . --target funcNext, download the configuration file that contains data about the node to which lite-client you will connect.
wget https://test.ton.org/ton-lite-client-test1.config.jsonLet's make the first requests in TON
Now let's run lite-client.
cd ~/TON/build
./lite-client/lite-client -C ton-lite-client-test1.config.jsonIf the build was successful, you will see the log connecting the light client to the node after launching.
[ 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 execute the command help and see what commands are available.
helpLet'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 Load a serialized message from and send it to server
getaccount [] Loads the most recent state of specified account; is in [:] format
runmethod [] ... Runs GET method of account with specified parameterslast gets the last created block from the server.
sendfile sends a file with a message to TON; this command is used to publish the smart contract and requests to it.
getaccount loads the current state of the smart contract at the specified address.
runmethod [] runs the get-methods of the smart contract. Now we are ready to write the contract itself.
Implementation
Idea
As mentioned earlier, the smart contract we are writing is a lottery.
This is not a lottery where you have to buy a ticket and wait an hour, a day, or a month, but an instant one, where the user transfers an amount to the contract address N grams, and instantly receives back 2 * N grams or loses. We will set the probability of winning to about 40%. If the grams for the payout are insufficient, we will consider the transaction to be a replenishment.
It is also important for bets to be visible in real time and in a convenient format so that the user can immediately understand whether they won or lost. Therefore, we need to create a website that will show bets and results directly from TON.
Writing the Smart Contract
For convenience, I have created code highlighting for FunC; the plugin can be found and installed in the Visual Studio Code search. If you want to add something, I have made the plugin publicly available. Also, previously someone created a plugin for working with Fift, which can also be found and installed in VSC.
Let's immediately create a repository where we will commit the intermediate results.
To make our lives easier, we will write a smart contract and test it locally until it is ready. Only after that will we publish it to TON.
The smart contract has two external methods that can be called. The first one, 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 formulate a message ourselves and send it through the lite-client. The second one, recv_internal() is when another contract within TON calls our contract. In both cases, parameters can be passed into the function.
Let's start with a simple example that will work if published, but has no functional load in it.
() recv_internal(slice in_msg) impure {
;; TODO: implementation
}
() recv_external(slice in_msg) impure {
;; TODO: implementation
}Here we need to clarify what is meant by slice. All data stored in the TON Blockchain is a collection of TVM cell or simply a cell, in such a cell you can store up to 1023 bits of data and up to 4 links to other cells.
A TVM cell slice or slice is part of an existing cell used for parsing it; it will become clearer later. The main point for us is that in the smart contract we can pass slice and depending on the type of message, process the data in recv_external() or recv_internal().
impure — a keyword indicating that the function modifies the smart contract data.
We will save the contract code in lottery-code.fc and compile it.
~\/TON\/build\/crypto\/func -APSR -o lottery-compiled.fif ~\/TON\/ton\/crypto\/smartcont\/stdlib.fc .\/lottery-code.fc You can check the value of the flags using the command
~\/TON\/build\/crypto\/func -helpWe have a compiled Fift assembly 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>cIt can be run locally; for this, we will prepare the environment.
Note that the first line includes Asm.fif, this is code written in Fift for the Fift assembler.
Since we want to run and test the smart contract locally, we will create a file lottery-test-suite.fif and copy the compiled code into it, replacing the last line that writes the smart contract code into the constant code, to later 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
It's clear for now, let's add to the same file the code that we will use to run TVM.
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()In c7 We are recording the context, that is, the data with which TVM will be launched (or the state of the network). Also during the contest, one of the developers showed how to create c7 and I copied it. In this article we may need to change rand_seed as it affects the generation of random numbers, and if not changed, the same number will be returned every time.
recv_internal and recv_external The constants with values 0 and -1 will be responsible for invoking the corresponding functions in the smart contract.
Now we are ready to create the first test for our empty smart contract. For clarity, we will add all tests in this same file for now lottery-test-suite.fif.
Let's create a variable storage and write an empty value to it cell, this will be the storage of the smart contract.
message this is the message that we will pass to the smart contract from the outside. We will also make it empty for now.
variable storage
<b b> storage !
variable message
<b b> message ! After we have prepared the constants and variables, we run TVM using the command runvmctx and pass the created parameters as input.
message @
recv_external
code
storage @
c7
runvmctx As a result, we will get Now we can run the resulting code. Fift.
export FIFTPATH=~\/TON\/ton\/crypto\/fift\/lib // execute once for convenience ~\/TON\/build\/crypto\/fift -s lottery-test-suite.fif
The program should run 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 wrote the first working version of the smart contract.Now we need to add functionality. First, we will deal with messages coming from the outside world into
The developer chooses the format of the message that the contract can accept. recv_external()
But usually,
first of all, we want to protect our contract from the outside world and ensure that only the owner of the contract can send external messages to it.
- First of all, we want to shield our contract from the outside world and ensure that only the contract owner can send external messages to it.
- Secondly, when we send a valid message to TON, we want it to happen exactly once, and if the same message is sent again, the smart contract should reject it.
Therefore, almost every contract addresses these two issues, as our contract accepts external messages; we also need to take this into account.
We will proceed in reverse order. First, we will solve the replay issue: if the contract has already received and processed such a message, it will not execute it a second time. Then, we will resolve the issue of restricting which specific individuals can send messages to the smart contract.
There are different ways to solve the problem of duplicate messages. Here’s how we will do it. In the smart contract, we will initialize a counter for received messages starting at 0. In every message to the smart contract, we will include the current counter value. If the counter value in the message does not match the value in the smart contract, we do not process it; if it does match, we process it and increment the counter in the smart contract by 1.
Returning to lottery-test-suite.fif and adding the second test. We will send an incorrect number, and the code should throw an exception. For example, let’s say the contract data stores 166, but we 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 it.
~\/TON\/build\/crypto\/fift -s lottery-test-suite.fif And we will see that the test runs 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 passedAt this stage, lottery-test-suite.fif should look like .
Now let's add the counter logic in 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);
}In slice in_msg contains the message we are sending.
The first thing we do is check if there is data in the message; if not, we simply exit.
Next, we parse the message. in_msg~load_uint(32) loads the 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 one provided; if not, we throw an exception. In our case, since we are passing 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 We will copy the resulting code into lottery-test-suite.fif, remembering to replace the last line.
We check that the test passes:
~\/TON\/build\/crypto\/fift -s lottery-test-suite.fifyou can view the corresponding commit with the current results.
Note that constantly copying the compiled smart contract code into the test file is inconvenient, so we will write a script that will record the code in a constant for us, and we will simply include the compiled code in our tests using "include".
In the project folder, we will create a file 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.fcLet's make it executable.
chmod +x ./build.shNow all we need to do is run our script to compile the contract. But in addition, we need to record it in a constant code. Therefore, we will create a new file lotter-compiled-for-test.fif, which we will include in the file lottery-test-suite.fif.
We will add to the sh script the code that will simply duplicate the compiled file into 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.fifNow, to check, we will run the resulting script, and it will generate the file lottery-compiled-for-test.fif, which we will include in our lottery-test-suite.fif
In lottery-test-suite.fif remove the contract code and add the line "lottery-compiled-for-test.fif" include.
We run the tests to check that they pass.
~\/TON\/build\/crypto\/fift -s lottery-test-suite.fifGreat, now to automate the test run, we will create a file test.sh, which will first execute build.sh, and then run the tests.
touch test.sh
chmod +x test.shInside we write
./build.sh
echo "nCompilation completedn"
export FIFTPATH=~/TON/ton/crypto/fift/lib
~/TON/build/crypto/fift -s lottery-test-suite.fifLet's make test.sh and run it to ensure the tests work.
chmod +x ./test.sh
./test.shWe check that the contract is compiled and the tests are executed.
Great, now on running test.sh compilation and test execution will occur simultaneously. Here is the link to .
Okay, before we proceed, let's do one more thing for convenience.
We'll create a folder build where we will store the compiled contract and its clone recorded in a constant lottery-compiled.fif, lottery-compiled-for-test.fif. We will also create a folder test where the test files will be stored lottery-test-suite.fif and potentially other auxiliary files. .
Let's continue with the development of the smart contract.
Next, there should be a test to check that the message is received and the counter is updated in the storage when we send the correct number. But we'll do that later.
Now let's consider what data structure and which data needs to be stored in the smart contract.
I will describe everything that we store.
`seqno` is a 32-bit unsigned integer counter.
`pubkey` is a 256-bit unsigned integer public key, which we will use to verify the signature of the message sent from outside, as explained below.
`order_seqno` is a 32-bit unsigned integer that stores the counter of the number of bets.
`number_of_wins` is a 32-bit unsigned integer storing the number of wins.
`incoming_amount` is of type Gram (the first 4 bits represent the length), storing the total number of grams that were sent to the contract.
`outgoing_amount` is the total number of grams that were sent to the winners.
`owner_wc` is the workchain number, a 32-bit integer (in some places it is noted as an 8-bit integer). Currently, there are only two: -1 and 0.
`owner_account_id` is a 256-bit unsigned integer, the address of the contract in the current workchain.
`orders` is a variable of dictionary type, storing the last twenty bets. Next, we need to write two functions. We will name the first one pack_state(), which will pack data for subsequent storage in the smart contract. The second one, we will 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 will result in an intermediate result.
To save the data, we will need to call the embedded function set_data() and it will write the data from pack_state() to the storage of the smart contract.
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 verify that the incoming message from outside is signed by the contract owner (or another user who has access to the private key).
When we publish the smart contract, we can initialize it with the necessary data in the storage that will be retained for future use. We'll record the public key there to verify that the signature of the incoming message was made with the corresponding private key.
Before we proceed, let's create a private key and write it to test/keys/owner.pk. To do this, we'll run Fift in interactive mode and execute four commands.
`newkeypair` generates the public and private keys and writes them to the stack.
`drop` removes the top element from the stack (in this case, the public key)
`.s` simply shows what is currently in the stack
`"owner.pk" B>file` writes the private key to a file named `owner.pk`.
`bye` ends the session with Fift. We'll create a folder keys inside the folder test and there we will write the private key.
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
byeWe see a file in the current folder called owner.pk.
We remove the public key from the stack, and when needed we can retrieve it from the private one.
Now we need to write a 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 the variable owner_private_key, then using the function priv>pub we convert the private key to public and record the result in 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 !Both keys will be needed.
We’ll initialize the smart contract storage with arbitrary data in the same sequence as in the function pack_state()and write it to the 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, gram, owner_wc i, owner_account_id 256 orders dict, b> storage !Next, we will compose the signed message, which will contain only the signature and the counter value.
First, we create the data we want to send, then we sign it with the private key, and finally we form the 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 ! As a result, the message that we will send to the smart contract is recorded in the variable message_to_send, about the functions hashu, ed25519_sign_uint can be read .
And to run the test again, we call.
message_to_send @
recv_external
code
storage @
c7
runvmctxThe file with tests should look like this at this stage.
We'll run the test and it will fail, so we'll change the smart contract to be able to receive messages in this format and verify the signature.
First, we read the 512-bit signature from the message and store it in a variable, then we read the 32-bit counter variable.
Since we have a function for reading data from the smart contract's storage, we will use it.
Next, we check the counter passed with the storage and verify the signature. If anything doesn't match, 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));Corresponding commit .
We'll run the tests and see that the second test fails. For two reasons: insufficient bits in the message and insufficient bits in the storage, so the code fails during parsing. We need to add the signature of the message we send and copy the storage from the last test.
In the second test, we'll add the message signature and modify the smart contract's storage. The file with tests currently looks like this.
Let's write a fourth test, in which we will send a message signed with someone else's private key. We will create another private key and save it to a file. not-owner.pk. We will sign the message with this private key. We'll run the tests and make sure all tests pass. at the moment.
Now we can finally move on to implementing the logic of the smart contract.
In recv_external() We will accept two types of messages.
Since our contract will accumulate players' losses, this money needs to be transferred to the lottery creator. The wallet address of the lottery creator is recorded in storage when the contract is created.
For safety, we need the ability to change the address to which we send the grams of the losers. We should also be able to send grams from the lottery to the owner's address.
Let's start with the first one. We'll write a test that checks that after sending a message, the smart contract saved the new address in storage. Note that, in addition to the counter and the new address, we are also passing action A 7-bit non-negative integer, depending on this, we will choose how to process the message in the smart contract.
<b 0 32 u, 1 @ 7 new_owner_wc i, new_owner_account_id 256 b> message_to_sign !In the test, you can see how the deserialization of the smart contract storage occurs. storage In Fift. Deserialization of variables is described in the Fift documentation.
with the addition of a test.
We run the test and make sure it fails. Now let's add the logic for changing the lottery owner's address.
In the smart contract, we continue parsing message, we read in action. Let's remember that we will have two action: changing the address and sending grams.
Then we read the new owner's address and store it in the storage.
We run the tests and see that the third test fails. It fails because the contract now additionally parses 7 bits from the message, which are missing in the test. Let's add a non-existent action. We will run the tests and see that all pass. commit for changes. Great.
Now let's write the logic to send the specified number of grams to the previously saved address.
First, we will write the test. We will write two tests: one when the balance is insufficient and the second when everything should succeed. The tests can be viewed .
Now let’s finish the code. First, we will write two helper methods. The first gets the current balance of the smart contract.
int balance() inline_ref method_id {
return get_balance().pair_first();
}And the second method sends 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. Next, we check the balance, and if it's insufficient, we throw an exception. If all is well, we send the 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));this is what the smart contract looks like right now. Let's run some tests and make sure they pass.
By the way, a fee is charged for each processed message by the smart contract. To process the message, after basic checks, we need to call accept_message().
Now let’s handle the internal messages. Essentially, we will only accept grams and send back double the amount to the player if they win, and a third to the owner if they lose.
First, let’s write a simple test. For that, we will need a test address of the smart contract from which we will send grams to the smart contract.
The smart contract address consists of two numbers: a 32-bit integer for workchain and a 256-bit non-negative unique account number in that workchain. For example, -1 and 12345; we will save this address to a file.
I copied the function for saving the address from .
// ( wc addr fname -- ) Save address to file in 36-byte format
{ -rot 256 u>B swap 32 i>B B+ swap B>file } : save-addressLet’s break down how the function works, this will give us an understanding of how Fift operates. We’ll run 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 executes the function -rot, which rotates the stack so that the unique smart contract number is at the top of the stack:
"sender.addr" -1 12345256 u>B converts the 256-bit non-negative integer to bytes.
"sender.addr" -1 BYTES:0000000000000000000000000000000000000000000000000000000000003039swap swaps the top two elements of the stack.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 -132 i>B converts the 32-bit integer to bytes.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 BYTES:FFFFFFFFB+ concatenates two byte sequences.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFFAgain swap.
BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF "sender.addr" And finally, it writes the bytes to the file B>file. After this, our stack is empty. We stop Fift. A file named sender.addr. Let’s move this file to the created folder test\/addresses\/.
We will write a simple test that will send grams to the smart contract. .
Now let’s get into the lottery logic.
The first thing we do is check whether the message bounced or not, if bounced, we ignore it. bounced means that the contract will return grams if any error occurs. We will not return grams if an error suddenly arises.
We check the balance; if it is less than half a gram, 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 remove old bets from the history if there are more than twenty. For convenience, I wrote three additional functions. pack_order(), unpack_order(), remove_old_orders().
Next, we check if the balance is insufficient for the payout; in that case, we consider it not a bet but a replenishment and save the replenishment in orders.
Next, we finally get to the essence of the smart contract.
First, if the player lost, we save him in the betting history, and if the amount is greater than 3 grams, we send 1/3 to the owner of the smart contract.
If the player won, we send double the amount to the player's address and then save the betting information in 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() = 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. .
Now it remains simple; let's create the GET methods so that information about the contract's state can be retrieved from the outside world (in fact, to read the data from the smart contract's storage).
We will write about how to retrieve information about the smart contract below.
I also forgot to add the code that will handle the very first request occurring upon the publication of the smart contract. And also, the bug with sending 1/3 of the amount to the owner's account.
Next, it remains to publish the smart contract. Let's create a folder requests.
I used the publication code as a base which in the official repository.
From what is worth paying attention to: we are forming the smart contract storage and input message. After that, the smart contract address is generated, which means the address is known even before publication in TON. Next, we need to send several grams to this address, and only after that, we need to send the file with the smart contract itself, as the network charges a fee for the storage and operations of the smart contract (validators that store and execute smart contracts). .
Next, we execute the publication code and receive lottery-query.boc the file and the smart contract address.
~/TON/build/crypto/fift -s requests/new-lottery.fif 0Don'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): kQAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KpFYOut of curiosity, let's make a request in TON
$ ./lite-client/lite-client -C ton-lite-client-test1.config.json
getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KsydAnd we will see that the account with this address is empty.
account state is emptyWe send to the address 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd 2 grams and after a few seconds, we execute the same command. To send grams, I use , and you can ask someone in the chat for test grams, which I will mention at the end of the article.
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KsydChecks that an uninitialized (state:account_uninit) smart contract with this address and a balance of 1,000,000,000 nanograms has appeared on the network.
The 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
The account balance is 2000000000ngNow let's publish the smart contract. We will start the 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 We will check that the contract has been published.
> last
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KsydAmong other things, we will receive.
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_activeWe see that account_active.
The corresponding commit with changes .
Now we will create requests to interact with the smart contract.
Specifically, we will leave the first one for changing the address as independent work, and the second one for sending grams to the owner's address. Essentially, we need to do the same thing as in the test for sending grams.
Here 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 9500000000 gram, b>Don't forget to sign the message with the private key lottery.pk, which was generated earlier when creating the smart contract. .
We obtain information from the smart contract using get methods
Now let's examine how to run the get methods of the smart contract.
Launch lite-client and we execute the get methods that we wrote.
$ .\/lite-client\/lite-client -C ton-lite-client-test1.config.json
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd balance
arguments: [ 104128 ]
result: [ 64633878952 ]
...In result contains the value returned by the function balance() from our smart contract.
We will perform the same for several other methods.
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd get_seqno
...
arguments: [ 77871 ]
result: [ 1 ] We will request the history of bets.
> 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 retrieve information about the smart contract on the website.
Displaying smart contract data on the website
I wrote a simple website using Python to present the data from the smart contract in a convenient format. I won’t dwell on it in detail here and will publish the website. .
Requests to TON are made from Python using lite-client. For convenience, the site is packaged in Docker and published on Google Cloud. .
Let's try
Now let's try sending grams there for replenishment from . We will send 40 grams and make a couple of bets for illustration. We see that the website shows the betting history, current percentage of wins, and other useful information.
, that we won the first bet, and lost the second.
Afterword
The article turned out to be much longer than I anticipated; perhaps it could have been shorter, but maybe it is just right for someone who knows nothing about TON and wants to write and publish a not-so-simple smart contract with interaction capabilities. Some things could possibly have been explained more simply.
Some aspects of the implementation could have been done more efficiently and elegantly, but that would have taken even more time to prepare the article. It is also possible that I made some mistakes or misunderstood something, so if you are doing something serious, you should rely on the official documentation or the official repository with the TON code.
It is worth noting that since TON is still in active development, changes may occur that break any of the steps in this article (which already happened while I was writing, but I fixed it), but the general approach is unlikely to change.
I won't speculate about the future of TON. The platform may become something bigger, and we should invest time in understanding it and occupy a niche with our products right now.
There is also Libra from Facebook, which has a potential user audience larger than that of TON. I don't know much about Libra, but according to the forum, its activity is much higher than that in the TON community. Although the developers and the TON community seem more underground, which is also cool.
Links
- Official documentation for TON:
- Official TON repository:
- Official wallet for various platforms:
- Smart contract repository from this article:
- Link to the smart contract website:
- Repository for the Visual Studio Code extension for FunC:
- Telegram chat about TON, which was very helpful in the early stages. I don't think it's wrong to say that everyone who has written something for TON is there. You can also request test grams there.
- Another chat about TON where I found useful information:
- First stage of the contest:
- Second stage of the contest:
Source: habr.com
