Skip to main content

Basic operations

In this section, we will explore basic operations on the Chromia blockchain, focusing on adding data through transactions. By the end of this tutorial, you will understand how to create users, make posts, and manage followers within your dapp.

Add data through transactions

In Chromia, you add data to the blockchain by sending transactions, which can contain one or more database operations known as "operations." Let’s define the operations for creating users, making posts, and following users.

Register a user

rell/src/news_feed/operations.rell
operation register_user(name) {
val account = register_account();
val user = create user ( name, account.id, account );
create follower ( user = user, follower = user );
}

The register_user operation creates a new user. It takes the parameter name (the user's name) and creates a user in the database. A public key, used as id, gets passed directly to the constructor, thanks to Rell’s ability to identify types.

  • Use a byte_array with a length of 32 or 64 for the public key.

An alternative notation would look like this:

create user( name = name, id = pubkey );

Make a post

rell/src/news_feed/operations.rell
operation make_post(user_id: byte_array, content: text) {
create post(
user @ { user_id },
content
);
}

The make_post operation allows users to create posts. It requires user_id (the ID of the posting user) and content (the text of the post). Note that you don’t need to specify the timestamp, as it has a default value in the entity definition.

Follow a user

rell/src/news_feed/operations.rell
operation follow_user(user_id: byte_array, follow_id: byte_array) {
val user = user @ { user_id };
val follow = user @ { follow_id };
create follower(
user = follow,
follower = user
);
}

The follow_user operation enables one user to follow another. It requires user_id (the ID of the user who wants to follow) and follow_id (the ID of the user to be followed). The operation fetches both users from the database and creates a follower entity to establish the relationship.

Unfollow a user

To unfollow a user, you need to delete the follower entity.

rell/src/news_feed/operations.rell
operation unfollow_user(user_id: byte_array, unfollow_id: byte_array) {
val user = user @ { user_id };
val unfollow = user @ { unfollow_id };
delete follower @? { .user == unfollow, .follower == user };
}

Test the operations

Let’s ensure your dapp code works as intended by diving into unit tests. Testing is essential to verify that your operations produce the expected results.

Set up the test module

  1. Navigate to the test folder in your project's directory. If the folder doesn’t exist, create it.
  2. Create a file named my_news_feed_test.rell inside the test folder. This file will contain your test cases.
  3. Add the following header to my_news_feed_test.rell:
src/test/my_news_feed_test.rell
@test module;

import main.*;
  • The @test module declaration indicates that this module contains test code.
  • The import main.*; line imports everything from the main module, making your dapp code accessible for testing.

Test dapp

Let’s examine some test cases to ensure that your dapp functions correctly. We will test the creation of users, following relationships, and posts.

src/test/my_news_feed_test.rell
@test module;

import main.*;

val alice = rell.test.pubkeys.alice;
val bob = rell.test.pubkeys.bob;

function test_create_entities() {
// Create two users: Alice and Bob
rell.test.tx()
.op(create_user("Alice", alice))
.op(create_user("Bob", bob))
.run();

// Check that there are two users in the user table
assert_equals(user @ { } (@sum 1), 2);

// Alice follows Bob and creates a post
rell.test.tx()
.op(follow_user(alice, bob))
.op(make_post(alice, "My post"))
.run();

// Verify that there is one follower and one post
assert_equals(follower @ { } (@sum 1), 1);
assert_equals(post @ { } (@sum 1), 1);

// Alice unfollows Bob and verify there are no followers
rell.test.tx()
.op(unfollow_user(alice, bob))
.run();
assert_equals(follower @ { } (@sum 1), 0);
}

In this test case:

  • We use two public keys, alice and bob, from the Rell test framework as user IDs.
  • The function test_create_entities, prefixed with test_ to indicate that it is a test case, runs a series of transactions to create users and manage their interactions.
  • After each transaction, we use the assert_equals function to check if the expected number of entities is present in the corresponding tables. We employ the @sum function to aggregate values from the table, setting it to 1 because we are only interested in counting the number of entities.

Update tests with FT authentication

To get your tests up and running with FT4 authentication, you need to understand how it works. For each authenticated operation, call either auth.ft_auth or auth.evm_auth in the same transaction directly before the operation. Use the ft_auth_operation_for function from the ft library's test module:

src/test/news_feed_test.rell
import lib.ft4.test.core.{ ft_auth_operation_for };

Inject these function calls into all transactions that involve make_post or follow_user, and remove the user_id argument from these operations. For example, in test_create_entities:

src/test/news_feed_test.rell
function test_create_entities() {
rell.test.tx()
.op(create_user("Alice", alice))
.op(create_user("Bob", bob))
.run();
assert_equals(user @ { } (@sum 1), 2);
rell.test.tx()
.op(ft_auth_operation_for(alice)) // Auth operation
.op(follow_user(bob)) // Argument removed
.op(ft_auth_operation_for(alice)) // Auth operation
.op(make_post("My post")) // Argument removed
.sign(alice_kp)
.run();
assert_true(is_following(alice, bob));
assert_equals(follower @ { } (@sum 1), 1);
assert_equals(post @ { } (@sum 1), 1);
}

Updating test_input_verification showcases the power of this approach, making it difficult to impersonate others. In the case where Charlie tries to make a post, he cannot proceed because he cannot create the auth operation. The modified test now looks like this:

src/test/news_feed_test.rell
function test_input_verification() {
rell.test.tx()
.op(create_user("Alice", alice))
.op(create_user("Bob", bob)).run();

// Alice cannot be impersonated by Bob
rell.test.tx()
.op(ft_auth_operation_for(alice)) // <-- malicious auth operation
.op(follow_user(bob)) // <-- argument removed
.sign(bob_kp)
.run_must_fail();
rell.test.tx()
.op(ft_auth_operation_for(alice)) // <-- malicious auth operation
.op(make_post("My malicious post")) // <-- argument removed
.sign(bob_kp)
.run_must_fail();

// Charlie cannot be followed by Alice since he doesn't exist
val f1 = rell.test.tx()
.op(ft_auth_operation_for(alice)) // <-- auth operation
.op(follow_user(charlie)) // <-- argument removed
.sign(alice_kp)
.run_must_fail();
assert_true(f1.message.contains("does not exist"));

// A post cannot be created by Charlie since he does not exist
val f2 = rell.test.tx()
//.op(ft_auth_operation_for(charlie)) // <-- Cannot create auth operation
.op(make_post("My secret post")) // <-- argument removed
.sign(charlie_kp)
.run_must_fail();
assert_true(f2.message.contains("Expected at least two operations"));
}

Make sure to also update test_follower_calculation and test_pagination_of_posts:

src/test/news_feed_test.rell
function test_follower_calculation() {
rell.test.tx()
.op(create_user("Alice", alice))
.op(create_user("Bob", bob))
.op(create_user("Charlie", charlie))
.run();

rell.test.tx()
.op(ft_auth_operation_for(alice))
.op(follow_user(bob))
.op(ft_auth_operation_for(alice))
.op(follow_user(charlie))
.sign(alice_kp)
.run();

assert_true(is_following(alice, bob));
assert_true(is_following(alice, charlie));
assert_equals(get_following_count(alice), 2);
assert_equals(get_following_count(bob), 0);

assert_equals(get_followers_count(alice), 0);
assert_equals(get_followers_count(bob), 1);
}

function test_pagination_of_posts() {
rell.test.tx()
.op(create_user("Alice", alice))
.op(create_user("Bob", bob)).run();
rell.test.tx()
.op(ft_auth_operation_for(alice))
.op(follow_user(bob))
.sign(alice_kp)
.run();

for (i in range(5)) {
rell.test.tx()
.op(ft_auth_operation_for(bob))
.op(make_post("Content %d".format(i)))
.sign(bob_kp)
.run();
}

val initial_posts = get_posts(alice, 0, 4);
assert_equals(initial_posts.pointer, 4);
assert_equals(initial_posts.posts.size(), 4);
val last_posts = get_posts(alice, initial_posts.pointer, 4);
assert_equals(last_posts.pointer, 5);
assert_equals(last_posts.posts.size(), 1);
}

Run the tests

Now you can run the tests to verify your dapp's functionality.

  1. Open a terminal and navigate to your project's root directory.

  2. Execute the following command to run the tests:

chr test

This command runs the tests located in the test: modules defined in chromia.yml. If all tests pass, you will see a confirmation that your dapp's functionality works as expected.

Congratulations! You have successfully added unit tests to your dapp, ensuring its reliability.