關於如何在 TON 中撰寫和發布智能合約
這篇文章是關於什麼的?
在這篇文章中,我將談論我如何參加第一屆(兩屆)Telegram 區塊鏈競賽,沒有獲獎,並決定將我的經驗記錄在一篇文章中,這樣它就不會被遺忘,也許還能有所幫助某人。
由於我不想編寫抽象程式碼,而是想做一些有用的事情,因此在這篇文章中,我編寫了一個用於即時彩票的智能合約和一個直接顯示來自TON 的智能合約數據而不使用中間存儲的網站。
對於那些想要在 TON 中創建第一個智能合約但不知道從哪裡開始的人來說,這篇文章將會很有用。
以彩票為例,我將從安裝環境到發布智能合約,與其交互,並編寫一個用於接收和發布資料的網站。
關於參加比賽
去年 XNUMX 月,Telegram 宣布舉辦新語言區塊鏈競賽 Fift
и FunC
。 有必要從編寫五個擬議的智能合約中的任何一個進行選擇。 我認為做一些不同的事情,學習語言並創造一些東西是件好事,即使我將來不需要寫任何東西。 另外,這個話題一直是人們談論的話題。
值得一提的是,我沒有開發智能合約的經驗。
我本來打算參加到最後,直到我可以再寫一篇評論文章,但我第一篇就失敗了。 我 FunC
這通常有效。 我以此為基礎
當時我就想,這絕對夠拿個獎了。 結果,40名參與者中大約有60人成為獲獎者,而我不在其中。 總的來說,這沒有什麼問題,但有一件事困擾著我。 公佈結果的時候,我的合約測試審核還沒做,我問聊天的人還有沒有沒有的,沒有。
顯然是在關注我的留言,兩天后評委們發表了評論,我仍然不明白他們是否在評審過程中不小心錯過了我的智能合約,或者只是認為它太糟糕了,不需要評論。 我在頁面上提出了問題,但沒有收到答案。 雖然誰評斷已經不是什麼秘密,但我認為沒有必要寫私人資訊。
花了很多時間去理解,所以決定寫一篇文章。 由於目前還沒有太多信息,本文將有助於為感興趣的每個人節省時間。
TON 中智能合約的概念
在你寫任何東西之前,你需要弄清楚從哪一邊來處理這件事。 那現在我就來跟大家介紹一下這個系統是由哪些部分組成。 更準確地說,您需要了解哪些部分才能至少編寫某種工作合約。
我們將專注於編寫智能合約並與 TON Virtual Machine (TVM)
, Fift
и FunC
,所以文章更像是對常規程式開發的描述。 我們不會在這裡詳細討論平臺本身是如何運作的。
一般而言,它是如何運作的 TVM
和語言 Fift
有很好的官方文件。 在參加比賽和現在寫合約的時候,我經常向她求助。
編寫智能合約的主要語言是 FunC
。 目前還沒有關於它的文檔,因此為了編寫一些東西,您需要研究官方存儲庫中的智能合約示例以及語言本身的實現,此外您還可以查看過去兩個中的智能合約示例比賽。 連結在文章最後。
假設我們已經寫了一份智能合約 FunC
,之後我們將程式碼編譯成Fift彙編器。
編譯後的智能合約仍有待發布。 為此,您需要編寫一個函數 Fift
,它將以智能合約程式碼和其他一些參數作為輸入,輸出將是一個擴展名為 .boc
(這意味著“細胞袋”),並且根據我們的編寫方式,私鑰和地址是根據智能合約程式碼生成的。 您已經可以將克發送到尚未發布的智能合約的地址。
在 TON 發布智能合約收到 .boc
該文件需要使用輕客戶端發送到區塊鏈(更多內容見下文)。 但在發布之前,需要將grams轉入生成的地址,否則智能合約將無法發布。 發布後,您可以透過從外部(例如,使用輕客戶端)或從內部(例如,智能合約在 TON 內向另一個智能合約發送訊息)發送訊息來與智能合約互動。
一旦我們了解了程式碼是如何發布的,事情就會變得更容易。 我們大致知道我們要寫什麼以及我們的程式將如何運作。 在寫作時,我們會尋找現有智能合約中如何實現這一點,或者研究實現程式碼 Fift
и FunC
在官方儲存庫中,或查看官方文件。
很多時候我會在所有參賽者和Telegram員工聚集的Telegram聊天室裡搜尋關鍵字,剛好比賽期間大家都聚集在那裡,開始討論Fift和FunC。 連結在文章最後。
是時候從理論轉向實踐了。
準備使用 TON 的環境
我做了 MacOS 文章中描述的所有內容,並在 Docker 上的乾淨 Ubuntu 18.04 LTS 中仔細檢查了它。
您需要做的第一件事是下載並安裝 lite-client
您可以使用它向 TON 發送請求。
官網的說明將安裝過程描述得相當詳細、清晰,省略了一些細節。 在這裡,我們按照說明進行操作,一路安裝缺少的依賴項。 我沒有自己編譯每個專案並從官方 Ubuntu 儲存庫安裝(在 MacOS 上我使用 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
一旦安裝了所有依賴項,您就可以安裝 lite-client
, Fift
, FunC
.
首先,我們克隆 TON 儲存庫及其相依性。 為了方便起見,我們將在一個資料夾中執行所有操作 ~/TON
.
cd ~/TON
git clone https://github.com/ton-blockchain/ton.git
cd ./ton
git submodule update --init --recursive
存儲庫還存儲實現 Fift
и FunC
.
現在我們準備好組裝該專案了。 存儲庫程式碼被克隆到一個資料夾中 ~/TON/ton
。 在 ~/TON
建立一個資料夾 build
並收集其中的項目。
mkdir ~/TON/build
cd ~/TON/build
cmake ../ton
既然我們要寫智能合約,我們不只需要 lite-client
但 Fift
с FunC
,所以讓我們編譯所有內容。 這不是一個快速的過程,所以我們正在等待。
cmake --build . --target lite-client
cmake --build . --target fift
cmake --build . --target func
接下來,下載配置文件,其中包含有關要連接到的節點的數據 lite-client
將連接。
wget https://test.ton.org/ton-lite-client-test1.config.json
向 TON 發出第一個請求
現在讓我們啟動 lite-client
.
cd ~/TON/build
./lite-client/lite-client -C ton-lite-client-test1.config.json
如果建置成功,那麼啟動後您將看到輕客戶端與節點的連線日誌。
[ 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)
...
你可以運行命令 help
並查看有哪些命令可用。
help
讓我們列出我們將在本文中使用的命令。
list of available commands:
last Get last block and state info from server
sendfile <filename> Load a serialized message from <filename> and send it to server
getaccount <addr> [<block-id-ext>] Loads the most recent state of specified account; <addr> is in [<workchain>:]<hex-or-base64-addr> format
runmethod <addr> [<block-id-ext>] <method-id> <params>... Runs GET method <method-id> of account <addr> with specified parameters
last получает последний созданный блок с сервера.
sendfile <filename> отправляет в TON файл с сообщением, именно с помощью этой команды публикуется смарт-контракт и запрсосы к нему.
getaccount <addr> загружает текущее состояние смарт-контракта с указанным адресом.
runmethod <addr> [<block-id-ext>] <method-id> <params> запускает get-методы смартконтракта.
現在我們已經準備好編寫合約了。
履行
想法
正如我上面所寫,我們正在寫的智能合約是一張彩券。
而且,這不是需要買票然後等待一個小時、一天或一個月的抽獎,而是用戶轉賬到合約地址的即時抽獎 N
克,並立即恢復 2 * N
克或損失。 我們將使獲勝的機率約為 40%。 如果沒有足夠的克數用於支付,那麼我們將把該交易視為充值。
此外,重要的是能夠以便捷的形式即時查看投注,以便用戶可以立即了解自己是贏了還是輸了。 因此,您需要建立一個直接從 TON 顯示投注和結果的網站。
編寫智能合約
為了方便起見,我突出顯示了 FunC 的程式碼;可以在 Visual Studio Code 搜尋中找到並安裝該外掛程式;如果您突然想要新增某些內容,我已將該外掛程式公開。 另外,之前有人製作了一個用於 Fift 的插件,您也可以安裝它並在 VSC 中找到它。
讓我們立即建立一個儲存庫,我們將在其中提交中間結果。
為了讓我們的生活更輕鬆,我們將編寫一個智能合約並在本地進行測試,直到它準備就緒。 之後我們才會在 TON 上發布它。
智能合約有兩個可以存取的外部方法。 第一的, recv_external()
當合約的請求來自外部世界(即不是來自 TON)時,例如當我們自己產生一條訊息並透過 lite-client 發送它時,就會執行此函數。 第二, recv_internal()
此時,在 TON 內部,任何合約都涉及我們的合約。 在這兩種情況下,您都可以將參數傳遞給函數。
讓我們從一個簡單的範例開始,一旦發布即可工作,但其中沒有功能負載。
() recv_internal(slice in_msg) impure {
;; TODO: implementation
}
() recv_external(slice in_msg) impure {
;; TODO: implementation
}
這裡我們需要解釋一下它是什麼 slice
。 TON 區塊鏈中儲存的所有資料都是一個集合 TVM cell
或者乾脆 cell
,在這樣的單元中,您最多可以儲存 1023 位元資料以及最多 4 個到其他單元的連結。
TVM cell slice
或 slice
這是現有的一部分 cell
用來解析一下,後面就清楚了。 對我們來說最重要的是我們可以轉移 slice
並根據訊息的類型,處理數據 recv_external()
或 recv_internal()
.
impure
— 表示函數修改智能合約資料的關鍵字。
我們將合約代碼保存在 lottery-code.fc
並編譯。
~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc
可以使用命令查看標誌的含義
~/TON/build/crypto/func -help
我們已經編譯了Fift彙編程式碼 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
可以在本地啟動,為此我們準備環境。
注意第一行連接 Asm.fif
,這是用 Fift 為 Fift 彙編器編寫的程式碼。
由於我們要在本地運行和測試智能合約,因此我們將創建一個文件 lottery-test-suite.fif
並將編譯後的程式碼複製到那裡,替換其中的最後一行,將智能合約程式碼寫入常數 code
然後將其傳輸到虛擬機器:
"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
到目前為止似乎很清楚,現在讓我們將用於啟動 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()
В c7
我們記錄上下文,即啟動 TVM(或網路狀態)所使用的資料。 即使在比賽期間,其中一位開發人員也展示瞭如何創建 c7
我複製了。 在這篇文章中我們可能需要改變 rand_seed
因為隨機數的產生取決於它,如果不改變,每次都會傳回相同的數字。
recv_internal
и recv_external
值為0和-1的常數將負責呼叫智能合約中對應的函數。
現在我們準備為我們的空智能合約建立第一個測試。 為了清楚起見,現在我們將所有測試添加到同一個文件中 lottery-test-suite.fif
.
讓我們創建一個變數 storage
並在其中寫入一個空的 cell
,這將是智能合約儲存。
message
這是我們將從外部傳輸到智慧聯絡人的消息。 我們現在也將其清空。
variable storage
<b b> storage !
variable message
<b b> message !
準備好常數和變數後,我們使用以下命令啟動 TVM runvmctx
並將創建的參數傳遞給輸入。
message @
recv_external
code
storage @
c7
runvmctx
最終我們會成功 Fift
.
現在我們可以運行生成的程式碼。
export FIFTPATH=~/TON/ton/crypto/fift/lib // выполняем один раз для удобства
~/TON/build/crypto/fift -s lottery-test-suite.fif
程式應該運行沒有錯誤,並且在輸出中我們將看到執行日誌:
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
太好了,我們已經編寫了智能合約的第一個工作版本。
現在我們需要新增功能。 首先讓我們處理來自外部世界的訊息 recv_external()
開發者自己選擇合約可以接受的訊息格式。
但通常情況下
- 首先,我們希望保護我們的合約免受外界影響,並使其只有合約的所有者才能向其發送外部訊息。
- 其次,當我們向 TON 發送有效訊息時,我們希望這種情況只發生一次,當我們再次發送相同的訊息時,智能合約會拒絕它。
所以幾乎每個合約都解決了這兩個問題,因為我們的合約接受外部訊息,所以我們也需要處理這個問題。
我們將以相反的順序進行。 首先我們來解決重複的問題;如果合約已經收到這樣的訊息並且處理了,就不會執行第二次。 然後我們將解決這個問題,以便只有特定圈子的人可以向智能合約發送訊息。
有多種方法可以解決重複訊息的問題。 我們將這樣做。 在智能合約中,我們將接收訊息的計數器初始化為初始值 0。在發送到智能合約的每個訊息中,我們將添加當前的計數器值。 如果訊息中的計數器值與智能合約中的值不匹配,那麼我們不處理它;如果匹配,那麼我們處理它,並將智能合約中的計數器加1。
讓我們回到 lottery-test-suite.fif
並向其添加第二個測試。 如果我們發送錯誤的號碼,代碼應該會拋出異常。 例如,讓合約資料儲存166,我們將發送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"
讓我們開始吧。
~/TON/build/crypto/fift -s lottery-test-suite.fif
我們將看到測試執行時出現錯誤。
[ 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
在這個階段 lottery-test-suite.fif
應該看起來像
現在讓我們將計數器邏輯加入到智慧合約中 lottery-code.fc
.
() recv_internal(slice in_msg) impure {
;; TODO: implementation
}
() recv_external(slice in_msg) impure {
if (slice_empty?(in_msg)) {
return ();
}
int msg_seqno = in_msg~load_uint(32);
var ds = begin_parse(get_data());
int stored_seqno = ds~load_uint(32);
throw_unless(33, msg_seqno == stored_seqno);
}
В slice in_msg
這是我們發送的訊息。
我們要做的第一件事是檢查訊息是否包含數據,如果不包含,則直接退出。
接下來我們解析該訊息。 in_msg~load_uint(32)
載入數字 165,32 位 unsigned int
從傳送的訊息。
接下來我們從智慧合約儲存載入 32 位元。 我們檢查載入的數字是否與傳遞的數字相符;如果不匹配,我們將拋出異常。 在我們的例子中,由於我們傳遞了一個不匹配的值,因此應該拋出異常。
現在我們來編譯一下。
~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc
將生成的程式碼複製到 lottery-test-suite.fif
,不要忘記替換最後一行。
我們檢查測試是否通過:
~/TON/build/crypto/fift -s lottery-test-suite.fif
請注意,不斷地將智能合約的編譯程式碼複製到帶有測試的文件中是不方便的,因此讓我們編寫一個腳本,將程式碼寫入常數中,然後我們只需使用以下命令將編譯的程式碼連接到我們的測試: "include"
.
在專案資料夾中建立一個文件 build.sh
包含以下內容。
#!/bin/bash
~/TON/build/crypto/func -SPA -R -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc
讓我們讓它可執行。
chmod +x ./build.sh
現在,只需運行我們的腳本來編譯合約。 但除此之外,我們還需要將其寫入常數 code
。 所以我們將創建一個新文件 lotter-compiled-for-test.fif
,我們將其包含在文件中 lottery-test-suite.fif
.
讓我們向 sh 添加skirpt 程式碼,這將簡單地將編譯後的檔案複製到 lotter-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
現在,為了檢查一下,讓我們運行生成的腳本並生成一個文件 lottery-compiled-for-test.fif
,我們將把它包含在我們的 lottery-test-suite.fif
В lottery-test-suite.fif
刪除合約程式碼並新增行 "lottery-compiled-for-test.fif" include
.
我們運行測試來檢查它們是否通過。
~/TON/build/crypto/fift -s lottery-test-suite.fif
太好了,現在為了自動啟動測試,讓我們建立一個文件 test.sh
,這將首先執行 build.sh
,然後執行測試。
touch test.sh
chmod +x test.sh
我們在裡面寫
./build.sh
echo "nCompilation completedn"
export FIFTPATH=~/TON/ton/crypto/fift/lib
~/TON/build/crypto/fift -s lottery-test-suite.fif
我們開始做吧 test.sh
並運行它以確保測試有效。
chmod +x ./test.sh
./test.sh
我們檢查合約是否編譯並執行測試。
太好了,現在正在啟動 test.sh
測試將立即編譯並執行。 這是連結
好的,在繼續之前,為了方便起見,讓我們再做一件事。
我們建立一個資料夾 build
我們將在其中儲存複製的合約及其寫入常數的克隆 lottery-compiled.fif
, lottery-compiled-for-test.fif
。 我們還創建一個資料夾 test
測試文件將儲存在哪裡? lottery-test-suite.fif
以及其他可能的支援文件。
讓我們繼續開發智能合約。
接下來應該進行一個測試,檢查訊息是否已收到,並且當我們發送正確的號碼時,商店中的計數器是否更新。 但我們稍後會這樣做。
現在我們來思考一下智能合約中需要儲存什麼資料結構和資料。
我將描述我們存儲的所有內容。
`seqno` 32-х битное целое положительное число счетчик.
`pubkey` 256-ти битное целое положительное число публичный ключ, с помощью которого, мы будем проверять подпись отправленного извне сообщения, о чем ниже.
`order_seqno` 32-х битное целое положительное число хранит счетчик количества ставок.
`number_of_wins` 32-х битное целое положительное число хранит количество побед.
`incoming_amount` тип данных Gram (первые 4 бита отвечает за длину), хранит общее количество грамов, которые были отправлены на контртакт.
`outgoing_amount` общее количество грамов, которое было отправлено победителям.
`owner_wc` номер воркчейна, 32-х битное (в некоторых местах написано, что 8-ми битное) целое число. В данный момент всего два -1 и 0.
`owner_account_id` 256-ти битное целое положительное число, адрес контракта в текущем воркчейне.
`orders` переменная типа словарь, хранит последние двадцать ставок.
接下來您需要編寫兩個函數。 我們先呼叫第一個 pack_state()
,它將打包資料以便隨後保存在智能合約儲存中。 我們來調用第二個 unpack_state()
將從儲存中讀取並返回資料。
_ 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;
}
我們將這兩個函數加入智能合約的開頭。 會成功的
要保存數據,您需要呼叫內建函數 set_data()
它將寫入數據 pack_state()
在智能合約儲存中。
cell packed_state = pack_state(arg_1, .., arg_n);
set_data(packed_state);
現在我們已經有了方便的寫入和讀取資料的函數,我們可以繼續前進了。
我們需要檢查從外部傳入的訊息是否由合約所有者(或有權存取私鑰的其他使用者)簽署。
當我們發布智能合約時,我們可以使用儲存中需要的資料來初始化它,這些資料將被保存以供將來使用。 我們將在那裡記錄公鑰,以便我們可以驗證傳入的訊息是否是使用相應的私鑰簽署的。
在繼續之前,讓我們建立一個私鑰並將其寫入 test/keys/owner.pk
。 為此,我們以交互模式啟動 Fift 並執行四個命令。
`newkeypair` генерация публичного и приватного ключа и запись их в стек.
`drop` удаления из стека верхнего элемента (в данном случае публичный ключ)
`.s` просто посмотреть что лежит в стеке в данный момент
`"owner.pk" B>file` запись приватного ключа в файл с именем `owner.pk`.
`bye` завершает работу с Fift.
我們建立一個資料夾 keys
資料夾內 test
並在那裡寫下私鑰。
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
我們在當前資料夾中看到一個文件 owner.pk
.
我們從堆疊中刪除公鑰,並在需要時從私鑰中取得它。
現在我們需要寫一個簽名驗證。 我們先從測試開始吧。 首先我們使用函數從檔案讀取私鑰 file>B
並將其寫入變數 owner_private_key
,然後使用該函數 priv>pub
將私鑰轉換為公鑰並將結果寫入 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 !
我們需要兩把鑰匙。
我們按照與函數中相同的順序使用任意資料初始化智能合約存儲 pack_state()
並將其寫入變數 storage
.
variable owner_private_key
variable owner_public_key
variable orders
variable owner_wc
variable owner_account_id
"./keys/owner.pk" file>B owner_private_key !
owner_private_key @ priv>pub owner_public_key !
dictnew orders !
0 owner_wc !
0 owner_account_id !
<b 0 32 u, owner_public_key @ B, 0 32 u, 0 32 u, 0 Gram, 0 Gram, owner_wc @ 32 i, owner_account_id @ 256 u, orders @ dict, b> storage !
接下來,我們將編寫一條簽名訊息,它只包含簽名和計數器值。
首先,我們創建要傳輸的數據,然後用私鑰對其進行簽名,最後產生簽名訊息。
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 !
因此,我們將發送到智能合約的消息記錄在變數中 message_to_send
, 關於函數 hashu
, ed25519_sign_uint
你可以閱讀
為了運行測試,我們再次調用。
message_to_send @
recv_external
code
storage @
c7
runvmctx
讓我們運行測試,它會失敗,因此我們將更改智慧合約,以便它可以接收這種格式的訊息並驗證簽名。
首先,我們計算訊息中簽署的 512 位元並將其寫入變量,然後計算計數器變數的 32 位元。
由於我們有從智慧合約儲存讀取資料的功能,因此我們將使用它。
接下來是檢查隨儲存轉移的計數器並檢查簽名。 如果某些內容不匹配,那麼我們會使用適當的程式碼拋出例外。
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));
相關提交
讓我們執行測試,看看第二個測試失敗了。 由於兩個原因,訊息中沒有足夠的位,並且儲存中沒有足夠的位,因此程式碼在解析時崩潰。 我們需要向發送的訊息添加簽名,並複製上次測試的儲存。
在第二次測試中,我們將新增訊息簽名並更改智慧合約儲存。
讓我們編寫第四個測試,其中我們將發送一條用其他人的私鑰簽署的訊息。 讓我們創建另一個私鑰並將其保存到文件中 not-owner.pk
。 我們將使用此私鑰對訊息進行簽署。 讓我們運行測試並確保所有測試都通過。
現在我們終於可以繼續實現智能合約邏輯了。
В recv_external()
我們將接受兩種類型的消息。
由於我們的合約會累積玩家的損失,因此這筆錢必須轉移給彩票的創建者。 彩票創建者的錢包地址在合約創建時記錄在存儲中。
為了以防萬一,我們需要能夠更改發送失敗者克數的位址。 我們還應該能夠將彩票中的克發送到所有者的地址。
讓我們從第一個開始。 我們首先編寫一個測試,檢查發送訊息後智能合約是否將新地址保存在儲存中。 請注意,訊息中除了櫃檯和新地址外,我們還傳送 action
一個7位元整數非負數,根據它,我們將選擇智能合約中如何處理訊息。
<b 0 32 u, 1 @ 7 u, new_owner_wc @ 32 i, new_owner_account_id @ 256 u, b> message_to_sign !
在測試中你可以看到智能合約儲存是如何反序列化的 storage
在第五。 Fift 文件中描述了變數的反序列化。
讓我們運行測試並確保它失敗。 現在讓我們添加邏輯來更改彩票所有者的地址。
智能合約中我們繼續解析 message
, 讀入 action
。 讓我們提醒您,我們將有兩個 action
:更改地址並發送克。
然後我們讀取合約所有者的新地址並將其保存在儲存中。
我們運行測試並發現第三個測試失敗。 它崩潰的原因是合約現在額外解析了訊息中的 7 位,而測試中缺少這些位。 在訊息中加入一條不存在的訊息 action
。 讓我們運行測試並查看一切是否通過。
現在我們來編寫將指定克數發送到先前儲存的位址的邏輯。
首先,我們來寫一個測試。 我們將編寫兩個測試,一個是在沒有足夠餘額的情況下進行的,第二個是在一切都應該成功通過的情況下進行的。 可以查看測試
現在讓我們來新增程式碼。 首先,讓我們來寫兩個輔助方法。 第一個 get 方法是找出智能合約的當前餘額。
int balance() inline_ref method_id {
return get_balance().pair_first();
}
第二個是將克發送到另一個智能合約。 我完全複製了另一個智能合約的這個方法。
() 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
}
我們將這兩個方法加入智能合約並編寫邏輯。 首先,我們解析訊息中的克數。 接下來我們檢查餘額,如果不夠,我們拋出異常。 如果一切正常,那麼我們將克發送到已儲存的位址並更新計數器。
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));
順便說一下,每次處理一則訊息都會從智能合約中扣除佣金。 為了讓智慧合約訊息執行請求,經過基本檢查後,您需要調用 accept_message()
.
現在讓我們繼續討論內部訊息。 事實上,我們只接受克數,如果玩家獲勝,我們將雙倍的金額退還給玩家,如果玩家輸了,我們將三分之一的金額退還給所有者。
首先,讓我們先寫一個簡單的測試。 為此,我們需要一個智能合約的測試地址,我們應該從該地址將克發送到智能合約。
智慧合約位址由兩個數字組成,一個負責工作鏈的 32 位元整數和一個在該工作鏈中唯一的 256 位元非負整數帳號。 例如,-1和12345,這是我們將保存到文件的地址。
我複製了保存地址的函數 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
讓我們看看該函數是如何運作的,這將有助於理解 Fift 的工作原理。 以互動模式啟動 Five。
~/TON/build/crypto/fift -i
首先,我們將 -1、12345 和未來檔案的名稱「sender.addr」壓入堆疊:
-1 12345 "sender.addr"
下一步是執行該函數 -rot
,它以這樣的方式移動堆疊,使得堆疊頂部有一個唯一的智慧合約編號:
"sender.addr" -1 12345
256 u>B
將 256 位元非負整數轉換為位元組。
"sender.addr" -1 BYTES:0000000000000000000000000000000000000000000000000000000000003039
swap
交換堆疊頂端的兩個元素。
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 -1
32 i>B
將 32 位元整數轉換為位元組。
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 BYTES:FFFFFFFF
B+
連接兩個位元組序列。
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF
再次 swap
.
BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF "sender.addr"
最後將位元組寫入文件 B>file
。 之後我們的堆疊就空了。 我們停下來 Fift
。 當前資料夾中已建立文件 sender.addr
。 讓我們將文件移到已建立的資料夾中 test/addresses/
.
讓我們編寫一個簡單的測試,將克發送到智能合約。
現在我們來看看抽獎的邏輯。
我們做的第一件事就是查看訊息 bounced
或不如果 bounced
,然後我們忽略它。 bounced
意味著如果發生錯誤,合約將返回克。 如果突然發生錯誤,我們將不會返回克。
我們檢查,如果餘額少於半克,那麼我們只需接受該訊息並忽略它。
接下來,我們解析訊息來自的智能合約的位址。
我們從儲存中讀取數據,然後從歷史記錄中刪除舊投注(如果超過二十個)。 為了方便起見,我額外寫了三個函數 pack_order()
, unpack_order()
, remove_old_orders()
.
接下來我們看看餘額是否不夠支付,那麼我們認為這不是投注,而是補款,並將補款保存在 orders
.
最後是智能合約的本質。
首先,如果玩家輸了,我們會將其保存在投注歷史中,如果金額超過 3 克,我們會將 1/3 發送給智能合約的所有者。
如果玩家獲勝,那麼我們會將雙倍的金額發送到玩家的地址,然後將有關投注的資訊保存在歷史記錄中。
() 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));
}
就是這樣。
現在剩下的事情很簡單,讓我們創建 get 方法,以便我們可以從外部世界獲取有關合約狀態的資訊(實際上,從智慧合約儲存中讀取資料)。
我還忘記添加處理發布智能合約時發生的第一個請求的程式碼。
下一步是發布智能合約。 我們建立一個資料夾 requests
.
我以發布程式碼為基礎 simple-wallet-code.fc
值得關注的事。 我們產生智能合約儲存和輸入訊息。 此後,智能合約的地址就產生了,也就是說,該地址甚至在 TON 中發布之前就已知。 接下來,您需要向該地址發送幾克,只有在此之後您才需要發送帶有智能合約本身的文件,因為網絡需要收取佣金來存儲智能合約和其中的操作(存儲和執行智能合約的驗證者)合約)。
接下來我們執行發布程式碼,得到 lottery-query.boc
智能合約文件和地址。
~/TON/build/crypto/fift -s requests/new-lottery.fif 0
不要忘記保存生成的文件: lottery-query.boc
, lottery.addr
, lottery.pk
.
除此之外,我們將在執行日誌中看到智能合約的位址。
new wallet address = 0:044910149dbeaf8eadbb2b28722e7d6a2dc6e264ec2f1d9bebd6fb209079bc2a
(Saving address to file lottery.addr)
Non-bounceable address (for init): 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
Bounceable address (for later access): kQAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KpFY
只是為了好玩,讓我們向 TON 提出請求
$ ./lite-client/lite-client -C ton-lite-client-test1.config.json
getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
我們會看到這個地址的帳戶是空的。
account state is empty
我們發送到地址 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
2 Gram,幾秒鐘後我們執行相同的命令。 發送克我用
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
看起來像是未初始化的(state:account_uninit
)具有相同地址且餘額為 1 奈克的智能合約。
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
現在讓我們發布智能合約。 讓我們啟動 lite-client 並執行。
> 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
讓我們檢查一下合約是否已發布。
> last
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
除此之外,我們得到了。
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
我們看到 account_active
.
相應的提交與更改
現在讓我們建立與智能合約互動的請求。
更準確地說,我們將把第一個更改地址作為一項獨立的工作,而我們將做第二個將克發送到所有者地址的工作。 事實上,我們需要做與發送克測試相同的事情。
這是我們將發送到智能合約的訊息,其中 msg_seqno
165, action
2克和9.5克用於發送。
<b 165 32 u, 2 7 u, 9500000000 Gram, b>
不要忘記使用您的私鑰對訊息進行簽名 lottery.pk
,這是在創建智能合約時先前產生的。
使用 get 方法從智能合約接收訊息
現在讓我們看看如何運行智能合約的 get 方法。
發射 lite-client
並且運行我們編寫的 get 方法。
$ ./lite-client/lite-client -C ton-lite-client-test1.config.json
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd balance
arguments: [ 104128 ]
result: [ 64633878952 ]
...
В result
包含函數傳回的值 balance()
來自我們的智能合約。
我們將對其他幾種方法執行相同的操作。
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd get_seqno
...
arguments: [ 77871 ]
result: [ 1 ]
讓我們詢問您的投注歷史記錄。
> 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]) ]
我們將使用 lite-client 和 get 方法在網站上顯示有關智能合約的資訊。
在網站上顯示智能合約數據
我用 Python 編寫了一個簡單的網站,以方便的方式顯示智慧合約中的資料。 這裡我就不詳細講了,將網站公佈出來
向 TON 發出的請求來自 Python
在...的幫助下 lite-client
。 為了方便起見,網站採用 Docker 打包並發佈在 Google Cloud 上。
咱們試試吧
現在讓我們嘗試將克送到那裡進行補充
後記
這篇文章比我預想的要長得多,也許它本來可以更短,或者也許只是為了一個對TON 一無所知,想要編寫和發布一個不那麼簡單的智能合約,並且能夠與TON 交互的人它。 也許有些事情可以更簡單地解釋。
也許實現的某些方面可以更有效、更優雅地完成,但那樣會花更多時間來準備文章。 也有可能我在某個地方犯了錯誤或沒有理解某些東西,所以如果你正在做一些嚴肅的事情,你需要依賴官方文件或帶有 TON 程式碼的官方儲存庫。
需要注意的是,由於 TON 本身仍處於開發的活躍階段,可能會發生一些變化,從而破壞本文中的任何步驟(這發生在我撰寫本文時,已被更正),但一般方法是不太可能改變。
我不會談論 TON 的未來。 也許這個平台會變得很大,我們現在應該花時間研究它並用我們的產品填補一個空白。
還有Facebook的Libra,其潛在用戶受眾比TON還要多。 我對 Libra 幾乎一無所知,從論壇來看,那裡的活動比 TON 社區多得多。 雖然TON的開發者和社區更像地下,這也很酷。
引用
- TON 官方文檔:
https://test.ton.org - 官方 TON 儲存庫:
https://github.com/ton-blockchain/ton - 不同平台的官方錢包:
https://wallet.ton.org - 本文中的智能合約儲存庫:
https://github.com/raiym/astonished - 智能合約網站連結:
https://ton-lottery.appspot.com - Visual Studio Code for FunC 擴充功能的儲存庫:
https://github.com/raiym/func-visual-studio-plugin - 在 Telegram 中討論 TON,這確實有助於在初始階段弄清楚這一點。 我認為如果我說每個為 TON 寫過東西的人都在那裡,那就不會錯了。 您也可以在那裡索取測試克數。
https://t.me/tondev_ru - 另一次關於 TON 的聊天,我在其中找到了有用的信息:
https://t.me/TONgramDev - 第一階段比賽:
https://contest.com/blockchain - 第二階段比賽:
https://contest.com/blockchain-2
來源: www.habr.com