11 / 27Module 1: A Rell backend with FT accountsBeginner
Basic operations
This part of the tutorial covers basic operations on the Chromia blockchain, focusing on how you write data with transactions. When you finish, you will know how to create users, publish posts, and manage followers in your dapp.
Add data through transactions
You write data to a Chromia blockchain by sending transactions. Each transaction holds one or more database actions called "operations." The operations below create users, publish posts, and set up follow relationships.
Register a user
operation register_user(name) { val account = register_account(); val user = create user ( name, account.id, account ); create follower ( user = user, follower = user );}register_user adds a new user to the database from a single parameter, name, which holds the user's name. The public key that serves as the id goes straight into the constructor, because Rell matches arguments to attributes by their type.
- The public key must be a
byte_arraythat is 32 or 64 bytes long.
You can also write it this way:
create user( name = name, id = pubkey );Make a post
operation make_post(content: text) { val account = auth.authenticate(); require(content.size() <= 255, "A post cannot be longer than 255 letters"); val user = user @ { account }; create post ( user, content );}make_post publishes a post. You pass it user_id, the ID of the author, and content, the post's text. You don't supply a timestamp, since the entity definition gives it a default value.
Follow a user
operation follow_user(follow_pubkey: pubkey) { val account = auth.authenticate(); val user = user @ { account }; val follow = require_user(follow_pubkey); create follower ( user = follow, follower = user );}follow_user lets one user follow another. It takes user_id, the ID of the user doing the following, and follow_id, the ID of the user being followed. It loads both users from the database and then creates a follower entity that records the relationship.
Unfollow a user
Unfollowing means deleting the matching follower entity.
operation unfollow_user(unfollow_id: byte_array) { val account = auth.authenticate(); val user = user @ { account }; val follow = require_user(unfollow_id); delete follower @? { .user == follow, .follower == user };}Test the operations
Next, you add unit tests to confirm the dapp code behaves as expected.
Test module
- Open the test folder in your project directory.
- Find the file
news_feed_test.rellthere. Your test cases go in this file.
@test module;import ^.test_operations. { create_user };import ^^.news_feed.*;import lib.ft4.test.core. { ft_auth_operation_for };- The
@testannotation on the module declaration marks it as test code. import^^.news_feed.*;pulls in everything from thenews_feedmodule so your tests can reach the dapp code.- For tests that use FT4 authentication, call
auth.ft_authorauth.evm_auth
in the same transaction, immediately before the operation. To build it, use theft_auth_operation_forfunction from the test module of theftlibrary.
Test dapp
The following test cases check that your dapp works correctly by covering user creation, follow relationships, and posts.
val alice = rell.test.pubkeys.alice;val bob = rell.test.pubkeys.bob;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)) .op(follow_user(bob)) .op(ft_auth_operation_for(alice)) .op(make_post("My post")) .sign(alice_kp) .run(); assert_true(is_following(alice, bob)); assert_equals(follower @ { } ( @sum 1 ), 1); assert_equals(post @ { } ( @sum 1 ), 1); rell.test.tx() .op(ft_auth_operation_for(alice)) .op(unfollow_user(bob)) .sign(alice_kp) .run(); assert_equals(follower @ { } ( @sum 1 ), 0);}What this test does:
- It takes two public keys from the Rell test framework,
aliceandbob, and uses them as user IDs. test_create_entitiesis a test case, which itstest_prefix signals. It sends a sequence of transactions that create users and handle their interactions.- After every transaction,
assert_equalsverifies that each table contains the expected number of entities. The count comes from@sum, which aggregates over the table; the summed value is set to1since only the number of entities matters.
The updated test_input_verification shows how well this approach blocks impersonation. When Charlie attempts to post, the attempt fails because he cannot produce the auth operation. Here is the revised test:
function test_input_verification() { rell.test.tx() .op(create_user("Alice", alice)) .op(create_user("Bob", bob)).run(); // Bob cannot impersonate alice rell.test.tx() .op(ft_auth_operation_for(alice)) .op(follow_user(bob)) .sign(bob_kp) .run_must_fail(); rell.test.tx() .op(ft_auth_operation_for(alice)) .op(make_post("My malicous post")) .sign(bob_kp) .run_must_fail(); // Alice cannot follow non-existing charlie val f1 = rell.test.tx() .op(ft_auth_operation_for(alice)) .op(follow_user(charlie)) .sign(alice_kp) .run_must_fail(); assert_true(f1.message.contains("does not exist")); // Charlie cannot create post since he does not exist val f2 = rell.test.tx() .op(make_post("My secret post")) .sign(charlie_kp) .run_must_fail(); assert_true(f2.message.contains("Expected at least two operations"));}Run the tests
You can now run the tests to check your dapp.
-
In a terminal, go to the root directory of your project.
-
Run this command:
chr testThe command executes the tests found in the test: modules listed in chromia.yml. When every test passes, the output confirms that your dapp behaves as intended.
Your dapp now has unit tests that help keep it reliable.