Skip to main content

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:

Solidity
// 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:

Rell
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.

Rell
entity post {
content: text;
timestamp = op_context.last_block_time;
author: user;
}

entity user {
user_id: byte_array;
name: text;
}
Solidity
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:

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:

Rell
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.

Solidity
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.

Rell
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.