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: GitHub - newton-blockchain/ton
- Official TON wallet for different platforms: TON Wallets
- Lottery smart-contract discussed in this article: GitHub - raiym/astonished: Simple TON-based lottery
- Website of the lottery: https://astonished-d472d.ondigitalocean.app
- Visual Studio Code syntax highlighter for FunC: GitHub - raiym/func-visual-studio-plugin: Syntax highlighter for FunC language
- Link to Telegram chat devoted to TON: Telegram: Contact @tondev
- The first stage of the contest: Blockchain Contest – Developer Challenges
- The second stage of the contest: Blockchain Contest, Stage 2 – Developer Challenges
Article carried from:How to develop and publish a smart-contract in the Telegram Open Network (TON) / Хабр