What is this article about?
In this article, I will tell about my participation in the first (out of two so far) Telegram blockchain contest. I didn't win any prize. However, decided to combine and share the unique experience I have had from the start to finish line, so my observations could help anyone who is interested.
Since I didn't want to write some abstract code, instead make something useful. I created instant lottery smart-contract and website which shows smart-contract data directly from Telegram Open Network (TON) avoiding any middle storage layers.
The article will be particularly useful for those, who want to write their first smart-contract for TON but don't know where to start.
Using the lottery as an example, I will go from setting up the environment to publishing a smart contract, interacting with it. Moreover, I will create a website that will show smart-contract data.
About participation in the contest
In October 2019 Telegram organized a blockchain contest with new programming languages Fift
and FunC
. The developers were asked to write any smart-contract out of five suggested. I thought that it would be interesting to do something out of the ordinary: learn a new language and create smart-contract even if it can be the last time using this it. Besides this topic is in trend nowadays.
It has to be mentioned that I have never had any sort of experience in writing the smart-contracts for blockchain networks.
My plan was to participate until the very end. Then I was going to write a summary article, but I was not selected further than the first stage of the competition, where I have submitted workable version of multi-signature wallet written in FunC
. Smart-contract was based on Solidity example for Ethereum Network.
I imagined that should make it, at this level of competition my work should be enough to gain a prize-winning spot. However, about 40 out of 60 participants happened to win and that excluded me. In general that is okay and could happen, but one thing bothered me: the review along with the test for my contract was not done, upon the announcement of the results. Thus, I asked other participants in the chat, if anyone else faced the same situation, there were none.
I believe, my messages draw some attention and after two days judges published several comments. I still did not get it: did they skipped my smart-contract by accident during the evaluation period? Or was it such a bad work that they decided it does not worth any comment at all? I asked these questions on the contest page, unfortunately questions were ignored as well. Although, it is not a secret who the judge was, nevertheless writing a private messages would have been too much to ask.
That being said it was decided to write this article that explains the subject in detail, since I have already spent plenty of time to understand how things work. In general, there is lack of information provided, so this article will save time for everyone who is interested.
The concept of smart-contract in TON
Before writing the first smart-contract we need to understand how to approach this thing in general. Therefore, now I will describe the complete set of the system, or more precisely which parts we should know in order to write at least some kind of functioning smart-contract.
We will focus on writing smart-contract using FunC
and Fift
, which will be compiled into Fift-assembler
and then will be executed in TON Virtual Machine (TVM)
. Therefore, the article is more like a description of the development of a regular program. We will not dwell on how the blockchain platform works.
How Fift
and TVM
work is well described in the official documentation. During the contest and now while writing current smart-contract I made a use of these docs.
The main language for writing smart-contract is FunC
. Currently there is no documentation available, therefore in order to develop something, we need to study the existing examples in the official repository, you may also find language implementation itself there and other participants' submissions from previous two contests. References are at the end of the article.
Let's say we have written the smart-contract using FunC
after that we compile FunC
code into Fift-assembler
.
The compiled code should be published in TON. In order to be able to do that we should write Fift
code, that takes as arguments smart-contract code and several other parameters and generates .boc file (which stands for "bag of cells") and depending on the way this code is written private key and address can be generated as well, based on the code of smart-contract.
We can send grams to the generated address even if the smart-contract is not published yet.
Since TON charges a fee for storage and transactions, before publishing a smart contract, you need to transfer grams to the generated address, otherwise, it will not be published.
As means to publish smart-contract, we should send generated .boc
file in TON using lite-client
(details are below). After that, we can interact with the smart-contract by sending external messages (e.g.: using lite-client
) or internal messages (e.g.: when one smart-contract sends a message to another).
Now, when we understand the process of publishing smart-contract code, it becomes easier. We already have an idea of what exactly we want to create and how it should work. While we code we can use existing smart-contracts as a reference or check the implementation of Fift
and FunC
in the official repository or in the documentation.
Frequently, I used Telegram-chat as a sourse searching by keywords, where during the contest all participants gathered along with Telegram employees and chatted about Fift
and FunC
. Link to chat in the end.
Let's move to implementation.
Preparing the environment to work with TON
Everything described below I have done in MacOS and re-tested in Ubuntu 18.04 LTS with Docker.
The first thing we need to do is to install lite-client
that will allow us to send requests to TON.
Instruction on the website explains the process of the installation pretty clear. Here we just follow the instruction and install missing dependencies. I didn't compile each library separately instead installed them from the official Ubuntu repository (used brew
on MacOS).
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 installed we install lite-client
, Fift
and FunC
.
Let's clone the TON repository together with its dependencies. For convenience, we will run everything in ~/TON
folder.
cd ~/TON
git clone https://github.com/newton-blockchain/ton.git
cd ./ton
git submodule update --init --recursive
The repository also contains Fift
and FunC
implementations.
Now we are ready to build the project. We cloned the repository into ~/TON/ton
folder. In ~/TON
we should create build
folder and execute the following.
mkdir ~/TON/build
cd ~/TON/build
cmake ../ton
Since we are going to write smart-contract we need more than just lite-client
meaning: Fift
and FunC
too, therefore, we compile them as well.
cmake --build . --target lite-client
cmake --build . --target fift
cmake --build . --target func
Next, we should download a configuration file that contains information about TON node to which lite-client
will connect.
wget https://newton-blockchain.github.io/global.config.json
First requests in TON
Now let's run installed lite-client
.
cd ~/TON/build
./lite-client/lite-client -C global.config.json
If the installation was successful we will see lite-client
connection logs.
[ 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)
...
We can execute help
and see available commands.
help
Let's list the commands that will be used 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
Now we are ready to start writing the smart-contract.
Implementation
Idea
As I described above the smart-contract that we are going to write is a lottery.
Moreover, it is not just an ordinary lottery, where there is a necessity to wait an hour, a day or even more, it is an instant lottery, in which the player transfers N
Grams to the given address and immediately receives back 2 * N
Grams or loses. Probability of winning we will set around 40%. If smart-contract do not have enough Grams to pay, we consider it as a top-up transaction.
It is extremely important to see the bets in a real-time and in a convenient way so that the players could easily understand if they won or lost. To solve this we will create a website that will show bets' history directly from TON.
Writing the smart-contract
For convenience, I created a syntax highlighter for FunC language, the plugin could be found in the Visual Studio Code plugin search and code available on GitHub. There is also a plugin for Fift language available and can be installed in the VSC.
Right away we can create a git repository and commit interim results.
To simplify our lives, we will write and test locally until the smart-contract is ready. Then once it is all set, we will publish it in TON.
Smart-contract has two external methods that can be triggered. First is recv_external()
this method is used when someone sends a request from external network (not within TON), for example when we send a message with lite-client
. Second is recv_internal()
this method is invoked when our contract receives message from some other contract within TON. In both cases, we can provide arbitrary parameters for these methods.
Let's start with a simple example that will still work upon publishing, however will not carry actual payload.
() recv_internal(slice in_msg) impure {
;; TODO: implementation
}
() recv_external(slice in_msg) impure {
;; TODO: implementation
}
Here we should explain what is the slice
. All data stored in TON Blockchain is the collection of TVM cells
or cell
for short, in such cell you can store up to 1023 bits of information and up to 4 references to other cells.
TVM cell slice
or slice
is part of the cell
that is used for parsing the data, it will be more understandable later. Most important for us is that we can pass data in slice
into smart-contract and depending on the message itself we can use recv_internal()
and recv_external()
methods to process it.
impure
— keyword indicates that a method changes data in the smart-contract persistance storage.
Now let's save contract code in lottery-code.fc
file and compile.
~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc
The meaning of flags can be checked with help
command.
~/TON/build/crypto/func -help
Now we have compiled Fift-assembler code in a file named 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
We can run it locally. Let's configure the environment first.
Note, that first line includes Asm.fif
file, which contains an implementation of Fift-assembler using Fift language.
Since we want to execute and test smart-contract locally we will create lottery-test-suite.fif
, copy compiled code and change the last line, write smart-contract code into constant code
than only we will be able to pass it to TON 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
So far it seems to be clear, now let's add the code to the same file that we will use to start 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
constant we should write context with which TVM will be launched (or network status). During the contest, one developer showed how c7
being formed and I just copied. In this article, we also might need to change rand_seed
because it is used to generate random number and if the number will remain same during the tests, it will always return the same number.
recv_internal
and recv_external
are just constants 0
and -1
that will be responsible for executing the corresponding functions of the smart-contract.
Now we are ready to write the first test for our empty smart-contract. For clarity, we will add all tests into the same file lottery-test-suite.fif
.
Let's create variable storage
and write empty cell
in it, storage
will be the permanent storage of the smart-contract.
message
is the variable that we will pass to the smart-contract from the external environment (or as per documentation from "nowhere"). Let's make message
empty for now.
variable storage
<b b> storage !
variable message
<b b> message !
Now, after we have prepared constants and variables we can run TVM with runvmctx
and pass created parameters.
message @
recv_external
code
storage @
c7
runvmctx
As a result, we have this interim Fift
code.
And now let's run following code.
export FIFTPATH=~/TON/ton/crypto/fift/lib // export once
~/TON/build/crypto/fift -s lottery-test-suite.fif
The code should be executde without errors and we should be able to see the following logs.
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 just wrote the first workable version of the smart-contract along with the test.
Processing external messages in a smart-contract
Now let's start adding functionality. Let's start with processing messages from "nowhere" in recv_external()
.
How to structure the message is up to the developer. But usually,
- At first, we want to protect smart-contract from the outside world and process messages only sent by the owner.
- Secondly, when we send a valid message we want the smart-contract to process it only once even if we will send the same message more than once. Also known as "replay attack".
Therefore, almost every smart-contract solves these two problems. Since our smart-contract will receive external messages we should take care of this. We will do it in the reverse order, we will solve the second issue and then move to the first.
There are different ways to solve a replay attack problem. This is one of the options: in smart-contract, we will initialize the counter with 0
that will count the number of received messages. In each message, among other parameters we will send to smart-contract current counter value. If the counter value in the message doesn't match the counter value in the smart-contract we will reject such message. When it will match, we will process the message and increase the smart-contract counter by one.
Let's go back to lottery-test-suite.fif
and add the second test. We should send the wrong counter number and code must throw an exception. For example, smart-contract counter will be 166 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 execute.
~/TON/build/crypto/fift -s lottery-test-suite.fif
And observe that test fails.
[ 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 point lottery-test-suite.fif
should be like this.
Now let's add counter into 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);
}
Message sent to the smart-contract will be stored in slice in_msg
.
First, we should check if a message is empty, if so, we just exit execution.
Next, we should start parsing the message. in_msg~load_uint(32)
that loads 32 bits from the message unsigned integer number 165.
Next, we should load 32 bits from the smart-contract storage. Then check if these two numbers are the same, if not exception should be thrown. In our case, since we have sent not matching counter, the exception will be thrown.
Let's compile.
~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc
Let's copy outcome into lottery-test-suite.fif
code, considering to change the last line. Monitor, if the test is passed successfully.
~/TON/build/crypto/fift -s lottery-test-suite.fif
Here is the commit with the current results.
Note that every time we change the smart-contract code we need to copy compiled code into lottery-test-suite.fif
which is inconvenient. Thus, we will create a small script, which will write compiled code into a constant. We will just have to include
this file in lottery-test-suite.fif
.
In the project folder create build.sh
file with the following code.
#!/bin/bash
~/TON/build/crypto/func -SPA -R -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc
Make it executable.
chmod +x ./build.sh
Now in order to compile the smart-contract we just need to run build.sh
and it will generate lottery-compiled.fif
. Besides that we need to write that into constant code
.
Let's add a script which will copy compiled lottery-compiled.fif
file then change the last line, like we did manually before. write the content into lottery-compiled-for-test.fif
.
# 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 let's execute resulting script and will get lottery-compiled-for-test.fif
, which we will include in lottery-test-suite.fif
.
In lottery-test-suite.fif
remove contract code and add "lottery-compiled-for-test.fif" include
line.
Run the tests to confirm that they are passing.
~/TON/build/crypto/fift -s lottery-test-suite.fif
Great, now lets automate test execution by creating test.sh
, which will firstly execute build.sh
and then will run tests.
touch test.sh
chmod +x test.sh
Write inside test.sh
.
./build.sh
echo "\nCompilation completed\n"
export FIFTPATH=~/TON/ton/crypto/fift/lib
~/TON/build/crypto/fift -s lottery-test-suite.fif
Run to confirm that the code compiles and tests are still passing.
./test.sh
Excellent, now every time we will run test.sh
, we will compile smart-contract and run all the tests. Here is the link to commit.
Before we continue let's make one more thing.
Create a folder named build
in which we will keep lottery-compiled.fif
and lottery-compiled-for-test.fif
. In addition to that we should create test
folder where we will keep lottery-test-suite.fif
and other supporting files. Link to the corresponding commit.
Let's continue smart-contract development.
Next thing that needs to be done is another test that sends the correct counter to the smart-contract and we should check it then save the updated counter. We will get back to this a bit later.
Now let's think about the data structure and what type of data should be kept in the storage of the smart-contract. I will describe what we store.
`seqno` 32 bits unsigned integer as a counter.
`pubkey` 256 bits unsigned integer for storing a public key, with this key we will check the signature of the sent message, explanation follows.
`order_seqno` 32 bits unsigned integer stores number of bets.
`number_of_wins` 32 bits unsigned integer stores number of wins.
`incoming_amount` Gram type, first 4 bits describe the length of number, remaining is the number of Grams itself, stores the number of received Grams.
`outgoing_amount` Gram type, stores the number of Grams sent to winners.
`owner_wc` work chain id, 32 bits (in some places said that 8 bits) integer. Currently only two work chains available 0 and -1
`owner_account_id` 256 bits unsigned integer, smart-contract address in current work chain.
`orders` stores dictionary type, stores 20 recent bets.
Then, we should write two convenience functions. First one we will name as pack_state()
, which will pack data for saving it in the smart-contract storage. Second one will be named as unpack_state()
, which will read and parse 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;
}
Add these at the beginning of the smart-contract. At this stage, our FunC
code should be like this.
In order to save the data we should call built-inset_data()
function and it will save the data from pack_state()
into smart-contract storage.
cell packed_state = pack_state(arg_1, .., arg_n);
set_data(packed_state);
Now when we have convenient functions to read and write the data, let's move on.
We need to verify that the external message received by our smart-contract is signed by the private key holder(s).
When we are publishing the smart-contract we can initialize it with pre-populated data in the storage. We will pre-populate it with the public key so that it could verify that the received message signed by the corresponding private key.
Before we continue let's create private/public key pair. We will save the private key in test/keys/owner.pk
. For this matter let's run Fift in interactive mode and execute four following commands.
`newkeypair` generates public and private key pair and places them on stack.
`drop` removes the first element from the stack.
`.s` just shows all stack elements.
`"owner.pk" B>file` writes the first element of the stack (in our case private key) into "owner.pk" file.
`bye` closes interactive mode.
In test
folder we should create a folder named keys
and put generated 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
Confirm that created owner.pk
file exists. Note: we have just dropped public key from the stack, because whenever we might need it, the public key can be generated from the private key.
Now we need to write signature verification functionality. Let's begin with the test. First of all we should read private key from the file with file>B
and assign it to owner_private_key
variable. Secondly we should convert the private into the public key using priv>pub
function and assign it to owner_public_key
variable.
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 will need both keys.
Let's initialize the smart-contract storage. We will fill storage
variable with arbitrary data in the same sequence as in pack_state()
.
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, let's form a signed message that will consist of signature and counter value.
In the first place we should create the data that we want to send, then it should be signed with the private key and last but not least put together 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 message_to_send !
The message which we are sending to the smart-contract is assigned to message_to_send
variable, about hashu
, ed25519_sign_uint
functions we can read in Fift documentation.
To run the test.
message_to_send @
recv_external
code
storage @
c7
runvmctx
Interim resuls commited here.
We can run the test and it will fail, therefore we will change the smart-contract, so it could receive this type of message and verify the signature.
Firstly we should count 512 bits signature and write it into variable, next we should count 32 bits variable counter.
Since we already have used earlier written unpack_state()
to parse storage data, we will use it.
Next, we should check the sent counter with the stored and the signature verification. If something does not match we must throw an exception accordingly.
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));
Commit with current changes here.
We can run the test and see that the second one fails. Well, there are two reasons why code is failing during parsing: lack of bits passed and lack of it in the storage. Moreover, we need to copy the storage structure from the third test.
During the second test run we will change the signature and the storage.The current state of tests' file can be found here.
Let's write the fourth test, in which we will send a message signed by someone else's private key. A new private key should be generated and saved in not-owner.pk
. We will sign the message with this key. It's time to run the tests and let's make sure that tests come off. Commit with corresponding changes.
Finally, we can start implementing business logic. In recv_external()
we will process two types of messages.
Due to the fact that our smart-contract will accumulate Grams of participants, these Grams need to be sent to the lottery owner. The lottery owner's address should be saved in the storage when the smart-contract is initialized. Yet, we need to have the option to change this address, in case it will be required.
Let's start by changing the owner's address. We will write a test to check that when the message received, a new address will be saved in the smart-contract storage. Note that in addition to the counter and a new address value the message we should include action
7 bits unsigned integer, and depending on the value of it we will choose how to process the message.
<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 implementation, we can observe how deserialization of the smart-contract storage happens. The implemented test you can see in this commit. Run the test to confirm its failure.
Now let's apply the logic of changing the owner's address so that the test will pass. In smart-contract, we continue to parse message
and read action
number. Reminder that we will have two action
s: owner's address change and the ability to send grams to the owner. Then we should read the new smart-contract address of the owner and save it in the storage.
Run the tests and observe that the third one is failing. It is happening because smart-contract code now parses additional 7 bits from the message, which is not included in the message that we have sent in the third test. Let's add missing action
in the message. After that we can run the tests and confirm that all of them are passing. Here is the commit with described changes. Great.
Now let's write the logic of sending a requested number of Grams to the owner's address. We will write two tests. The first one is when the balance of the lottery is not enough to make Gram transfer, the second is the opposite when the amount is enough. Here is the commit with updated tests.
Let's add code. We will start by adding two methods for convenience. The first one is the get-method that displays the remaining balance of the smart-contract.
int balance() inline_ref method_id {
return get_balance().pair_first();
}
The second method is regarding Grams transfer to an arbitrary address. I 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
}
We should add these two methods in the smart-contract and write a business logic. Firstly we should parse the number of Grams from the message. Secondly we should check remaining balance, if not enough throw the exception. In case if everything is on track, then Grams should be sent to the saved address and update counter value.
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));
Here is the commit with described changes. let's run the tests to confirm the successful outcome.
By the way, for processing the accepted message (storage and computing power) each time the smart-contract pays a fee in Grams. In order to fully process the valid external message, after basic checks, we should call accept_message()
.
Processing internal messages to the smart-contract
Now let's work on internal messages. In fact, we will receive Grams and send back twice the amount if the player wins or the 1/3 of the amount to the owner if the player loses.
Let's write a simple test. To do this, we need the test address of the smart-contract, out of which we would be sending grams to the lottery smart-contract.
Any smart-contract consists of two parts, 32 bits integer number responsible for work chain and 256 bits unsigned integer is a unique account number in this work chain. For example, -1 and 12345, this address we will save in a file.
I have copied the function for saving address in a file 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 figure out how this function works, it will help us understand Fift. Run Fift in interactive mode.
~/TON/build/crypto/fift -i
Firstly we should put on the stack numbers -1, 12345 and the string "sender.addr".
-1 12345 "sender.addr"
Next, we should run -rot
, which moves elements in the stack to the right, allowing the unique number of the smart-contract appear above the stack.
"sender.addr" -1 12345
256 u>B
converts 256 bits unsigned integer into bytes.
"sender.addr" -1 BYTES:0000000000000000000000000000000000000000000000000000000000003039
swap
swaps top two elements of the stack.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 -1
32 i>B
converts 32-bits integer into bytes.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 BYTES:FFFFFFFF
B+
concatenates two bytes sequences into one.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF
Again swap
.
BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF "sender.addr"
Finally B>file
receives two parameters, bytes and string that would be a file name. Function writes bytes into a file and names the file as sender.addr
. In current folder file has been created. We should move it into test/addresses/
.
Let's write a test which emulates sending Grams from the address that we have just created. Here is the commit.
Now let's work on the business logic of the internal message of the lottery.
First of all we should check if the received message bounced
or not, if bounced
we can ignore this message. bounced
means that the smart-contract will return Grams if an error happens during the execution. However, we will not return Grams if an error happens.
Next, we must check the amount of sent Grams if it is less than half of the Grams, we can simply accept the message without doing anything else.
Then we parse the address of the smart-contract where the message came from.
Read data from storage and then we should remove old orders from the history, if there are more than twenty items. For convenience, I wrote three additional functions pack_order()
, unpack_order()
, remove_old_orders()
.
Next, we should check the remaining smart-contract balance and if there are not enough Grams to pay, we should not consider it as a bet but as a top-up and save it in orders
.
Finally, the heart of the smart-contract.
If the player lost, we should save the order in the history of bets, moreover if the order amount is higher than 3 Grams, we must send 1/3 to the owner of the lottery.
If the player won, then we should send a double amount back to the player and save the order 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() < 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 is all. Corresponding commit.
Writing get-methods
Let's create the get-methods that will allow us to request information about the smart-contract's state from the external world (in fact, these methods will parse and return storage data).
Here is the commit with added get-methods. Later on we will see how these methods are being used.
I forgot to add code, that will process the very first request when we will publish the smart-contract. Corresponding commit. I also fixed the bug regarding sending 1/3 Grams to the owner.
Publishing smart-contract to the TON
Now we need to publish created smart-contract. Let's create folder requests
in the project root folder.
As a base code for publishing the smart-contract, I took a simple wallet publishing code in the official repository and altered it a bit.
Here is what we should pay attention to: we should form the smart-contract storage and entry message. After that address of the smart-contract is generated, so it is known even before deploying in TON. Next, we need to transfer several Grams to the generated address. And only after that we can deploy generated .boc
file with the smart-contract code. Because as we already mentioned network charges a fee for the storage and processing time. Here is deploy code.
Then we can run new-lottery.fif
and generate lottery-query.boc
file and lottery.addr
.
~/TON/build/crypto/fift -s requests/new-lottery.fif 0
Let's not forget to save lottery.pk
and lottery.addr
.
Also, among other things, we will see the smart-contract address.
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 let's make a request to TON.
$ ./lite-client/lite-client -C global.config.json
getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
And observe that account with this address is empty.
account state is empty
Now let's transfer 2 Grams to our smart-contract address 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
and after a few seconds make the same request. I have used official TON wallet and test Grams could be requested in the group chat, a link will be shared at the end of this article.
> last
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
Observe that the smart-contract has changed its status from empty to uninitialized (state:account_uninit
) with a balance of 2 000 000 000 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 we can deploy the smart-contract. Let's run lite-client
and execute the following.
> 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
Confirm that the smart-contract has been published.
> last
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
We can observe other things in the log.
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
Finally, we can see account_active
. The corresponding commit is here.
Sending external messages
Now let's create requests to interact with the smart-contract. We support two: sending grams to the owner and changing owner's smart-contract address. We need to make the same request as in the test #6.
This is the message that we will be sending to the smart-contract, where msg_seqno
165, action
2 and 9.5 Gram to be sent.
<b 165 32 u, 2 7 u, 9500000000 Gram, b>
We should remember to sign a message with the private key lottery.pk
, which has been generated earlier. Here is the corresponding commit.
Getting the information from a smart-contract using the get-methods
Now let's see how to run the get-methods of a smart contract.
Run lite-client
and invoke runmethod
with the smart-contract address and preferred get-method.
Getting sequence number.
$ ./lite-client/lite-client -C ton-lite-client-test1.config.json
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd balance
arguments: [ 104128 ]
result: [ 64633878952 ]
...
And orders' history.
> 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 the get-methods to show the data from the smart-contract.
Showing the data of the smart-contract on the website
I have written a website using Python to show information from the smart-contract in a convenient layout. Here I will not dwell on it in details and make one commit with changes.
Requests to TON are made using lite-client
via Python code. For convenience, everything packed in Docker and deployed at Digital Ocean. Website link.
Making a bet
Now let's transfer 64 Grams to our lottery to top it up using the official wallet. And make some bets for clarity. We can see that information on the website is updating and we can observe orders' history, current winning rate, and other useful information directly from the smart-contract.
Afterword
The article is way longer than I expected it to be. Maybe it could be shorter, or maybe it is just for a person who does not know anything about TON and wants to write and publish not the easiest smart-contract with the ability to interact with it.
Maybe some things could be explained easier. Some moments could be implemented more effectively, for example, we could parse orders' history from the blockchain itself and do not need to store it inside the smart-contract. But then we couldn't be able to show how a FunC dictionary works.
Since I could make a mistake somewhere or understand something incorrectly, you also need to rely on official documentation and official TON code repository.
Need to mention, that TON still in an active development state and incompatible changes could be made that would break some of the steps in this article (which happened and has been fixed).
I will not talk about the TON's future here. Maybe it will become something really big and we might need to spend some time to learn and start creating TON-based products or maybe not.
There is also Libra by Facebook which has potential audience of users that is even greater than that is of TON's. I don't know anything about Libra, judging by the official community it is more active than TON-community. TON developers and the community are more like an underground movement, which is also cool.
Links
- Official TON website: https://ton.org
- Official TON repository: https://github.com/newton-blockchain/ton
- Official TON wallet for different platforms: https://ton.org/wallets
- Lottery smart-contract discussed in this article: https://github.com/raiym/astonished
- Website of the lottery: https://astonished-d472d.ondigitalocean.app
- Visual Studio Code syntax highlighter for FunC: https://github.com/raiym/func-visual-studio-plugin
- Link to Telegram chat devoted to TON: https://t.me/tondev
- The first stage of the contest: https://contest.com/blockchain
- The second stage of the contest: https://contest.com/blockchain-2
July 7, 2020: Sadly, reflecting on the latest Telegram statement about test TON network servers discontinuing I decided to stop supporting the website with smart contract data. Smart-contract along with server code still accessible on the GitHub.
January 28, 2022: Article updated with current information and checked for relevance.