Build a news feed dapp
In this section, we will create something more useful: a decentralized news feed where users can create posts and follow each other. We'll start by designing the core functionality of our news feed to store individual posts and connect them to a user.
The code shown below should be familiar to Solidity developers:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract NewsFeedClone {
struct Post {
string text;
uint256 timestamp;
address user;
}
struct User {
string username;
}
event PostCreated(address indexed user, string text, uint256 timestamp);
event UserCreated(address indexed user, string username);
function createPost(address user, string memory _text) public {
emit PostCreated(user, _text, block.timestamp);
}
function registerUser(string memory username) public {
emit UserCreated(msg.sender, username);
}
}
Now let's do the same in Rell and then we will look at the differences:
module;
entity post {
content: text;
timestamp: timestamp = op_context.last_block_time;
author: user;
}
entity user {
key id: byte_array;
mutable name;
}
operation register_user(user_id: byte_array, username: text) {
create user ( id = user_id, name = username );
}
operation create_post(author_id: byte_array, content: text) {
create post ( user @ { author_id }, content = content );
}
As we can see, these implementations are very similar but there are a few key differences. In our Solidity solution, we emit an event instead of storing the data in the contract. This is done because, on EVM blockchains, gas fees need to be considered, and storing data and keeping logic in the contract is not a scalable solution. Using Chromia and Rell, we can store all data on-chain. There is no need to store data outside of your dapp. Our posts will be stored in a relational database, making it easy to retrieve directly from the dapp using queries, all without worrying about transaction fees.
Let's look at the details
In Rell, we have the concept of data models, which isn't really the case for Solidity. In these examples, we use the
struct
keyword to define structures to hold data in Solidity, and in Rell we use entity
, which is an actual data
model entity. The struct keyword is also available in Rell and works just like in Solidity. Read more on structs
here. Entities in Rell are similar to tables in a SQL
database. Deploying the code we just wrote will automatically create the necessary tables and relations in Chromia's
distributed relational database. In this example, we also have a relation between a user and their posts. They are part
of the model in both implementations, but in Rell, it becomes an actual database relation. To gain a deeper insight into
managing these relationships, explore our Understand relationships in Rell
course.
entity post {
content: text;
timestamp = op_context.last_block_time;
author: user;
}
entity user {
user_id: byte_array;
name: text;
}
struct Post {
string text;
uint256 timestamp;
address user;
}
struct User {
address user;
string username;
}
Let's look at the Rell parts:
- The
entity
keyword initiates the definition of our entity, similar to defining a table in a database. - Attributes, like columns in a database table, each hold specific data types related to the entity.
- Our connection between a post and its author is simply defined by adding an attribute
author: user
. This will automatically set up the relation in the underlying database for the state.
So now we have defined our model and can look at functions to create new instances of the model. We start with Solidity:
function createPost(address user, string memory _text) public {
emit PostCreated(user, _text, block.timestamp);
}
Sending a transaction calling the createPost
function will result in an event being emitted to the Event log with data
for the post. Emitting events in Solidity is a gas-efficient way to record information. Since transactions on Ethereum
require gas, optimizing your smart contract to reduce the amount of on-chain data storage (which is expensive) is wise.
Events log data in transaction receipts and don't consume as much gas as storing data directly on the blockchain.
However, there is a trade-off between centralization and decentralization with this approach.
In Rell, the function is similar:
operation create_post(author_id: byte_array, content: text) {
val author = user @ { author_id }
create post ( author, content = content );
}
An operation in Rell is a function where the body contains logic to change the dapp's state. A transaction can contain
one or many operations, and the logic of each operation will alter the dapp table state. The create
keyword is used to
insert a new post in the dapp state. Since we want to connect the author to the post, we first fetch the entity and then
use it as a parameter in our create call. When this transaction is executed, a new post is inserted into the state with
a relation to the author, and then the transaction is added to the Chromia blockchain.
Fetching posts for a user
Now that we have our model set up with relationships between users and posts in both Solidity and Rell, we can set up methods for fetching posts for a specific user. Since our Solidity contract emits events to the log when we add a new post, we need to query the event log instead of interacting directly with the smart contract to fetch posts.
const contract = new web3.eth.Contract(contractABI, contractAddress);
contract
.getPastEvents("PostCreated", {
filter: {
user: "0x123...", // Ethereum address of the user
},
fromBlock: fetchFromBlock,
toBlock: "latest",
})
.then(function (events) {
console.log(events);
// Perform logic here
});
In this example, we use web3.js to fetch and filter events. In a real app, we would probably put this in the backend and expose this function to the client using an API. In Chromia and Rell, we can stay on-chain to fetch our data.
query get_my_posts(user_id: byte_array) {
val user = user @ { .id == user_id };
return post @* { .author == user } ( .content );
}
Since we have a more relational approach, we can create a strongly typed query for all posts of a specific user. This is done in just two lines of code, and the query syntax is a form of query language. Let's dissect it a bit.
First, we call val user = user @ { .id == user_id }
, this query uses user @
to fetch a single entity and then we add
the filter { .id == user_id }
to specify which user to fetch. Our next query uses the @*
operator in post @*
to
retrieve a collection of post entities. @*
means that we expect 0 or more objects of this type from the query. The
curly braces at the end, as seen in post @* { .author == user }
, specify our filter criteria for the query. The
subsequent segment of the query defines which attributes we wish to retrieve, in this case, .content
. More about
operators and database operations can be found
here.
This summarizes the first part of the news feed dapp and the corresponding smart contract in Solidity. Our app can now create posts and keep track of which posts belong to which user.