作者:@Web3Mario(https://x.com/web3_mario)

摘要:承接上一篇關於TON技術介紹的文章,這段時間深入研究了一下TON官方開發文檔,感覺學習起來還是有些門檻,當前的文檔內容似乎更像是一個內部開發文檔,對新入門的開發者來說不太友好,因此試着以自己的學習軌跡,梳理一系列關於TON Chain項目開發的文章,希望可以對大家快速入門TON DApp開發有一些幫助。行文有誤也歡迎大家指正,一起學習。

在EVM中開發NFT和在TON Chain上開發NFT有哪些不同

發行一個FT或NFT對於DApp開發者來說通常是最基本的需求。因此我也以此作爲學習入口。首先讓我們來了解以下在EVM技術棧中開發一個NFT和在TON Chain中的區別。基於EVM的NFT通常會選擇繼承ERC-721的標準。所謂NFT,指的是不可分割的加密資產類型,且每個資產具有唯一性,即存在某些專屬的特性。而ERC-721就是對這個類型的資產的一種通用的開發範式。讓我們看一個常見的ERC721合約需要實現哪些函數以及記錄哪些信息。下圖是一個ERC721接口。可以看到與FT不同,在轉賬接口中需要輸入的是待轉賬的tokenId而非數量。這個tokenId也是NFT資產唯一性最基本的體現,當然爲了承載更多的屬性,通常會爲每個tokenId記錄一個metadata,這個metadata是一個外部鏈接,保存了該NFT的其他可擴展數據,例如一張PFP圖片的鏈接,某些屬性名稱等。

對於熟悉Solidity或者熟悉面向對象的開發者來說,實現這樣一個智能合約是件容易的事,只要定義好合約中需要的數據類型,例如一些關鍵的映射關係mapping,並根據所需功能開發相應的對這些數據的修改邏輯,即可實現一個NFT。

然而在TON Chain中這一切變的不太相同,造成不同的核心原因有兩個:

  • 在TON中數據的存儲是基於Cell實現的,而同一個賬戶的Cell通過有向無環圖來實現。這樣就導致需要之久化存儲的數據不能無邊界的增長下去,因爲一個有向無環圖來說,數據深度決定的查詢成本,當深度無限延伸之後,有可能造成查詢成本過高,從而導致合約陷入死鎖問題。

  • 爲了追求高併發性能,TON捨棄了串行執行的架構,轉而採用了一個專爲並行而生的開發範式,Actor模型,來重構執行環境。這就造成了一個影響,智能合約之間只能通過發送所謂內部消息的方式異步調用,注意無論是狀態修改類型或只讀類型的調用都需要遵循這個原則,除此之外,也需要仔細考慮異步調用若失敗,如何處理數據回滾的問題。

當然關於技術上其他不同點在上一篇文章中有過詳細的論述,本篇文章希望可以聚焦在智能合約開發上,所以不展開討論。上述兩條設計原則讓TON中智能合約開發與EVM產生了很大區別。在開始的論述中,我們知道一個NFT合約中需要定義一些映射關係,也就是mapping,來保存NFT相關的數據。其中最重要的就是owners,這個mapping存儲了某個tokenID對應的NFT的所有者地址的映射關係,決定了NFT的所有權,轉賬就是對該所有權的修改。由於理論上這是一個可以無邊界的數據結構,需要儘量避免。因此官方推薦以是否存在無邊界數據結構作爲分片的標準。即當有類似的數據存儲需求時,通過主從合約的範式來替代,通過創建子合約的方式來管理每個key對應的數據。並通過主合約管理全局參數,或幫助處理子合約之間的內部信息交互。

這也就意味着在TON中的NFT也需要採用類似的架構來設計,每個NFT都是一個獨立的子合約,保存了諸如所有者地址,metadata等專屬數據,並通過一個主合約來管理全局數據,例如NFT name,symbol,總供應量等。

在明確了架構後,接下來就需要解決核心功能的需求了,由於採用了這個主從合約的方式,因此就需要明確哪些功能由主合約承載,哪些功能由子合約承載,並且兩者之間通過什麼內部信息溝通,同時當出現執行錯誤時,如何回滾之前的數據。通常情況下,在開發複雜的大型項目之前,通過一個類圖並明確彼此之間的信息流,並仔細思考內部調用失敗後的回滾邏輯是必要的,當然上述NFT開發雖然簡單,但也可以做類似驗證。

從源碼學習開發TON智能合約

TON選擇了設計一種類 C 語言的、靜態類型語言,名爲Func來作爲智能合約開發語言,那麼接下來就讓我們從源碼來學習如何開發TON智能合約,我選擇了TON官方文檔中的NFT示例來進行介紹,感興趣的小夥伴可以自行去查閱。在這個case中實現了一個簡單的TON NFT例。讓我們看下合約結構,共分爲兩個功能合約以及三個必要的庫。

這兩個主要的功能合約即按照上述的原則進行設計,首先讓我們來看下主合約nft-collection的代碼:

這引入了第一個知識點,如何在TON智能合約中持久化存儲數據,我們知道在Solidity中數據的持久化存儲是由EVM根據參數的類型自動處理的,通常情況下,智能合約的狀態變量將在執行結束後根據最新值自動被持久化存儲,開發者並不需要考慮這個過程。但在Func中情況並不如此,開發者需要自己來實現相應的處理邏輯,這個情況有點類似於C和C++需要考慮GC的過程,但其他新的開發語言通常將這部分邏輯自動化處理。我們來看下代碼,首先引入一些需要的庫,然後看到第一個函數load_data用於讀取被持久化存儲的數據,其邏輯爲首先通過get_data返回持久化合約存儲cell,注意這是由標準庫stdlib.fc實現的,通常情況下可以將其中的一些函數視爲系統函數來使用。

該函數的返回值類型爲cell,這是 TVM中的cell類型。在之前的介紹中,我們已經知道TON 區塊鏈中的所有持久數據都存儲在cell樹中。每個cell最多有 1023 位任意數據和最多四個對其他cell的引用。cell在基於堆棧的 TVM 中用作內存。cell中保存的是緊編碼後的數據,要想獲取其中具體的明文數據,需要將cell轉換爲被稱爲slice的類型。cell可以通過begin_parse函數轉換成爲slice類型,然後可以通過從slice加載數據位和對其他cell的引用來獲得cell中的數據。注意15行代碼中的這種調用方法是一個func中的語法糖,可以直接調用第一個函數的返回值的第二個函數。並在最後按照數據持久化順序依次加載相應的數據。注意這個過程和solidity不同,並不是根據hashmap調用,所以這個調用順序不能亂。

在save_data函數中,邏輯與之類似,只不過這是一個反向的過程,這就引入了下一個知識點,一個新的類型builder,這是cell構建器的類型。數據位和對其他cell的引用可以存儲在構建器中,然後構建器可以最終化爲新cell。首先通過標準函數begin_cell創建一個builder,並依次通過store相關函數存儲相關函數,注意上文中調用順序與此處存儲順序需要保持一致。最後通過end_cell完成新cell構建,這時該cell被管理在內存中,最後通過最外層的set_data,就可以完成對該cell的持久化存儲。

接下來讓我們來看下業務相關函數,首先需要先介紹下一個知識點,如何通過合約創建一個新的合約,這在剛剛介紹的主從架構中將被經常用到。我們知道在TON中,智能合約之間的調用是通過發送內部消息的方式來實現的。這是通過一個名爲send_raw_message來實現的,注意第一個參數是message編碼後的cell,第二個參數是標識位,用於表明該交易的執行方式的區別,在TON中設置了不同的內部消息發送的執行方式,目前有3種消息Modes和3種消息Flags。可以將單一Mode與多個(也許沒有)標誌組合以獲得所需的mode。組合只是意味着將它們值的和填入即可。下面給出了Modes和Flags的描述表格:

那麼讓我們來看第一個主要函數,deploy_nft_item,顧名思義,這是一個用於創建或者說鑄造新NFT實例的函數,經過一番操作編碼一個msg後,通過send_raw_message發送該內部合約,並選擇了flag 1的發送標識位,僅將編碼中指定的fee作爲本次執行的gas fee。經過上文的介紹我們很容易意識到,這個編碼規則應該是對應創建一個新的智能合約的方式。那讓我們來看看具體是怎麼實現的。

讓我們直接看51行,上面兩個函數是用於生成message所需信息的輔助函數,因此我們後面再來看,這是一個用於創建智能合約的內部消息的編碼過程,中間的一些數字其實也是一些標識位,用於說明該內部消息的需求,這裏要引入下一個知識點,TON選擇了一種名爲TL-B 的二進制語言來描述消息的執行方式,並且根據設置不同的標記位來實現某些特定功能的內部消息,最容易想到的兩個使用場景,新合約創建和已部署合約函數調用。而51行的這種方式即對應了前者,創建一個新的nft item合約,而這主要是通過55,56,57三行指定的。首先55行這一大串數字是一系列標識位,注意store_uint的第一個入參是數值,第二個是位長,其中決定了該內部消息是合約創建的是後三個標記位,以及相應二進制值位爲111(十進制即爲4+2+1),其中前兩個表示該消息將附帶StateInit數據,這個數據即爲新合約的源碼,以及初始化所需的數據。而後一個標記位表示內部消息附載,即希望執行相關邏輯以及需要的參數。因此你會看到在第66行代碼並沒有設置該三位數據,則表明的是一次對已部署合約的函數調用。具體的編碼規則在這裏查看。

那麼StateInit的編碼規則即對應了49行代碼,通過calculate_nft_item_state_init計算,注意stateinit數據的編碼也遵循了一種既定的TL-B編碼規則,除了一些標記位之外,主要涉及到兩部分新合約code和以及初始化data。data的編碼順序需要與新合約指定的持久化cell的存儲順序保持一致。在36行可以看到,初始化數據有item_index,即類似與ERC721中的tokenId,以及由標準函數my_address返回的當前合約地址,即爲collection_address,這個數據的順序與nft-item中的聲明保持一致。

接下來一個知識點就是在TON中,所有未生成的智能合約而可以預先計算其生成後的地址,這點與Solidity中的create2函數類似,在TON中新地址的生成由兩部分組成,workchain標識位與stateinit的哈希值拼接而成,前者在之前的介紹中我們已經知道是爲了相應TON無限分片架構而需要被指定的,當前爲統一值。由標準函數workchain獲得。後者由標準函數cell_hash獲得。因此回到該例子,calculate_nft_item_address即爲預先計算新合約地址的函數。並將生成值在第53行編碼到message中,作爲該內部消息的接收地址。而nft_content則對應了對被創建合約的初始化調用,具體的實現在下一篇文章中介紹。

至於send_royalty_params,則需要是對某隻讀請求的內部消息的相應,在之前的介紹中,我們特意強調了在TON中內部消息不光包含可能會修改數據的操作,只讀操作也需要通過這種方式實現,因此該合約即爲此類操作,首先值得注意的是67行表示響應該請求後對請求者回調函數的標記,記下來即爲返回的數據,分別是請求的item index,以及相應的royalty數據。

接下來讓我們引入下一個知識點,TON中智能合約只有兩個統一的入口,名爲recv_internal和recv_external,其中前者爲所有內部消息的統一調用入口,後者爲所有外部消息的統一調用入口,開發者需要在函數內部根據需求,採用類似switch的方式根據message指定的不同標記位來響應不同的請求,這裏的標記位即爲上述67行的回調函數標記。回到該例子,首先對message進行空位檢查,通過後分別解析message中的信息,首先在83行解析獲得sender_address,該參數將用於後續的權限檢查,注意這裏的~操作符,屬於另一個語法糖。這裏先不展開將。接下來解析op操作標記位,而後根據不同的標記位,分別處理相應請求。其中即根據某些邏輯分別調用了上述的函數。例如響應對royalty參數的請求,或鑄造新的nft,並自增全局index。

接下來一個知識點對應了108行,想必大家通過命名也可以知道該函數的處理邏輯,與Solidity中的require函數類似,Func中通過標準函數throw_unless來拋出異常,第一個入參爲錯誤碼,第二個是檢查位布爾值,若位false則拋出異常,並附帶該錯誤碼。而在這行中通過equal_slices來判斷上面解析到的sender_address是否等於該合約持久化存儲的owner_address,做權限判斷。

最後爲了使代碼結構更清晰,開始閒了一系列幫助獲取持久化信息的輔助函數,在這裏就不展開介紹了,開發者可以參考這種結構來開發自己的智能合約。

TON生態的DApp開發實在是件有趣的事情,與EVM的開發範式有很大差異,因此我會通過一系列文章來介紹如何在TON Chain中開發DApp。與大家共同學習,把握這波機會。也歡迎大家在twitter上與我互動,碰撞一些新的有趣的dapp idea,一起開發。