本文爲 dfuse 與 EOS Studio 合作內容,原文由 EOS Studio 發佈
我們將在近一段時間內陸續推出多期的系列教程,深度詳解一些開源的 EOS 智能合約項目。我們將仔細挑選那些內容優質、設計精心、並可以成功構建的合約示例,其中的一些已經在 EOS 主網上廣泛使用。通過本次系列教程,我們希望能爲 EOSIO 上的 dApp 開發者提供更多的學習資料,並幫助他們瞭解更多智能合約的設計模式和應用場景。
在第一期和第二期中,我們討論了智能合約 eosio.forum 的設計動機,以及進行提案和投票的基本流程。本篇教程中,我們將會帶大家一起閱讀源碼,解析技術細節,理解這些智能合約的具體運行原理。
eosio.forum 合約可以大致分成四個部分:proposal (提案),vote (投票),status (狀態),post (發佈和回覆)。所有的 actions (調用合約的方法) 和 tables (表) 都放在 forum 這個類裏面。源碼中定義的 action 和 table 我們都提供了指向源代碼的鏈接,方便您在需要時快速查閱和引用。
提案
整個 proposal 部分包含了一個名爲 proposals 的 table 以及一些用來運行這個 proposals 的 actions:
ACTION propose(eosio::name proposer, eosio::name proposal_name, string title, string proposal_json, eosio::time_point_sec expires_at)
TABLE proposals { eosio::name proposal_name; // primary key eosio::name proposer; // secondary key string title; string proposal_json; eosio::time_point_sec created_at; eosio::time_point_sec expires_at; }
所有的賬號都可以通過執行 propose() 來創建一個新的提案。通過一些必要的參數檢查後,新的提案將會被存儲在 proposals 的表裏,其中所消耗的 RAM 將從提案者 proposer 的賬戶中扣除。每個 action 中的參數都對應着 table 中的的一列:
- proposer 指的是創建提案的賬戶;
- proposal_name 指的是 proposals 這個表的主鍵,這是一個提案的唯一標識;
- title 指的是類型爲字符串,長度少於 1024 的提案標題;
- proposal_json 指的是類型爲 JSON 格式的字符串,用以表示提案的具體描述,格式需要遵循 Proposal JSON Structure Guidelines 的規範;
- expires_at 定義了投票的截止期限,這個時間點需要是 action 執行的時間到未來 6 個月之間
一旦某個提案創建了,只要在 expires_at 的時間內,任何賬戶(包括提案者 proposer 自己)都可以通過 vote() 的這個 action 來投票。
ACTION expire(eosio::name proposal_name)
這個 action 允許提案者 proposer 提前結束他/她的提案,並且立刻終止投票。這個操作的本質實際上是把 expires_at 字段修改成了當前的時間。
proposal_table.modify(itr, proposer, [&](auto& row) {
row.expires_at = current_time_point_sec();
});
只有初始提案者 proposer 纔可以通過調用 expire() 來提前結束提案。如果在一個不存在的,或者已經結束的提案中調用此 action,系統會返回錯誤信息。
ACTION clnproposal(eosio::name proposal_name, uint64_t max_count)
當一個提案的凍結時間超過 3 天后(通過 FREEZE_PERIOD_IN_SECONDS 來設置這個時間),我們就可以通過這個 action 來移除該提案。
bool can_be_cleaned_up() const { return current_time_point_sec() > (expires_at + FREEZE_PERIOD_IN_SECONDS); }
操作 clnproposal() 將清除與這個提案相關的所有投票記錄。由於每次刪除的數量由 max_count 來限制,因此這個操作採用不斷迭代的方式,通過多次調用這個 action ,直到所有的投票記錄被刪除。
auto index = vote_table.template get_index<"byproposal"_n>();
auto vote_key_lower_bound = compute_by_proposal_key(proposal_name, name(0x0000000000000000)); auto vote_key_upper_bound = compute_by_proposal_key(proposal_name, name(0xFFFFFFFFFFFFFFFF));
auto lower_itr = index.lower_bound(vote_key_lower_bound); auto upper_itr = index.upper_bound(vote_key_upper_bound);
uint64_t count = 0; while (count < max_count && lower_itr != upper_itr) { lower_itr = index.erase(lower_itr); count++; }
請注意,二級索引 byproposal 是用來通過 proposal_name(請查看 vote 表) 來查詢和迭代所有給定投票記錄的,一旦所有相關的投票記錄都被移除,提案本身也將會被刪除。
if (lower_itr == upper_itr && itr != proposal_table.end()) {
proposal_table.erase(itr);
}
這個操作可以有效清理提案以及它的投票記錄所佔用的所有 RAM 資源。任何人都可以調用 clnproposal() 這個 action,因爲這個操作僅接受對已經完成且凍結期結束的提案執行。我們鼓勵所有投票者、提案者以及社區的成員調用 clnproposal() 來清理過期提案,減少 RAM 資源的佔用。
投票
投票部分包含了一個名爲 vote 的 table 以及 vote() 和 unvote() 兩個 action。
ACTION vote(eosio::namevoter, eosio::nameproposal_name, uint8_t vote, string vote_json)
ACTION unvote(eosio::namevoter, eosio::nameproposal_name)
TABLE vote { uint64_t id; // primary key eosio::name proposal_name; // secondary key eosio::name voter; // secondary key uint8_t vote; string vote_json; eosio::time_point_sec updated_at; }
對於尚未截止的提案,任何賬戶都可以使用 vote() 來進行投票,這將會消耗投票者的少量 RAM 資源 (430 字節) ,用以把投票信息保存在 vote 表中。
vote 表中具體含義由 vote 的字段來表示:
- 0 代表拒絕票
- 1 代表同意票
- 255 代表棄權票
- 其他值可以用來表示其他含義
在 vote 表中,主鍵 id 是自動生成的。二級索引 proposal_name 和 voter 是爲了使用 proposal 或者 voter 字段進行搜索,而 vote_json 字段則是用來記錄一個投票記錄的額外信息,例如可以記錄投票人投票時的一些看法等。
投票人可以通過再次調用 vote() 來修改他/她的投票,或者調用 unvote() 來把他/她的投票記錄從 vote 表中刪除。移除有效的投票可以拿回存儲這個投票記錄所佔用的 RAM 資源,相應的,這個投票記錄將不再被這個提案所記錄。
vote() 和 unvote() 這兩個 action 會首先檢查其提案是否處於投票階段,如果一個提案已經超過了投票期限,和投票相關的操作將會被系統拒絕。
bool is_expired() const { return current_time_point_sec() >= expires_at; }
因此,當一個提案的投票階段結束後,系統可以保證所有投票記錄不能被修改,大家便可以着手清點和計算投票結果了。
狀態
ACTION status(eosio::name account, string content)
TABLE status { // scope is self eosio::name account; // primary key string content; eosio::time_point_sec updated_at; }
status() 會記錄與之關聯的 account 賬號的狀態,如果參數 content 爲空,這個 action 會移除之前的狀態;如果不爲空,將會在 status 表中新增或者修改這個 account 賬號所對應的狀態記錄。
發佈和回覆
ACTION post(eosio::name poster, string post_uuid, string content, eosio::name reply_to_poster, string reply_to_post_uuid, bool certify, string json_metadata)
ACTION unpost(eosio::name poster, string post_uuid)
我們也可以通過 post() 和 unpost() 來發布帖子和回覆,不過這兩個 action 僅驗證參數,並不會將數據存在數據庫中。因此所有的帖子和回覆內容都不會保存在 RAM 中,他們只能通過鏈上的交易記錄來查看。因此,需要一些鏈下工具來爲 post() 和 unpost() 這兩個 action 的數據提供排序、展示、計數和統計報告等服務。例如,Novusphere 按照他們的數據格式,爲用戶提供了一個有用戶界面的應用來展示和分類帖子。他們使用 eosio.forum 合約作爲後端服務,並提供了一個基於 EOSIO 的類似 Reddit 的網頁應用。
下一步是什麼?
如果您覺得本教程有幫助,請別忘了點贊或關注我們的微信公衆號黑曜石實驗室 (Obsidianlabs),幣乎號 EOSStudio,我們會持續更新更多的產品信息、技術文章和精彩內容。
深度解析 EOS 合約:eosio.forum
非常感謝 dfuse 團隊爲本期教程的編寫提供的諸多幫助!
- EOS Studio Website: https://www.eosstudio.io
- EOS Studio 微信公衆號:obsidianlabs
- EOS Studio Telegram: https://t.me/eosstudio
- EOS Studio Twitter: https://twitter.com/obsidian_labs
- dfuse Website: https://www.dfuse.io/
- dfuse 微信公衆號:dfuse API
- dfuse Telegram: https://t.me/dfuseAPI