- 原文標題:《Web3 DApp 最佳編程實踐指南》
- 撰文:郭宇
自宣布進入創業間隔年以來,CodeforDAO(GitHub)與 Checks Finance(@checksfinance)兩個項目進入了密集而緊張的迭代周期,在合約編寫,單元測試,工作流自動化,前端與客戶端方面都遇到了較多問題,對此,我總結出了一些經驗。當前這兩個項目還有大量細節等待優化,尚未正式 landing,我認為將開發過程中的經驗和總結與大家進行分享,能幫助更多工程師轉向 Web3,也有助於項目的長遠發展。
這篇文章將會涉及到開發一個 DApp 所涵蓋的幾乎所有方面內容,因此,它會非常冗長繁瑣,如果你對某一方面特別感興趣,我建議你可以通過下邊這個目錄直接跳去感興趣的章節閱讀。另外,這篇文章並不是 Step by Step 的代碼教學範例,因此,跳躍章節閱讀並不會影響體驗。
本文中提到的所有項目均列在我的 GitHub Star 清單中,可以在這裡統一查閱:
Guo-Yu’s list / DApp Best Practice Stack
Web3 DApp 最佳編程實踐指南
1. 認識 DApp 技術棧
與傳統的 App(包括 Web App 與 Mobile App)最大的不同點在於,DApp 的大量功能依賴直接與智能合約(以下簡稱合約)進行交互。我們無法直接使用前端代碼調用合約,因此,在開發 DApp 之前,我們必須理解這一技術棧中存在哪些技術細節以及它們分別扮演何種角色。
- 智能合約:通常指代運行在 EVM 兼容網路中的 Solidity 或其他合約語言代碼,他們負責與用戶交易我們發行的資產並儲存 DApp 的鏈上狀態。
- DApp:整合合約接口以及其他功能的應用程序界面,目前,它們大部分是 Web App,你可以用流行的框架例如 React / Vue 來進行編寫。
- Provider/Signer:這是一個 DApp 架構中特殊的角色,它負責與區塊鏈進行通信,並進行合約的讀/寫操作。Metamask 是一個流行的 InjectProvider(Web3Provider)你也可以使用其他 JSON-RPC Provider 與區塊鏈進行通信。
- Relay:這個角色隱藏在 Provider/Signer 之後,是真正負責我們與區塊鏈的某一個節點同步狀態的服務器集群,它保存了所有帳本(全節點)它通常是 Infura、Alchemy、Quicknode、Moralis 或者 Pocket 提供的服務。
- 服務端(可選):大部分 DApp 仍然有他們的服務端邏輯,這意味著,你需要自己搭建服務環境,或使用流行的 BasS/FaaS 服務,你可以使用深度整合區塊鏈的 Moralis 來完成服務端的開發,也可以使用成熟的 Firebase 體系。當然,你也可以挑戰完全不依賴服務端的方式來構建 DApp,就像 Uniswap 所做的那樣。
現在,我們知道編寫一個 DApp 大概需要哪些領域的知識,如果你已經決定邁向下一代互聯網並打算闖蕩一番,我會在接下來的內容中仔細介紹這些角色分別需要理解哪些編程語言,框架和庫。
讓我們先進入最重要的部分,智能合約。大量程序員望而卻步的重要門檻是,他們認為智能合約需要學習一門新的編程語言,Solidity,這毫無疑問,我非常推薦入門 Web3 的程序員—— 無論你是從哪一個軟件開發領域轉型而來 —— 從 Solidity 入手學習 DApp 開發。
在智能合約的編碼方面,我們目前有許多工具,但認識和理解 Solidity 非常有必要,大量的已經存在的,和流行的合約都使用它進行編碼,因此,學習 Solidity 不但有助於幫助你理解區塊鏈開發的基本知識和概念,還能讓你在許多優秀的開發者已有的卓越工程上快速起步。
就編程語言而言,在目前的 EVM 兼容鏈上,你可以使用 Solidity 或 Vyper 進行開發,在其他 L1s 區塊鏈上,例如 Solana,你可以使用 Rust 來進行合約的開發;在 Layer2 方案 StarkNet 中,你可以使用 Cairo 來進行開發;在 Arweave 儲存網絡中,也存在著類似 3em 這樣的運行環境支持你使用 JavaScript 來編寫合約。
在這些百花齊放的方案中,實際上存在著兩種不同的合約運行環境,EVM 或非 EVM 方案,前者的代碼都會被編譯成 EVM bytecode,而後者則會採用各種各樣的 runtime,各顯神通。
這篇文章不會在合約編程語言上討論太多,我認為,我們目前正處於合約 runtime 的戰國時代,沒有人能斷言哪種合約編程語言的地位會成為 Web 世界的 JavaScript。但對於智能合約編碼來說,我們必須要了解和熟悉 Solidity,這是毫無疑問的。
關於 Solidity,我推薦你從 Solidity by Example 教程開始學習:
這一教程沒有繁瑣的語法介紹,而根據范例幫助讀者掌握基本知識,因此,完成這一教程大約只需要不到一個工作日。 Solidity 並不是一個特別複雜的語言,在使用它時,我們可以逐步理解每一項語句的語義,我推薦你設置好編碼環境後按照網站上的範例來進行實踐。當你已經掌握所有範例的寫法之後,可以打開 Solidity 語言官方文檔(中文)對照編碼中的錯誤來進行針對性的學習:
Solidity — Solidity 0.8.13 documentation
理解並掌握智能合約後,我們可以進入 DApp 的編碼,這是許多互聯網行業從業者的強項,我不會在此贅述關於前端編碼的經驗,如上所述,我們可以使用流行的前端框架,例如 React 或者 Vue 來進行 DApp 的編碼。毫無疑問,你會需要一些前端的技術棧知識,主要是 JavaScript 與 CSS。
在此,我想向大家推荐一些優秀的前端庫,使用這些代碼庫來進行合約交互,會使我們的開發效率事半功倍。
以 React 為例,我們可以使用 wagmi 來幫助我們更好的操作合約,它集成了大量基礎但夠用的 hooks,並提供了與外部 Provider/Signer 交互的快捷函數。與此同時,wgami 沒有過多的外部依賴,它的核心依賴只有 ethers.js
Github – tmm/wagmi: React Hooks for Ethereum
如果你不是一個框架愛好者,想要從零開始構建應用程序,不可避免的,你需要使用 ethers.js 或者 web3.js 來進行基本操作。從我自己的使用經驗來看,我更推薦 ethers.js
web3.js – Ethereum JavaScript API — web3.js 1.0.0 documentation
一般來說,我們並不需要其他的庫為我們提供專門的 Provider/Signer 支持,如果你打算支持更多複雜的 Provider,或者同時支持多網絡 Provider/Signer 的讀寫功能,類似 Apeboard 為它的用戶提供跨區塊鏈的數據展現,可以參考 react-web3 或者 w3modal 兩個流行的模塊,這些模塊提供了一些好用的功能,但他們的設計不夠解耦,有時會帶來不必要的 bug,對此,我保持謹慎推薦。
Github – Web3Modal/web3modal: A single Web3 / Ethereum provider solution for all Wallets
進一步,如果你不想支持外部 Provider/Signer,而為自己的用戶構建一個 Web 錢包,你可以使用 ethers.js 從零開始構建。
如果你想為用戶提供一個 onboard 體驗更好(但更不去中心化)的託管錢包系統,讓他們可以從普通的賬戶密碼或者社交網絡賬戶來登錄你的 DApp,可以選擇採用 Web3Auth 或 MagicLink 的方案。託管錢包系統是一個非常大的話題,感興趣的讀者可以參考上述兩個解決方案自行研究。
Web3Auth:Auth infrastructure for Web3.0 wallets and applications
Magic:Delightful Web3 auth, wallet creation, and key management
在理解合約以及 DApp 使用何種方式與區塊鏈進行交互後,開發者很快會意識到,我們並沒有通過在本地建立一個節點的方式來與區塊鏈進行操作。如果你在本地部署過 IPFS,你會很快發現它會默認在本地同步節點,就像 BT 下載軟件那樣。這是否意味著我們的 DApp 不夠「去中心化」呢?
實際上,仍然有大量的軟件基於本地的全節點來進行交互,只是,對於大部分開發者而言,他們放棄了這樣的權利,而轉而使用更便利的 Relay Network 與區塊鏈進行通信,通過這種方式,我們節省了部署成本,並且不再需要維護節點的狀態緩存,對於快速構建 DApp 來說,選擇一個靠譜的 Relay,是無可非議的方案。
使用 Relay Network 不需要特殊的知識,在前端,我們使用上述提及的代碼庫(ethers.js 或者 web3.js)與 Relay 進行交互;在服務端,如果你使用 Node 運行環境,也可以直接拷貝前端的代碼來使用。如果你使用其他的運行環境,你可能會需要一些特定的 JSON-RPC 函數包裝,以訪問這些 Relay。
Infura 是世界上最早和最大的以太坊 Relay Network,它提供一些公開的 Gateway 節點,但一般來說,我們需要獲取屬於自己的 DApp Access Key 並為這些訪問權限設置 origin 和 IP 限制,以提升使用我們自己的 DApp 用戶的訪問速度體驗。 Infura 目前支持 ETH,ETH2 網絡,以及 IPFS 和 Filecoin 兩個分佈式儲存方案。
Infura:EthereumAPI|IPFS API & Gateway|ETH Nodes as a Service
Alchemy 也是一個非常流行的 Relay Network,它在 Infura 的功能上更近一步,為開發者提供了相當多實用的功能,例如調試工具,區塊狀態推送與豐富的 Webhooks。從某種意義上說,Alchemy 不是一個單純的 Relay Network,它更像是一個 SaaS 服務,它提供了豐富的自定義 JSON-RPC 方法,實際上,我們的函數庫與它的緩存網絡進行交互,而不是直接與區塊鏈節點進行交互,這在很大程度上提升了視圖(view)方法的訪問速度,但依賴 Alchemy 獨有的 JSON-RPC 方法,也讓 DApp 變得更加中心化了。
我不會在這裡評判去中心化的「道德問題」,各位讀者可以根據自己的開發時間週期,風險偏好和使用習慣來決定何種服務適合自己,並為自己的客戶與用戶提供更好的服務。
在 Relay Network 方面,我想再推荐一個服務:
Moralis:The Ultimate Web3 Development Platform
Moralis 集成了許多 FaaS 的功能到他們的 Relay Network 中,這使得你可以快速在服務端訪問區塊鏈的狀態,而不需要反複調用第三方網絡的 API,這是一個非常有趣而實用的方案,他們的定位是 Web3 的 Firebase,我希望他們能夠將軟件質量和可用性真正提升到 Firebase 的水平,那這就會是一件非常棒的事兒。
在本文編寫的過程中,我得知 Google Cloud Platform 也正在組建他們的 Web3 團隊,這意味著我們有可能在不久的將來能在 Firebase 或者 GAE 服務上使用到 Google 的 Relay 服務,我們可以保持適當的關注。
服務端方面,你可以使用任何你喜歡的編程語言,運行環境和軟件架構,沒有什麼特殊的限制,只要保證你選擇的技術棧能和本地節點或者(通常是)Relay Network 進行交互即可。
一般來說,我會選擇 Node 運行環境。說些題外話,由於大部分合約使用 NPM 來進行包管理,並使用 hardhat 來做編譯和測試工作流,使用 JavaScript 已經成為智能合約編碼中必不可少的一個環節。既然如此,在服務端同時使用 JavaScript 語言有助於我們復用代碼,留出更多的時間享受人生。
編寫服務端並不意味著我們需要做完所有事,通常,我們使用 DApp 的服務端代碼來儲存沒必要儲存在合約中的「鏈下狀態」。在合約中儲存數據是十分昂貴的選擇(至少目前看來)這種昂貴不僅涉及到我們部署合約中產生的費用,還涉及到每一次修改狀態的函數請求帶來的,用戶需要付出的 gas 成本。所以,大部分時候,我們會使用自己的服務端來儲存這些「鏈下狀態」
使用一個健壯的 FaaS 對許多工程師來說是簡單而且實用的選擇,我推薦 Firebase,如果你想體驗深度集成區塊鏈的 FaaS,也可以參考上述提及的 Moralis。
我選擇 Firebase 的主要原因是他們提供成本低廉,服務完善和穩定的健壯 API,同時,他們針對開發者開發了功能齊全的本地模擬測試套件,這會節省我們相當多的時間。
Firebase Documentation|Introduction to Firebase Local Emulator Suite
FaaS 在市面上有太多可選的方案,你可以依賴一個全功能 FaaS,也可以將自己為數不多的「鏈下狀態」儲存在 headless CMS 當中,例如 Vercel 或者 Netlify。
Vercel:Develop. Preview. Ship. For the best frontend teams
Netlify:Develop & deploy the best web experiences in record time
或者,如果你希望自己搭建 FaaS 服務器,以獲得更完善的控制與更低的成本,我向你推荐一些 Firebase 的開源替代品,例如 Supabase:
2. 智能合約編碼
在這一章節,我們會從 Solidity 語言入手,理解編寫一個智能合約與傳統的應用軟件或界面有何不同,你可以使用上一章節提到的其他智能合約編程語言,但本章節將使用 Solidity(以下簡稱 Sol) 作為範例闡述智能合約編碼中應當注意的問題。
在此,我不會逐行逐句解釋 Sol 語言的語義細節,因此,閱讀這一章要求你有起碼的 Sol 語言知識。我建議,在此之前,請參考並讀完所有的 Solidity Examples:
2.1 合約特徵
事務性:我們可以將區塊鏈看成是一個事務性數據庫,這意味著,要么我們在合約中編寫的函數全部被執行,狀態依次被修改,要么,所有的狀態都會回滾到當初未曾被修改的樣子。這意味著,我們在對智能合約進行編碼的過程中,要十分注意函數 API 的設計,在具體的函數中,不應當對參數進行重載。同時,也意味著我們在進行錯誤處理時要十分小心。
錯誤處理:我們可以選擇兩種常用的錯誤處理方式,require(condition, ERR_MESSAGE) 或者 revert customError(),前者傳入一個字符串代表錯誤,後者可以自定義錯誤類型。兩種方式並無本質上的不同,並且都會導致 tx 失敗。對於前端而言,我們都需要自定義錯誤類型來捕獲這兩種錯誤。
運行成本:合約的狀態儲存會消耗 Gas 費用(區塊鏈的激勵機制,作為付給運行節點的計算與儲存費用)為此,在設計儲存對象時,如何善用聲明的內存是需要被考慮的問題之一。簡單的法則是,不要為不需要的狀態聲明過多的內存空間,如果你需要優化一個合約的運行成本,可以考慮參考許多合約使用內聯彙編來優化內存佔用。
為此,合約中的複雜數據結構必須聲明儲存空間位置,例如 storage, memory, calldata,每種位置所產生的費用會有很大不同。合約的函數也會有對應的函數類型聲明,view 函數 與 pure 函數在外部調用時不需要承擔 gas 費用,但改變狀態的函數都需要消耗 gas。
注意:由於合約運行和儲存成本高,許多對外部白名單進行管理的最佳實踐是使用 MerkleProof,你可以在這裡找到它的合約實現和 JavaScript 實現。
不可變:合約一旦部署,就無法動態替換或進行升級,這意味著,你需要在部署前考慮是否要依賴可升級架構(Proxy 部署方案)這些方案所依賴的合約和抽象合約,都需要遵循同一種初始化範式,才能保證合約的可升級性。
權限和可見性:合約不同於服務端代碼,它對網絡中的所有人是透明的,這裡的透明不僅指的是合約的字節碼,還包括它的公開和私有狀態。這意味著,你不應當在合約中儲存任何敏感數據,也不應當依賴區塊當中的任何狀態(比如區塊高度和時間戳)作為核心業務邏輯的判斷基準。
為此,發布一個未經權限控制的合約是十分危險的,任何外部賬戶都可以輕易地對某個合約進行修改,並通過發送消耗合約指令將合約中的資產轉走。所以,除了特定的治理合約不受權限管制以外,我推薦任何合約都必須至少依賴 Ownable 來進行基本的權限配置,同時,複雜合約可以使用 AccessControl 來進行管理。
安全性:如上所述,合約的安全性是非常重要和嚴謹的問題,在將合約發佈到生產環境網絡之前,確保你已閱讀 ConsenSys 編寫的合約代碼安全最佳實踐指南並遵守其中所有的約定,同時,確保合約有足夠的測試用例並且較高的測試覆蓋度。請不要帶有僥倖心理髮布未經任何測試的合約代碼,並主觀地希望它能夠正常工作。
2.2 合約依賴與調用
依賴引入:合約可以通過 import 引入依賴的外部合約,抽象合約,Interface 或者庫。通常,我們使用 npm 管理合約的外部依賴,管理合約的依賴也有其他辦法(例如 git submodule)這會在工作流章節中詳細敘述。
調用:合約可以調用其他合約,只需知道地址和 ABI,我們就可以在合約內部調用其他合約,需要注意的是,調用合約也是事務性操作,因此,你不需要通過手動管理異步操作的方式來等待返回結果。在合約內部調用其他合約需要消耗額外的 Gas 費用。調用合約可能由於 ABI 錯誤或者不支持某個函數方法而導致失敗,但 Gas 費用並不會返還,我們需要確保在調用其他第三方合約前理解對方合約的接口(包括參數類型,順序,返回結構)
如果你試圖調試本地合約調用某個生產環境的線上合約,可以使用 fork 的方式將某個高度的區塊鏈下載到本地運行,這會在工作流章節中詳細敘述。
ABI:也叫應用程序二進制接口(Application Binary Interface)ABI 是我們理解如何操作一個合約的具體方法的描述,通常在 Interface 文件中被定義(如果合約命名為 Membership.sol,那麼它的 Interface 文件通常叫做 IMembership.sol)
注意:通過這種方式定義可以讓任意合約通過引用 interface 的方式來調用你的合約,但如果你不在 Interface 中文件定義它,編譯器也能幫助你編譯出 ABI。
我們可以依賴完整的 ABI 來調用合約(對外部調用者來說,ABI 通常被編譯成一個 JSON 文件),也可以使用它其中的一部分來調用,只要它滿足真實合約所聲明的函數(包括參數,參數類型,返回值,返回值類型都一致)後者通常被成為 human-readable ABI,例如:
calldatas[0] = abi.encodeWithSignature( 'execTransfer(uint256,address,address[],uint256[])', memberId, memberWallet, payroll.tokens.addresses, payroll.tokens.amounts );
合約事件:由於合約的函數調用是事務性的,並且無法為外部調用者(指代 DApp 或錢包用戶)提供返回值,合約引入了事件的概念。
事件通過向日誌系統中寫入特定數據的方式來實現函數修改的記錄。我們可以通過監聽和查詢的方式列出一個合約註冊的所有事件,實現對函數異步結果的查詢和前端 UI 狀態變更。合約事件以某個單一合約為 key 來進行索引,同時,在聲明事件時,我們可以指定不多於三個 index key 來確保 DApp 前端對這些索引 key 的查詢效率,例如:
event ModuleProposalCreated( address indexed module, bytes32 indexed id, address indexed sender, uint256 timestamp );
如果你期望的查詢是非常複雜的,包括一系列相關聯的合約事件,更好的方法是採用 Relay 提供的 graph/webhook 來進行查詢。
創建合約:我們可以通過合約創建其他合約,這意味著,合約可以成為其他合約的工廠合約或者代理合約。我們也可以通過外部調用者(錢包賬戶)向 0x00 地址發送合約創建操作來新建網絡上的合約,這是我們進行測試和依賴工作流創建合約的方法。
創建合約需要消耗大量 Gas 費用,通常,我們會使用特定工具在創建合約前預估併計算費用,這會在工作流章節中詳述。
2.3 合約編程語言特徵
Sol 需要依賴相應工作流被編譯成字節碼發佈到對應環境的網絡中才能被運行,因此,它不像 JavaScript 那樣的動態類型語言有隨處可見的 runtime,編譯器在檢查時會幫助我們發現大部分問題,因此,你需要一個 IDE,例如 VSCode 之類的 IDE 或編輯器才能進入合約開發。
Sol 與大部分編程語言類似,支持基本多種數據類型(但不支持浮點數)、複雜數據結構(例如 map,array 和 struct)、合約支持繼承和多重繼承(is)、原型方法重寫(override)等。合約有特殊的構造函數,合約聲明的函數支持修飾器語法。特殊地,合約中可以通過 payable 聲明或顯示轉換來實現對原生 Gas token (ETH) 的資金操作。
Sol 雖然是圖靈完備的語言,但其中復雜結構的操作會帶來相應的 gas 消耗,因此,在設計合約中的狀態變量時,應當足夠清晰和簡單。
2.4 閱讀優秀的合約代碼
合約編程雖然不復雜,但大量的運行時限制和非冗餘的設計,導致我們在進行合約編碼時,不得不參考許多優秀的合約代碼,才能保證我們的合約代碼質量。
對於許多其他領域的程序員來說,這一步更是非常必要的。我推薦大家在合約編碼的過程中,反复參考優秀合約項目的設計思路和編碼思維。在這裡,我為大家推荐一些我認為不錯的智能合約開源項目。
首先,OpenZeppelin 合約是進入 Web3 領域必須反复的閱讀的聖經之一,自 2017 年以來,他們實現了大量的 EIP(以太坊改進提案),並成為了智能合約編碼的實際標準。雖然,OZ 的合約在 Gas 費用和效率上存在一些問題,但他們在安全性、代碼完成度、可維護性、註釋和測試方面都做的很好,是值得信賴的合約基礎庫。最近,OZ 也發布了他們在 StarkNet 上的 Cairo 語言版本合約。
Solmate 也提供了一系列對應的 EIP 實現,同時,他們更注重合約的運行效率,優化了執行中的 gas 費用,並且每個合約依賴更少,閱讀起來更加簡單。
ERC721A 是知名 NFT 項目 Azuki 發布的 ERC721 改善版本,通過特定的位操作,他們實現了內存佔用的優化,帶來了批量 mint 低 Gas 費用的優勢。如果你的項目涉及到大量 NFT 的鑄造,可以參考它的合約代碼來進行實現。
Github – chiru-labs/ERC721A: https://ERC721A.org
Compond 是 DeFi 借貸領域的老牌項目,代碼質量經過實踐的檢驗,如果你的項目涉及到 DeFi 相關的需求,請務必閱讀他們的合約代碼。
Github – compound-finance/compound-protocol: The Compound On-Chain Protocol
Uniswap 是世界上最大的 DEX,他們的合約實現的非常優秀,無論你是否有 DeFi 方面的需求,我都建議你完整閱讀他們的合約代碼。
Github – Uniswap/v3-core: 🦄 🦄 🦄 Core smart contracts of Uniswap v3
Lens 是 AAVE 推出的以 NFT 為核心的新型社交合約開發套件(或者他們稱之為社交合約協議)如果你的項目設計到 SocialFi,可以參考他們的代碼實現。
Github – aave/lens-protocol: The Lens Protocol
其次,我想給大家推薦的是 Zora v3 版本合約與 Gonsis safe,前者是著名的 NFT 交易市場退出的交易合約,後者是著名的多簽名錢包合約實現。這些都是我們在使用智能合約能夠完成的產品當中非常重要的組成部分:
Github – safe-global/safe-contracts: Gnosis Safe allows secure management of blockchain assets.
最後,如果你對 DAO 和鏈上治理感興趣,我推薦你閱讀我編寫的 CodeforDAO 的合約,在這個項目中,實現了傳統的治理模式,多簽積極治理與模塊化合約。
Github – CodeforDAO/contracts: smart contracts of codefordao
3. 開發工作流與單元測試
當我們掌握編寫智能合約的編程語言後,便可以開始進行工程編碼,在這一章節中,我會介紹流行的 DApp 合約開發工作流和編寫單元測試的方法。
智能合於自 2017 年發展至今,已存在相當多的項目支撐合約開發中的工作流,如今,大部分項目使用 Hardhat 來支持本地開發工作流。
Hardhat:Ethereum development environment for professionals by Nomic Foundation
Hardhat 提供了一種簡單的方式創建本地 EVM 兼容區塊鏈開發的環境,並且支持直觀的 debug 方式,此外,還有豐富的插件社區,幫助開發者完成一系列特定的需求。
依賴 Hardhat,我們可以在本地創建 block 快速確認的開發環境,使用公開私鑰的調試錢包作為測試用戶,編譯合約並發佈到本地測試網絡,編寫並在內存網絡中快速運行單元測試,如果你是 DApp 開發的入門新手,使用 Hardhat 是作為工作流最簡單和直接的方案。
Hardhat 還支持配置不同的區塊鍊網絡並通過工作流部署合約到生產環境,或者將某個高度的區塊鏈 fork 到本地創建集成測試環境。它提供豐富齊全的文檔,可以在他們的官網進行參考。
3.1 整合 Hardhat 工作流
有兩種方法可以簡單將 Hardhat 整合到你的合約項目,最簡單的方法是採用 npx hardhat 嚮導,它會幫助你在本地建立一個特定的腳手架項目並安裝對應依賴。
如果你打算在已經初始化的項目中引入 hardhat 工作流,手動的方式是新建一個配置文件 hardhat.config.js 並安裝對應依賴 npm install –save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
hardhat 工作流非常簡單,我們不需要花太多時間理解它,對於前端工程師而言,它就像是智能合約領域中的 Webpack(但沒有 Webpack 的配置那麼複雜)它預設了一系列的 task(任務)幫助你將 Sol 文件編譯為字節碼並輸出對應的 ABI 到本地,當然,它也幫助你整合單元測試環境並運行測試。
Hardhat 任務:
對於一般的合約開發而言,常用的任務是:
- npx hardhat node 運行一個本地區塊鏈節點並將 JSON-RPC endpoint 暴露給客戶端。
- npx hardhat test 運行合約的單元測試。
- npx hardhat deploy 發布合約的字節碼到某個區塊鍊網絡,網絡地址和 deployer 賬戶,這些我們可以在 hardhat.config.js 文件中配置。
整合插件:
hardhat 工作流還支持插件機制,插件能夠將特定邏輯作為 hook function (鉤子函數)插入到對應的 task(任務)當中,所以,當我們執行某個任務時,需要確認它是否依賴了某個插件,否則它可能有與預期不同的行為。
引入插件的方式是,首先使用 npm 安裝這個插件,再於 hardhat.config.js 配置文件頭部引入即可。
HRE 運行環境變量:
當我們在 JS 文件中引入 hardhat 時,HRE 會被插入運行環境。一些插件可能會拓展 HRE,將他們的實用函數方法插入到 HRE 中,類似地,我們也可以使用這種方法構建特定的 hardhat 插件。但一般來說,我們不需要這麼做。
HRE 會在我們執行 npx hardhat run 任務時被自動插入到全局變量中去,我們可以通過這種方法編寫某些簡單的合約發布腳本或合約交互腳本。
3.2 單元測試
編寫合約的第二步是編寫合約的單元測試。當我們運行 npx hardhat test 任務時,hardhat 會自動尋找 ./test 文件夾下的單元測試並運行它們。這個默認的地址可以在 hardhat.config.js 配置文件中使用 path.tests 修改:
// Rewrite the `./test` folder to `./tests` paths: { tests: './tests', },
運行測試所需要安裝的依賴可以在這個指南上找到:
Hardhat:Testing with ethers.js & Waffle
與傳統的單元測試一樣,使用 Mocha 作為單元測試框架。對於合約特定的變量類型,我們使用 Waffle 和 chai 作為斷言庫。
一般來說,我們在 hardhat.config.js 配置文件頭部引入測試輔助插件 @nomiclabs/hardhat-waffle 可以幫助我們解決大部分問題,而不需要額外手動安裝 mocha, waffle 和 chai 並進行配置,與前一小節所提到的 HRE 相關,它們會被自動插入 HRE 運行環境。
關於合約事件,合約方法調用,BigNumber 等完整的斷言庫範例可以在這個文檔中找到:
Chai matchers—waffle documentation
注意:合約的單元測試中可以使用 contractInstance.connect(signer) 來隨意改變調用合約的外部賬戶。
3.3 改善測試效率
編寫單元測試首先需要我們在測試鉤子中編寫發布合約的代碼,這意味著,我們需要在每次 beforeEach 鉤子中重新發布我們的合約並使其從零狀態開始運行。
即使 hardhat 支持在內存中運行區塊鏈並整合了單元測試流程,但這樣反复的發布合約也會極大拖慢測試速度。
因此,就單元測試的最佳實踐,我向大家推薦 hardhat-deploy 插件。
hardhat-deploy 插件支持使用 evm_snapshot 快速地跳轉到某個高度的區塊鏈狀態,因此,我們可以使用它在單元測試中維護測試前、中、後以及各種特定高度狀態,極大地加快測試速度。
Github – wighawag/hardhat-deploy: hardhat deployment plugin
注意,引入 hardhat-deploy 插件,需要修改對應的 @nomiclabs/hardhat-ethers 插件來源,這可能會導致在未來的 npm install 中帶來版本衝突,如果你遇到了版本衝突,可以使用 npm install –force 跳過版本依賴檢查,強制安裝兩者。
"devDependencies": { "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers", "hardhat-deploy": "^0.11.2", ... }
簡單來說,在單元測試中,我們可以使用:
await deployments.fixture(['SomeContractName']);
來確保在測試執行前跳回某個狀態。如果你需要更加複雜和自定義的 fixture(而非直接跳回某個合約發布後的干淨狀態)可以使用 deployments.createFixture 來創建自定義 fixture,具體的範例代碼和指南可以在這裡尋找到:
Github – contracts/helpers.js at main · CodeforDAO/contracts
Github – wighawag/hardhat-deploy: hardhat deployment plugin
值得注意的是,使用 hardhat-deploy 插件會同時改變我們發布合約的代碼邏輯(正如其名)它支持在 ./deploy 文件夾下編寫每個合約的發布腳本。事實上,默認的 deployments.fixture 正會退回這些發布腳本所敘述的合約狀態。
不同於 hardhat 默認的發布腳本(使用 npx hardhat run)我們可以使用 hardhat-deploy 插件提供的功能在發布腳本中做更多工作,例如使用 execute 函數立即修改發布後的合約狀態,這會在對權限敏感的合約當中非常有用:
const { deploy, execute } = deployments; const shareGovernor = await deploy('ShareGovernor', { contract: 'TreasuryGovernor', from: deployer, args: [name + '-ShareGovernor', share.address, treasury.address, settings.share.governor], log: true, }); // Setup governor roles // Both membership and share governance have PROPOSER_ROLE by default await execute( 'Treasury', { from: deployer }, 'grantRole', PROPOSER_ROLE, membershipGovernor.address );
除此之外,hardhat-deploy 插件還提供了非常多的 HRE 實用函數,例如getNamedAccounts 能幫助我們命名本地測試賬戶,而非使用數組下標訪問它們。你可以參考該插件的 GitHub 主頁了解這些實用功能。
3.4 測試覆蓋率與 Gas 報告
當合約的單元測試編碼到一定程度之後,我們會希望這些合約被發佈到某個測試或生產環境(例如測試網絡或 ETH 主網)時是否是健壯和低成本的,在此時,我向你推薦兩個 hardhat 插件 hardhat-gas-reporter 與 solidity-coverage
hardhat-gas-reporter 插件幫助你了解運行單元測試中部署和執行合約方法消耗的 gas 費用,如果在本地環境變量中提供 COINMARKETCAP_API_KEY,它會自動將這些成本折算為美元或其他法幣計價。
solidity-coverage 插件提供單元測試覆蓋率報告,這有助於開發團隊理解合約是否得到了應有的測試。
Github – sc-forks/solidity-coverage: Code coverage for Solidity smart-contracts
3.5 其他實用插件
如前所述,hardhat 並不需要以來太多的插件就可以正常工作,滿足大部分合約開發團隊的需求,我在這裡推薦兩個其他的使用插件,他們是 @nomiclabs/hardhat-etherscan 和 @tenderly/hardhat-tenderly
這些插件都是可選的,並依賴第三方服務的 API Key,各位讀者可以根據自己的情況選擇是否使用他們。
hardhat-etherscan 插件將 etherscan 網站的源碼 verify 功能整合到發布工作流中,能夠將所發布合約的源碼和 ABI 都展示在合約地址頁面。
hardhat-tenderly 插件整合了 Tenderly 工作流,後者是一個新興的 CI/監控 平台,能夠幫助我們監控線上合約的狀態並提供 debug 建議。
Tenderly|Ethereum Developer Platform
Github – Tenderly/hardhat-tenderly: Tenderly plugin for HardHat
3.6 更快的工作流方案:Foundry
儘管在過去的三年中 hardhat 逐漸壟斷了 EVM 兼容鏈中的智能合約開發工作流市場,但最近也有很多強有力的競爭者出現,Foundry 就是其中之一:
Foundry 由 Rust 語言編寫,並在很多方面極大地提升了合約單元測試的運行效率:
Foundry 由它的命令行工具 Forge 與 cast 組成,前者幫助我們安裝第三方依賴組件(使用 git submodule 方式)運行測試,發布合約,後者幫助我們與合約進行 RPC 通信交互。
同時,它也改變了我們編寫測試的方法,讓開發者可以直接編寫 Sol 而不再依賴 JavaScript 就可以編寫合約的單元測試,測試文件與合約源碼在同一個文件夾中管理,通常以 ContractName.t.Sol 特殊後綴結尾。
它也提供了一系列的測試套件工具幫助我們編寫基於 Sol 的單元測試,包括可繼承的 Test 合約,和一個特殊的,與 vm 通信的合約 Cheatcodes 幫助我們改變外部調用者地址,進行錯誤斷言等等功能。
如果你對 Foundry 感興趣,我推薦你閱讀他們的文檔,它的學習曲線並不陡峭,但考慮到使用它會改變絕大部分智能合約項目的開發流程,我推薦你在本地分支中支持它並同時兼容 hardhat 工作流。
4. 前端與客戶端開發
由於第三方錢包軟件的盛行,Web3 大部分產品(DApp)的用戶節目實際上都由前端網頁所構成,這與移動互聯網的開發流程相悖,但很像早期 Web2.0 歷史發展進程的一部分。
許多 DApp 並不提供 Mobile App 版本,部分原因是由於構建一個跨平台錢包方案過於復雜,以及大部分 Web3 領域內的用戶都在使用諸如 MetaMask 這類瀏覽器插件錢包,而它的移動端 App 體驗並不好用。
我們當然可以使用託管錢包服務來進行開發,讓更多不熟悉 Web3 的用戶使用郵箱或者密碼登錄,但這會帶來一系列的安全問題與風險,況且,就算我們能夠使用簡單易用的錢包降低用戶准入門檻,在許多國家,用戶仍需要復雜的 kyc 才能獲取到某些 token。
所以,我建議你也採用 Web 前端作為第一個 DApp 的界面方案。
在本章節,我們所涉及到的大部分內容都是基於這樣的假設,因此,這部分內容需要你熟悉 JavaScript,React.js/Vue.js 和它們相關的工作流。
4.1 前端框架的選擇
使用何種視圖框架並不會影響你的 DApp 體驗,但是,這會影響到開發效率。
在本文的第一部分「認識 DApp 技術棧」我們介紹了 ethers.js 和 web3.js,這兩者都是構建 Web 前端的基礎類庫。如上所述,我建議使用 ethers.js 入門進行開發。
事實上,相較於 Vue 而言,React 的生態系統中目前擁有較多的活躍 Web3 開發者和相關依賴庫,如果你沒有特殊的偏好,可以嘗試先採用 React 作為框架來進行開發。
另外,大部分對於 ethers.js/web3.js 的包裝項目,諸如 web3-react,wagmi 等都依賴你理解 React hooks 的概念,所以在你著手進行編碼之前,需要查閱它的文檔。
4.2 搭建腳手架項目
我們可以使用任何藍圖工具創建對應的視圖框架的腳手架項目,但是,我們也可以參考現有的 Web App 項目來進行開發。
在編寫前端代碼之前,我推薦你參考流行的腳手架項目 scaffold-eth
scaffold-eth 是一個完整的合約腳手架,它的 packages/react-app 文件夾是它的 Web 前端代碼,對於熟練合約開發的工程師而言,這個腳手架的組織方式有些不太理想,但對於剛入門 Web3,打算搭建第一個測試項目的朋友來說,這是個很棒的入門工具。
Github – scaffold-eth/scaffold-eth: 🏗 forkable Ethereum dev stack focused on fast product iterations
當我們決定了使用何種視圖框架來開發前端 Web App 之後,就可以著手使用它對應的腳手架 cli 來創建項目了。對於沒有任何偏好的讀者,我推薦你使用 React + Next.js 來初始化新項目,Next.js 使用 React 作為基礎視圖框架,並提供了豐富的工作流,簡單的路由系統,好用的 SSR 與 FaaS 支持,當然,它也是一個非常好用 site builder 工具。
Next.js by Vercel – The React Framework
4.3 前端與合約的交互流程
當我們在 DApp 的業務邏輯編碼進行到一定程度後,需要與合約 ABI 進行讀寫,或者,我們需要連接用戶的錢包,為其鑄造一個 NFT,這裡就涉及到前端與合約的交互。
在本文的第一章節「認識 DApp 技術棧」中,我們提到與區塊鍊網絡進行交互最終會使用 Provider/Signer(前端) + Relay Network(區塊鏈端)因此,這個流程最終會使用 ethers.js 或 web3.js 發送對應的 XHR 請求到 Relay Network 的 API Endpoint。但是,具體而言,開發者和用戶如何理解這一與眾不同的流程呢?
一般來說,我們可以將這個交互流程簡述為:
- 用戶打開網頁,默認進入只讀模式。我們只能通過 Provider + Relay Network 訪問到我們所設定的默認網絡的合約的 view 方法返回值。
- 用戶點擊「連接錢包」時,我們和對應的 connector 進行通信,獲取連接狀態。如果用戶授權連接,我們可以通過 connector 獲取到用戶錢包的地址(0x)在此之後,用戶錢包的 Provider只讀模式將只支持用戶錢包中選定的網絡,如果網絡不符合我們的期望,我們可以通過 connector 的特定通信來請求用戶修改它。
- 用戶點擊某個表單,希望與合約的寫操作函數進行交互。此時, ethers.js 或 web3.js 會將交易信息打包發給 connector,後者將引導用戶進行簽名和確認交易的操作。如果交易成功提交到區塊鍊網絡(經由我們配置的 Relay Network)我們需要監聽該交易的狀態和合約事件,進行下一步前端狀態更新。
- 用戶進行錢包登錄操作或其他簽名操作時,我們可以不與合約通信僅在本地請求 connector來發行使用用戶錢包私鑰簽名後的加密數據。
在進行以上所述的流程之前,我們需要回顧 「認識 DApp 技術棧」中的內容,並且準備好 Relay Network的 access key,無論操作是讀或者寫,我們都需要準備這些 access key 才能為用戶提供高質量和穩定的請求訪問。
我們可以通過提供多個 Provider 實體的方式來訪問多個區塊鍊網絡的合約 ABI,但一般而言,我們只能依賴 connector中用戶選定的網絡來進行寫操作。
對於合約多個 ABI 的寫操作可以合併請求,這樣可以減少用戶在進行操作時的 Gas 費用,如果你有這個需求,可以參考 Multicall.js 的實現:
Github – makerdao/multicall.js: A JavaScript blockchain state management library for dapps
4.4 前端依賴
大量的重複工作都建立在優秀的開源項目基礎上,在 DApp 編寫過程中,我推薦你使用一些優秀的前端庫來減少工作量,並實現更好的代碼交付質量。
wagmi 是我推薦的核心依賴之一,它提供了豐富的 React hooks 來完成 DApp Web 前端與合約交互的所有流程。它的實現簡單,測試健全,而且沒有多餘的冗餘依賴庫。
Github – tmm/wagmi: React Hooks for Ethereum
談到 React hooks,我也推薦 useDApp,相比於 wagmi,它更加複雜,但默認支持 multicall.js
如果你的網站將要集成錢包登錄的功能,那你則需要考慮引入 Siwe(Sign-In with Ethereum)它實現了 EIP-4361 中的錢包登錄流程。
Github – spruceid/siwe: Sign-In with Ethereum library and example app
如果你的 Next.js DApp 計劃提供多語言版本和檢測,我推薦你使用 i18next 與 react-i18next 與 i18next-browser-languagedetector 這些依賴與 DApp 的核心交互邏輯沒有關係,因此不再贅述。
在 UI 庫方面,我推薦基於 Google Materials UI 設計系統的 MUI 與 NextUI:
Mui:The React component library you always wanted
NextUI – Beautiful, fast and modern React UI Library
4.5 客戶端開發
客戶端開發的方案比較多樣,流行的方案是 React Native(跨平台)Flutter(跨平台)Swift(iOS)和 Java (Android) 這些方案都有一些流行的依賴庫可以藉鑑。
考慮到 React Native(跨平台)的實現,它的依賴庫與 React 應當並無差異,可以使用上述針對 React 的方案。
對於 Flutter(跨平台) 而言,我推薦你參考這一官方指南:
Ethereum for Dart developers|ethereum .org
其中,我們可以使用 web3dart 來與區塊鏈 Relay 進行通信:
對於 Swift(iOS)而言,我們可以使用 Argent labs 團隊提供的 web3.swift 方案:
Github – argentlabs/web3.swift: Ethereum Swift API with support for smart contracts, ENS & ERC20
對於 Java (Android) 而言,流行的方案之一是 Web3j:
Github – web3j/web3j: Lightweight Java and Android library for integration with Ethereum clients
5. 開發、測試與生產環境調試
與其他軟件一樣,DApp 在正式上線過程前也會經過幾個環節的調試與測試過程。與其他軟件不同的時,我們通常無法簡單地在本地搭建所有測試環境。
5.1 開發環境調試
在「開發工作流與單元測試」章節中,我們提到使用 hardhat node 能夠快速在本地運行一個自動 mining 的區塊鏈調試網絡。那麼,我們如何將每次修改的合約 ABI 同步給前端項目呢?
默認地,hardhat 會將編譯後的 ABI 文件和合約字節碼放在 ./artifacts 文件夾,但不包括合約地址,文件組織方式對前端項目也並不友好。
借助 hardhat-deploy 插件,我們可以簡單地使用 –export-all 導出所有被發布的合約 ABI(包含地址信息)為一個完整的 json 文件,例如:
npx hardhat node --export-all ../website/contracts/localhost.json
這個 json 文件的結構看起來是這樣:
{ "31337": [ { "name": "localhost", "chainId": "31337", "contracts": { "Membership": { address: "...", abi: [...] } ... } ] }
可以看出,它是一個由 Chain ID(31337 是 hardhat Chain ID)索引的合約 ABI 清單,根據這個清單,我們可以很方便的構建出對應的合約實例並與本地合約進行交互。
注意:前端項目與本地合約進行調試時,請特別注意 Provider/Signer 當前連接的網絡。另外,默認地,hardhat 網絡的區塊確認是即時的,如果你需要模仿公開網絡的行為,可以在這裡尋找到修改它的配置。
5.2 測試與生產環境調試
當我們的合約準備發佈到公開測試網絡,例如 Rinkeby, Kovan, Ropsten 或者 Goerli 時,我們只需要在 hardhat deploy 中指定對應的 network 選項即可:
npx hardhat deploy --network rinkeby
需要注意的是,我們需要確保對應的 deployer 錢包賬戶中有足夠的 Gas Token 餘額,對於上述網絡來說,即是 ETH 餘額。
合約一旦發佈到公開網絡,它的狀態就不受到我們的控制,任何用戶都可以根據我們提供的 ABI 修改某個合約的狀態,因此,如果你需要穩定的,某種狀態的測試合約充當不同版本的測試環境,確保你在發布之前使用了特殊的權限管理或是地址硬編碼。
如果你不想如此麻煩地管理測試網絡,我推薦你使用 hardhat node –fork URI 功能在本地計算機或者服務器集群中 fork 主網狀態充當測試環境。你可以在這裡找到它的詳細指南。
公開測試環境中的第三方合約狀態是未可知的,因此,我們需要再三確認調用的地址是否正確。其次,大部分流行的協議或者 DEX 在幾大公開測試網絡都提供了測試合約,包括 OpenSea 在內,部分流行的 DApp 前端也提供了測試網絡的版本,以幫助開發者在發佈到線上網絡前發現問題:
此外,你需要一些測試 ETH 才能確保公開測試網絡中合約與邏輯正常工作,同時,你的用戶也會需要它們。這裡是一些可以獲得測試 ETH 的網站和服務:
Paradigm MultiFaucet|Bootstrap your testnet wallet
6. 服務端編碼與集成
服務端一直是 DApp 被認為沒那麼「去中心化」的原因之一。就我所知,世界上絕大多數 DApp 都有服務端 API 提供支持,只有少數類似 Uniswap 這樣的產品,僅依賴前端與合約進行通信。
但絕大多數 DApp 的 Web UI 實現了去中心化。因此,我們需要區分服務端 API 在 DApp 開發中所屬的地位,不能將核心邏輯放在私有服務器中依賴,或者一味使用服務端儲存的機器人錢包私鑰來操作區塊鏈。
我們需要編寫服務端 API 的原因之一是,鏈上狀態儲存的成本過高,以及反复地簽名與交互對用戶來說體驗不佳。另外一些原因是,一些不重要的,可以被丟棄的數據並不需要放在合約中儲存。
DApp 的服務端 API 並不要求特殊配置。因此,你可以使用任何你喜歡的編程語言運行環境來編寫它。一般來說,我們使用 Node,因為這樣可以復用一部分 Provider/Signer 的前端業務邏輯。
6.1 開發環境
如果你的 DApp 並不復雜,不需要儲存太多狀態,我推薦你使用上述章節提到的 Next.js 方案,它可以直接被 push 到 Vercel,後者將能夠自動地將你 Next.js 項目中的 API 部署到對應的服務環境。
如果你的 DApp 依賴較多的數據庫和服務,我推薦你使用 FaaS,我常用的一個 FaaS 服務是 Firebase,你可以使用 Firebase 快速連接實時數據庫,整合 Twitter 或 GitHub 的第三方登錄,它還提供非常好用的本地模擬器工具套件,以及,它能夠非常好地支持跨平台。
Firebase Documentation|將 Firebase 添加到您的 JavaScript 項目
服務端編碼沒有太多與 DApp 開發相關的內容,但其中有兩個我們需要提及的部分:
- 理解使用 Siwe 進行錢包登錄的流程
- 在服務端使用 Provider/Signer 與區塊鏈 Relay 進行通信
接下來我們會簡單介紹這兩部分內容。
6.2 關於錢包登錄
許多剛入門 Web3 的開發者會認為,錢包登錄只需要使用前端腳本連接錢包即可,但這種邏輯很容易被 hack,因為任何客戶端狀態都能夠被低成本修改。
Siwe(Sign-In with Ethereum)將 EIP-4361 草案引入了以太坊改進協議,目的是標準化開發者使用錢包登錄授權 Off-chain 產品的邏輯。它的流程與 JWT 的發行相似。
Siwe 支持絕大多數編程語言和它們的運行環境(JavaScript, Rust, Python, Golang, Ruby 等)因此你可以直接使用官方提供的依賴庫來完成大部分流程。如果你使用 Next.js 也可以採用 NextAuth 來快速整合它。
簡單來說,Siwe 通過對服務端發行的隨機 nonce 和其他標準輸入進行簽名,再通過服務端驗證簽名內容的方式來確認提交方的地址。
因此,我們需要提供兩個 API 來實現 Siwe,分別是 /nonce 和 /vertify 你可以在這裡找到它們的代碼範例:
Github – spruceid/siwe-quickstart
6.3 服務端與區塊鏈的通信
在某些情況下我們需要在服務端進行與區塊鏈 Relay Network 的通信,如果你使用 Node 作為運行環境,它的邏輯代碼和前端是一致的。
如果服務端只需要使用到 Provider 與 Relay Network 通信(只讀模式),比如,我們通過整合區塊高度,某個合約狀態和訂閱事件,來組建我們自己的數據緩存服務並提供 API,那我們只需要按照前端的方式與 Relay Network 建立通信即可。在這種模式下,我們可以把 access key 存放在生產環境的環境變量中,我推薦你使用 dotenv 去處理它們。
Github – motdotla/dotenv: Loads environment variables from .env for nodejs projects.
如果你使用 Next.js 它會自動讀取 dotenv 的環境變量文件,因此你可以在 process.env 中快速使用到它們。這裡是 Next.js 的相關文檔。如果你使用其他服務端框架或 FaaS,你可以自己維護這些環境變量文件。
如果服務端需要使用到 Provider 和 Signer 與 Relay Network 進行通信(讀寫模式)我們就需要好好考慮如何存放機器人錢包的密鑰的問題。
注意:在做這些事情之前,我們應該提前思考為何需要在服務端使用機器人錢包對某些合約進行寫操作,以及這樣的設計帶來的合約權限問題與安全風險。
我認為,將私鑰放在環境變量中不是一個好辦法,我們無法控制第三方模塊是否會將環境變量中的內容計入日誌或者遠程統計。因此,我們可以採用專業的私鑰管理服務來管理,例如使用 Google Secret Manager 或者 AWS Secrets Manager 來進行管理,而前者可以與 Firebase 很好地進行整合。
通過 Provider 和 Signer 與 Relay Network 進行通信只需要傳遞錢包私鑰給對應的 Provider/Signer 實例即可完成操作,與本地進行單元測試的機器人錢包一樣,它不會有額外的確認過程,因此,我們需要確認錢包中有足夠的 ETH 或其他 Gas token 餘額,否則該交易會失敗。
一般來說,如果我們的服務端接口中有對應的合約寫操作,我們不會等待交易完成再返回數據,因此,我們需要返回對應的 tx.hash 方便前端界面處理後續邏輯。
6.4 實用的 SDK
一些 SDK 和對應服務可以幫助我們更方便地在服務端與合約進行通信,例如 ThirdWeb:
thirdweb:Web3 SDKs for developers · No-code for NFT artists
Thirdweb 提供了一個 SaaS 合約開發平台,你可以通過它的前端 App 發布預設功能的合約,例如 NFT Drop 或者 NFT 交易平台,也可以使用它提供的第三方 access key 與已發布的合約進行通信(而無需依賴合約的 ABI):
import { ThirdwebSDK } from "@thirdweb-dev/sdk"; // The RPC url determines which blockchain you want to connect to const rpcUrl = "https://polygon-rpc.com/"; // instantiate the SDK as read only on a given blockchain const sdk = new ThirdwebSDK(rpcUrl); // access your deployed contracts const nftDrop = sdk.getNFTDrop("0x..."); const marketplace = sdk.getMarketplace("0x..."); // Read from your contracts const claimedNFTs = await nftDrop.getAllClaimed(); const listings = await marketplace.getActiveListings();
你可以在這裡找到它的 JavaScript SDK(Typescript)
Github – thirdweb-dev/typescript-sdk: Best in class web3 SDK for Browser, Node and Mobile apps
7. 合約部署方案 L1s & L2
在 DApp 開發中,與傳統產品最大的不同點之一是我們需要決定將產品核心邏輯的智能合約發佈到那個網絡(或者哪些網絡)這意味著,DApp 需要有「跨平台跨網絡」的支持能力。
對於傳統互聯網開發人員來說,我們很容易理解「跨平台」,它是指我們需要為 App 界面提供 PC Web/Mobile Web,iOS/Android App 的各種版本。 「跨網絡」在 DApp 的開發中指的是,我們需要讓 DApp 前端/客戶端支持多個區塊鍊網絡。在那之前,我們需要決定哪些區塊鍊網絡是我們首選的發布環境。
自 2017 年發展至今,目前市場中有大量的區塊鍊網絡供我們使用。按照它們的共識證明種類,可以被分為 PoS 和 PoW;按照它們的角色定位,可以分為 L1 與 L2;按照它們對 EVM 兼容的類型,可以分為 EVM 兼容鍊和非 EVM 兼容鏈。
一般來說,我們可以選擇發行到這些流行的區塊鍊網絡:
- Ethereum (ETH) 主網:Gas 費用昂貴,但其中儲存了大量資產,如果你的項目與 NFT 相關,許多人會選擇發佈到主網。
- Polygon (Matic):類 ETH 的 PoS 側鏈,EVM 兼容,在許多國家都有一定的用戶基礎,有限的 TPS 支持與可接受的成本,開發者友好。
- BNB Chain (BNB): 幣安的區塊鍊網絡,EVM 兼容,開發者友好。
- Solana (SOL): 高性能區塊鍊網絡,支持多種編程語言編寫合約,EVM 兼容(使用 Neon)
- AVAX C-chain (AVAX):AVAX 的應用鏈,EVM 兼容,提供快速區塊確認,相當程度的 TPS,可以自己搭建 C-chain 作為應用鏈 sub-chain(例如 DFK Crystalvale)
- Cosmos(ATOM): 連接應用鏈的區塊鍊網絡,非 EVM 兼容(Evmos 除外)提供快速的 IBC 跨鏈橋支持,可以自定義應用鏈的 Gas token,適合 GameFi 與需要定制 TPS 的大型應用。
- Near (NEAR):提供完善的開發者套件和網頁錢包一整套方案,因此用戶入門難度最低,非 EVM 兼容(使用 Rust 編寫合約)。
- StarkNet (ETH Layer2): 使用 zkRollup 技術支持的 L2 網絡,非 EVM 兼容,由 Starkware 提供技術支持(它同時支持了 IMX 與衍生品 DEX DyDx)支持與 L1 的合約進行通信,支持使用 warp 工具將 Sol 代碼轉換為 Cario 語言的合約代碼。
- zkSync2.0 (ETH Layer2): 使用 zkRollup 技術支持的 L2 網絡,同時支持了 DEX ZigZag。支持與 L1 的合約進行通信。
- scroll (ETH Layer2): 使用 zkRollup(zkEVM)技術支持的 L2 網絡。
- xDai(Gnosis Chain): 它支持了著名的到場證明合約 POAP。
- Harmony(ONE):高性能區塊鏈,它支持了 DFK 的第一個版本。
- Dfinity (ICP): 一個完整的 DApp 生態系統。
我們可以選擇發佈到某個區塊鍊網絡,或者發佈到所有支持 EVM 兼容的網絡中。不過,不同的區塊鍊網絡中的合約無法直接通信,資產也無法隨意互換(可以採用跨鏈橋合約進行鎖定和重新鑄造)目前,大部分 DApp 只會選擇某一個區塊鍊網絡進行發布。
如果你的項目涉及到 NFT,我會推薦發佈到 ETH 主網或儲存了相當數量資產的網絡,如果你的項目涉及 GameFi,可以考慮 TPS 高的區塊鍊網絡。如果你考慮 TPS 又同時注重資產安全,可以考慮使用 Layer2 網絡。
我們正處於一個區塊鍊網絡的戰國時代,因此,選擇部署的網絡不存在絕對的最佳實踐,可以參考個人的需求進行選擇。
8. 去中心化儲存方案
在 DApp 開發中,我們通常會將資產的元數據、DApp UI 界面等儲存在去中心化儲存網絡當中,以防止單點故障導致的資產損失和不可用。
簡單來說,資產的元數據通常指的是 NFT 中合約儲存的 tokenURI() 返回的內容,它可能是一個 JSON 編碼的字符串,將這些字符串儲存在合約當中需要耗費相當大的成本,因此,最佳實踐是將他們部署到去中心化儲存網絡中,再保存儲存對象的索引 ID(例如 IPFS CID)
流行的去中心化儲存方案有:
- IPFS:最早和最流行的去中心化儲存網絡。
- Filecoin:以 IPFS 為基礎的儲存網絡。
- Arweave(AR):去中心化的永存網絡,一次寫入付費,讀數據免費。你正在閱讀的這篇文章即由 Mirror 代為保存在 AR 上。
由於 IPFS 等方案需要多個節點保持(Pin)儲存對象的狀態,因此,上述服務都有針對開發者的高級包裝儲存服務(類似 AWS S3)我向大家推薦:
- Web3.storage 基於 Filecoin 的免費儲存服務。
- NFT.storage Web3.storage 提供的 NFT 元數據針對性儲存服務,提供網頁界面上傳與 SDK。
- Filebase 整合了多個去中心化儲存網絡的服務,接口類似 AWS S3,提供豐富的 SDK 與 API,支持信用卡付費。
- Bundlr 基於 Arweave 構建的永久儲存服務,支持用多鏈 token 結算。
如果你考慮為 NFT 元數據尋找去中心化儲存方案,需要確保 OpenSea 支持它們,否則你的 NFT 與合約將無法正常在 OpenSea 頁面中顯示。 OpenSea 目前支持 IPFS 與 Arweave 的儲存協議。
另外,如果你在編寫合約時預先硬編碼寫入 NFT 和其他元數據,可以考慮使用 IPFS CAR(Content-Addressed Archive) 來預先計算他們的 CID hash,這有助於保存一些 OpenSea 要求的非標準數據,例如 NFT 合約描述和 Banner 背景圖地址。
IPFS Content Addressed Archiver
9. 附錄
本文中提到的所有項目均列在我的 GitHub Star 清單中,可以在這裡統一查閱:
guo-yu’s list / DApp Best Practice Stack
此外,附錄中列出了許多我認為有助於幫我們理解 DApp 與智能合約開發的指南,如果你感興趣,可以參閱這些指南(這個列表也會不定時更新):
- 合約安全風險指南 Ethereum Smart Contract Security Best Practices
- 合約 Gas 優化指南 Awesome Solidity Gas-Optimization
- brownie:一個 Python 語言的合約工作流工具。
- ethers-rs:一個 Rust 的錢包實現。
- juice-interface:著名眾籌網站 juicebox 的前端界面。
- starknet.js: StarkNet 的 JS SDK。
- DFK/contracts: 著名 DeFi 遊戲 DFK 的智能合約。
- thirdweb-dev/contracts: ThirdWeb 開源的合約代碼。
- OpenZeppelin/nile: OZ 為 StartNet Cairo 語言編寫的工作流工具。
- Argent X:StarkNet 的開源錢包。