eosio.forum Part 3: Source Code Walkthrough
The purpose of this tutorial series is to elaborate on some EOS smart contracts in depth. We will carefully select examples that are well designed and built, some are already widely used on the EOS Mainnet. Through this series, we hope to provide more learning materials for dApp developers and help them understand more design patterns and application scenarios of smart contracts.
In Part 1 and Part 2, we discussed the motivation and basic workflow of the smart contract eosio.forum
. In this article, we will go to the source code and look at the technical details of how the smart contract is implemented.
This contract can be roughly divided into four parts: proposal, vote, status and post. All the actions and tables live in the same class forum
. Links to source code definitions are provided for each action and table, so you can easily refer them when necessary.
Proposal
The proposal part contains a table proposals
and a few actions to operate a proposal.
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;
}
Any account can execute the propose()
action to create a new proposal. After some necessary parameter checks, a new proposal will be saved in table proposals
with RAM charged to the proposer
. Each action parameter corresponds to a column in the table:
proposer
is the account who created the proposal;proposal_name
is the primary key for tableproposals
and used as the unique identifier for a proposal;title
is a string for the proposal title and should be less than 1024 characters;proposal_json
is a JSON string for the proposal description and should comply with Proposal JSON Structure Guidelines;expires_at
defines the deadline for the voting period, which needs to be a time point between the action execution time and the next 6 months.
Once a proposal is created, any account (including the proposer
itself) can vote on it via the vote()
action, as long as the current time is before expires_at
.
ACTION expire(eosio::name proposal_name)
This action allows the proposer
to manually expire his/her proposal and end the voting immediately. This is done by modifying the expires_at
field to the current time.
proposal_table.modify(itr, proposer, [&](auto& row) {
row.expires_at = current_time_point_sec();
});
The action expire()
can only be called by the original proposer
. Calling it on a non-existant or already expired proposal will return an error.
ACTION clnproposal(eosio::name proposal_name, uint64_t max_count)
It’s possible to clean a proposal if it has expired and its freeze period of 3 days (set by FREEZE_PERIOD_IN_SECONDS
) has fully elapsed.
bool can_be_cleaned_up() const { return current_time_point_sec() > (expires_at + FREEZE_PERIOD_IN_SECONDS); }
The action clnproposal()
will clean up all votes related to a proposal. It works iteratively by removing as many as max_count
votes, and can be executed multiple times until all votes are removed.
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++;
}
Notice that the secondary index byproposal
is used to query and iterate over all votes of a given proposal_name
(see table vote
). Once there are no more associated votes, the proposal itself will be deleted.
if (lower_itr == upper_itr && itr != proposal_table.end()) {
proposal_table.erase(itr);
}
This effectively clears all the RAM consumed for a proposal and all its votes. It’s safe to allow anybody to call clnproposal()
since the action will only accept an expired proposal that has passed the freeze period, which means it has terminated its lifecycle. Voters, proposers, or any community member is invited to call clnproposal()
to clean the RAM related to a proposal.
Vote
The vote part contains a table vote
as well as vote()
and unvote()
actions.
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;
}
For a non-expired proposal, any accounts can use the vote()
action to publish a vote. It will consume a little bit of RAM from the voter (430 bytes) to save the vote info in table vote
.
The meaning of the vote is represented by the vote
field.
0
means no1
means yes255
means abstain- Other values can be used to represent other meanings
In table vote
, the primary key id
is generated automatically. Secondary keys are created for fields proposal_name
and voter
to support searching by proposal or voter. The field vote_json
is designed to provide extra information for a vote, such as a comment explaining the thought behind the vote.
The voter can execute the vote()
action again to change his/her vote, or call the unvote()
action to delete his/her vote in table vote
. Removing the current active vote reclaims the stored RAM of the vote. Of course, the vote will not count anymore.
The vote()
and unvote()
actions will first check whether the proposal is still active, and refuse the execution if the proposal is expired.
bool is_expired() const { return current_time_point_sec() >= expires_at; }
Therefore, it is guaranteed that the vote statistics for a proposal will be fixed once the proposal is expired, so that people will be able to count the votes and compute the voting result.
Status
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;
}
The action status()
will record status for the associated account
. If the content
is empty, the action will remove a previous status. Otherwise, it will add a status entry or modify the existing entry for the account
using the content
received.
Post
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)
It’s also possible to create posts or responses related to those posts, but the two actions only validate the parameters without any database operations. Therefore, post data are not stored in the RAM and they are only visible in the transaction history of the chain. Off-chain tools are needed to sort, display, aggregate, and report on the outputs of post()
and unpost()
actions. For example, Novusphere has provided a user interface to list and categorize posts in accordance with their data format. It uses the eosio.forum
contract as a backend server to support a Reddit-like web on the EOSIO blockchain.
What’s next?
Don’t forget to clap and star if you find this tutorial helpful, and make sure to follow us if you’d like to be notified when we have new updates.
EOS Contract in Depth: eosio.forum
Many thanks to the dfuse team for their help in writing this tutorial!
- EOS Studio Website: https://www.eosstudio.io
- EOS Studio Telegram: https://t.me/eosstudio
- dfuse Website: https://www.dfuse.io/
- dfuse Telegram: https://t.me/dfuseAPI