<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dmytro Zahumennov</title>
    <description>The latest articles on DEV Community by Dmytro Zahumennov (@zahumennov).</description>
    <link>https://dev.to/zahumennov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3099518%2F691aca11-503e-4c03-89b1-41af2ff819f4.jpg</url>
      <title>DEV Community: Dmytro Zahumennov</title>
      <link>https://dev.to/zahumennov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zahumennov"/>
    <language>en</language>
    <item>
      <title>Building a Prediction Market on Chromia | Step 4 — Bets, Moderation, and Validation Rules</title>
      <dc:creator>Dmytro Zahumennov</dc:creator>
      <pubDate>Wed, 14 May 2025 14:14:01 +0000</pubDate>
      <link>https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-4-bets-moderation-and-validation-rules-ng3</link>
      <guid>https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-4-bets-moderation-and-validation-rules-ng3</guid>
      <description>&lt;p&gt;Today, we’ll start with a small fix to the &lt;code&gt;event&lt;/code&gt; entity and expand it a bit. Let's add the &lt;a href="https://learn.chromia.com/courses/rell-masterclass/entities" rel="noopener noreferrer"&gt;&lt;code&gt;mutable&lt;/code&gt;&lt;/a&gt; annotation to the &lt;code&gt;is_closed&lt;/code&gt; and &lt;code&gt;result&lt;/code&gt; fields so that we can update them later. Also we’re introducing a new field called &lt;code&gt;status&lt;/code&gt; with three possible values: &lt;code&gt;PENDING&lt;/code&gt;, &lt;code&gt;APPROVED&lt;/code&gt;, and &lt;code&gt;REJECTED&lt;/code&gt;. This is our way of adding moderation, so we don’t let unwanted content slip into the system. And finally, we’re adding the ability to configure the bet amount.&lt;/p&gt;

&lt;p&gt;Here’s what the updated event looks like in &lt;code&gt;oddly/entities.rell&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;entity event {
    creator: ft4_account;
    question: text;
    created_at: timestamp = utils.last_known_time();
    mutable is_closed: boolean = false;
    expires_at: timestamp;
    mutable result: result_values = result_values.UNRESOLVED;
    mutable status: event_status = event_status.PENDING;
    bet_amount: integer;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s update the &lt;code&gt;create_event&lt;/code&gt; operation. But first, let’s clean things up a bit. Right now all the required checks are sitting directly inside the operation, and it looks a little overloaded. So we’re going to move those validations into a separate file to keep things neat and readable. Create a new file at &lt;code&gt;oddly/requirements.rell&lt;/code&gt; and drop in the following functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function validate_question_length(question: text) {
    require(
        question.size() in range(utils.MIN_QUESTION_LENGTH, utils.MAX_QUESTION_LENGTH),
        "Question must be between 10 and 280 characters."
    );
}

function validate_expiration_period(expires_in_days: integer) {
    require(
        expires_in_days in range(utils.MIN_EXPIRATION_DAYS, utils.MAX_EXPIRATION_DAYS),
        "Expiration period must be between 1 and 365 days."
    );
}

function validate_bet_amount(bet_amount: integer) {
    require(
        bet_amount in range(utils.MIN_BET_AMOUNT, utils.MAX_BET_AMOUNT),
        "Bet amount must be between 1 and 1000."
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We simplified them a bit by using &lt;a href="https://docs.chromia.com/rell/language-features/types/complex-types#examples-2" rel="noopener noreferrer"&gt;&lt;code&gt;range()&lt;/code&gt;&lt;/a&gt; instead of plain comparison operators. And don’t forget to update &lt;code&gt;utils/constants.rell&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val MIN_EXPIRATION_DAYS = 1;
val MAX_EXPIRATION_DAYS = 366;
val MIN_QUESTION_LENGTH = 10;
val MAX_QUESTION_LENGTH = 281;
val MIN_BET_AMOUNT = 1;
val MAX_BET_AMOUNT = 1001;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now each validation lives in its own neat little place. Next up, let’s update the operation itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation create_event(question: text, expires_in_days: integer, bet_amount: integer) {
    val account = auth.authenticate();

    validate_question_length(question);
    validate_expiration_period(expires_in_days);
    validate_bet_amount(bet_amount);

    val expires_at = utils.days_from_now(expires_in_days);
    create event ( creator = account, question, expires_at, bet_amount );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also want to add a new value to &lt;code&gt;result_values&lt;/code&gt; — &lt;code&gt;VOID&lt;/code&gt;. This will come in handy when an event can’t be resolved for some reason. Maybe the event never happened, the question turned out to be invalid, or there’s simply not enough information to decide the outcome. So let’s update our &lt;code&gt;result_values&lt;/code&gt; enum in &lt;code&gt;oddly/enums&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enum result_values {
    YES,
    NO,
    UNRESOLVED,
    VOID
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we just need to tweak the tests. We’ll update the &lt;code&gt;create_event&lt;/code&gt; call to include the bet amount, and in &lt;code&gt;test_create_event&lt;/code&gt;, we’ll throw in two extra checks, one for the event status and one for the bet amount. It should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function test_create_event() {
    val alice = rell.test.keypairs.alice;

    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    rell.test.tx()
        .op(open_strategy.ras_open(auth_descriptor))
        .op(strategies.register_account())
        .sign(alice)
        .run();

    rell.test.tx()
        .op(ft_auth_operation_for(alice.pub))
        .op(create_event("Will CHR be $10 in a week?", 7, 10))
        .sign(alice)
        .run();

    val events = get_events();

    assert_equals(events[0].creator.id, alice.pub.hash());
    assert_equals(events[0].question, "Will CHR be $10 in a week?");
    assert_true(events[0].created_at &amp;gt; 0);
    assert_equals(events[0].is_closed, false);
    assert_true(events[0].expires_at &amp;gt; events[0].created_at);
    assert_equals(events[0].result, result_values.UNRESOLVED);
    assert_equals(events[0].status, event_status.PENDING);
    assert_equals(events[0].bet_amount, 10);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s pause here for now with adding new features to the events, so we don’t overload the MVP. Honestly, I’ve got tons of ideas and features I want to bring to life in this dapp. But the main goal right now is to ship a minimal but functional version. We’ll keep building and leveling it up from there until we shoot it into orbit 🚀&lt;/p&gt;

&lt;p&gt;So for now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the bet amount will be static&lt;/li&gt;
&lt;li&gt;each account can vote only once per event&lt;/li&gt;
&lt;li&gt;the result will be resolved manually by an admin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alright, time to add an admin-only operation that lets moderators review and approve (or reject) events. In general, any &lt;a href="https://docs.chromia.com/ft4/backend/accounts/overview#register-with-ft4-admin-operation" rel="noopener noreferrer"&gt;admin&lt;/a&gt; operation brings a bit of the centralized world into our beautifully decentralized one, so in the future, we’ll definitely need to fix that. In &lt;code&gt;oddly/module.rell&lt;/code&gt; we import the admin module like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import lib.ft4.core.admin;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we create a new operation inside &lt;code&gt;oddly/operations.rell&lt;/code&gt;. First things first, let’s make sure only an admin can call it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation moderate_event(event, event_status) {
    admin.require_admin();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And really, that’s all we need this operation to do, just update the event’s status. Nice and simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation moderate_event(event, event_status) {
    admin.require_admin();

    event.status = event_status;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maybe it would make sense to add extra checks here, like blocking re-moderation or preventing status changes from &lt;code&gt;REJECTED&lt;/code&gt;  to &lt;code&gt;APPROVED&lt;/code&gt;. But for now, let’s keep things simple and just trust the admins 🙂&lt;/p&gt;

&lt;p&gt;Time to test. First, we add the admin &lt;code&gt;public key&lt;/code&gt; we generated earlier to the blockchain config. And for testing, we’ll generate a separate key just for that. Run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chr keygen --file .chromia/admin_test_keypair
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure the project structure looks exactly like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7dpk9sby7flcnz4ery58.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7dpk9sby7flcnz4ery58.png" alt="Image description" width="582" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then update &lt;code&gt;chromia.yml&lt;/code&gt; so the system knows who the admin is in production and who gets admin powers during tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blockchains:
  oddly_development:
    module: development
    moduleArgs:
      lib.ft4.core.admin:
        admin_pubkey: x"030E7FE665AA55349A9503DD0DB8FA6CB2C1267A50A9F50A60A1EA34D4571FCDE3"
compile:
  rellVersion: 0.14.5
database:
  schema: schema_oddly
test:
  modules:
    - tests
  moduleArgs:
    lib.ft4.core.admin:
      admin_pubkey: x"035A93A7DDF932BB4742CE678D330BC1A586C442A1B6FE3244AA5209F311AF8C16"
libs:
  ft4:
    registry: https://gitlab.com/chromaway/ft4-lib.git
    path: rell/src/lib/ft4
    tagOrBranch: v1.1.0r
    rid: x"FEEB0633698E7650D29DCCFE2996AD57CDC70AA3BDF770365C3D442D9DFC2A5E"
    insecure: false
  iccf:
    registry: https://gitlab.com/chromaway/core/directory-chain
    path: src/lib/iccf
    tagOrBranch: 1.87.0
    rid: x"9C359787B75927733034EA1CEE74EEC8829D2907E4FC94790B5E9ABE4396575D"
    insecure: false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, in &lt;code&gt;tests/development_tests&lt;/code&gt;, create a file called &lt;code&gt;helpers.rell&lt;/code&gt; and add our test admin key there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function admin_keypair() = rell.test.keypair(
    priv = x"820D5301627C33AF91C3BCBCE8BC32D36DF7BBE232FC76AF309AE2A2A36F97FA",
    pub  = x"035A93A7DDF932BB4742CE678D330BC1A586C442A1B6FE3244AA5209F311AF8C16"
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great. The admin is in place. Now we can move on to testing event moderation. We’ve already duplicated quite a bit of code in our tests, so it’s time to clean things up. Let’s start with a quick refactor. First, in &lt;code&gt;development_tests/module.rell&lt;/code&gt; add the following import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import core_accounts: lib.ft4.core.accounts;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now in &lt;code&gt;development_tests/helpers.rell&lt;/code&gt; let’s extract all the repetitive pieces that keep popping up in every test. We’ll start with account creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function register_account(user_kp: rell.test.keypair, auth_descriptor: core_accounts.auth_descriptor) {
    rell.test.tx()
        .op(open_strategy.ras_open(auth_descriptor))
        .op(strategies.register_account())
        .sign(user_kp)
        .run();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let’s prepare some structs to help us pass parameters around more easily. In some of our helper functions, the number of parameters is getting out of hand, and passing them one by one would be painful. Structs to the rescue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;struct create_event_struct {
    user_kp: rell.test.keypair;
    question: text;
    expires_in_days: integer;
    bet_amount: integer;
}

struct create_event_must_fail_struct {
    user_kp: rell.test.keypair;
    question: text;
    expires_in_days: integer;
    bet_amount: integer;
    error_message: text;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now, the functions themselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function create_test_event(params: create_event_struct) {
    rell.test.tx()
        .op(ft_auth_operation_for(params.user_kp.pub))
        .op(create_event(params.question, params.expires_in_days, params.bet_amount))
        .sign(params.user_kp)
        .run();

    return get_events()[0];
}

function create_test_event_must_fail(params: create_event_must_fail_struct) {
    rell.test.tx()
        .op(ft_auth_operation_for(params.user_kp.pub))
        .op(create_event(params.question, params.expires_in_days, params.bet_amount))
        .sign(params.user_kp)
        .run_must_fail(params.error_message);
}

function create_test_event_and_moderate(event: event, status: event_status) {
    rell.test.tx()
        .op(moderate_event(event, status))
        .sign(admin_keypair())
        .run();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take a look at the &lt;code&gt;create_test_event&lt;/code&gt; and &lt;code&gt;create_test_event_must_fail&lt;/code&gt; functions. Since both take more than three parameters, we’re using a &lt;code&gt;struct&lt;/code&gt; to pass them around. It’s way more convenient than stuffing a bunch of arguments into every call.&lt;/p&gt;

&lt;p&gt;Now in the main test file, we replace all the direct calls with these helper functions and throw in one more test for event moderation. Let’s go.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function test_create_event() {
    val alice = rell.test.keypairs.alice;
    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    register_account(alice, auth_descriptor);

    val event = create_test_event(
        create_event_struct(
            user_kp = alice,
            question = "Will CHR be $10 in a week?",
            expires_in_days = 7,
            bet_amount = 10
        )
    );

    assert_equals(event.creator.id, alice.pub.hash());
    assert_equals(event.question, "Will CHR be $10 in a week?");
    assert_true(event.created_at &amp;gt; 0);
    assert_equals(event.is_closed, false);
    assert_true(event.expires_at &amp;gt; event.created_at);
    assert_equals(event.result, result_values.UNRESOLVED);
    assert_equals(event.status, event_status.PENDING);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function test_create_event_too_short_question_must_fail() {
    val alice = rell.test.keypairs.alice;
    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    register_account(alice, auth_descriptor);

    create_test_event_must_fail(
        create_event_must_fail_struct(
            user_kp = alice,
            question = "Too short",
            expires_in_days = 7,
            bet_amount = 10,
            error_message = "Question must be between 10 and 280 characters."
        )
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function test_create_event_too_long_question_must_fail() {
    val alice = rell.test.keypairs.alice;
    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    register_account(alice, auth_descriptor);

    val long_question = "A".repeat(300);

    create_test_event_must_fail(
        create_event_must_fail_struct(
            user_kp = alice,
            question = long_question,
            expires_in_days = 7,
            bet_amount = 10,
            error_message = "Question must be between 10 and 280 characters."
        )
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function test_create_event_exceeds_max_expiration_must_fail() {
    val alice = rell.test.keypairs.alice;
    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    register_account(alice, auth_descriptor);

    create_test_event_must_fail(
        create_event_must_fail_struct(
            user_kp = alice,
            question = "Will CHR be $100?",
            expires_in_days = 400,
            bet_amount = 10,
            error_message = "Expiration period must be between 1 and 365 days."
        )
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function test_create_event_zero_days_must_fail() {
    val alice = rell.test.keypairs.alice;
    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    register_account(alice, auth_descriptor);

    create_test_event_must_fail(
        create_event_must_fail_struct(
            user_kp = alice,
            question = "Will CHR reach $1 today?",
            expires_in_days = 0,
            bet_amount = 10,
            error_message = "Expiration period must be between 1 and 365 days."
        )
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function test_create_event_and_moderate() {
    val alice = rell.test.keypairs.alice;
    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    register_account(alice, auth_descriptor);

    var event = create_test_event(
        create_event_struct(
            user_kp = alice,
            question = "Will CHR be $10 in a week?",
            expires_in_days = 7,
            bet_amount = 10
        )
    );

    create_test_event_and_moderate(event, event_status.APPROVED);

    event = get_events()[0];

    assert_equals(event.status, event_status.APPROVED);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s run the tests and boom — everything works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TEST RESULTS:

OK tests.development_tests:test_account_creation (0.673s)
OK tests.development_tests:test_create_event (0.179s)
OK tests.development_tests:test_create_event_too_short_question_must_fail (0.126s)
OK tests.development_tests:test_create_event_too_long_question_must_fail (0.113s)
OK tests.development_tests:test_create_event_exceeds_max_expiration_must_fail (0.117s)
OK tests.development_tests:test_create_event_zero_days_must_fail (0.111s)
OK tests.development_tests:test_create_event_and_moderate (0.120s)

SUMMARY: 0 FAILED / 7 PASSED / 7 TOTAL (1.442s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we’re moving on to betting functionality. As always, let’s start with the entity. We’re adding a new one called bet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;entity bet {
    key event, better: ft4_account;
    choice: choice_values;
    created_at: timestamp = utils.last_known_time();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ve created a &lt;a href="https://docs.chromia.com/rell/core-concepts#keys-and-indexes" rel="noopener noreferrer"&gt;&lt;code&gt;composite key&lt;/code&gt;&lt;/a&gt;, which is an index that enforces uniqueness across two fields at once: &lt;code&gt;event&lt;/code&gt; and &lt;code&gt;better&lt;/code&gt;. This setup ensures that a single account can only place one bet per event. If they try to place another one, the system will throw an error. Simple, reliable, and exactly what we need.&lt;/p&gt;

&lt;p&gt;One more thing worth highlighting is the event field. Here we’ve directly linked &lt;code&gt;bet&lt;/code&gt; to &lt;code&gt;event&lt;/code&gt;, creating a &lt;a href="https://docs.chromia.com/rell/core-concepts#keys-and-indexes" rel="noopener noreferrer"&gt;one-to-many&lt;/a&gt; relationship. In plain terms, a single event can have many bets attached to it.&lt;/p&gt;

&lt;p&gt;Next, let’s add a validation rule to make sure users aren’t trying to bet on expired events. It’s a basic check, but absolutely essential. We’ll drop this into &lt;code&gt;requirements.rell&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function validate_event_expiration(event: event) {
    require(
        event.expires_at &amp;gt; utils.last_known_time(),
        "Event has expired."
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function simply checks that the event hasn’t expired yet. If the expiration time has already passed, we throw an error and prevent the user from placing a bet. &lt;/p&gt;

&lt;p&gt;Alongside this, we’re also adding a very straightforward operation to place a bet. Nothing fancy, just straight to the point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation place_bet(event, choice: choice_values) {
    val account = auth.authenticate();

    validate_event_expiration(event);

    create bet ( event, account, choice );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We authenticate the account, make sure the event is still active, and then create the bet. Simple as that.&lt;/p&gt;

&lt;p&gt;And just like we did with events, we add a simple query that returns all bets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query get_bets(): list&amp;lt;bet&amp;gt; {
    return bet @* { };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For now, there’s no filtering or sorting, just a full list. Later on, we’ll definitely level this up.&lt;/p&gt;

&lt;p&gt;We’ll be placing and testing bets more than once, so it’s a good idea to pull the main logic into &lt;code&gt;helpers.rell&lt;/code&gt; to avoid repeating ourselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function create_test_bet(user_kp: rell.test.keypair, event: event, choice: choice_values) {
    rell.test.tx()
        .op(ft_auth_operation_for(user_kp.pub))
        .op(place_bet(event, choice))
        .sign(user_kp)
        .run();

    return get_bets()[0];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll move the main test cases into a separate file &lt;br&gt;
&lt;code&gt;development_tests/bets_test.rell&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function test_place_bet() {
    val alice = rell.test.keypairs.alice;
    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    register_account(alice, auth_descriptor);

    val event = create_test_event(
        create_event_struct(
            user_kp = alice,
            question = "Will CHR be $10 in a week?",
            expires_in_days = 7,
            bet_amount = 10
        )
    );

    val bet = create_test_bet(alice, event, choice_values.YES);

    assert_equals(bet.event, event);
    assert_equals(bet.better.id, alice.pub.hash());
    assert_equals(bet.choice, choice_values.YES);
    assert_true(event.created_at &amp;gt; 0);
}

function test_place_bet_twice_must_fail() {
    val alice = rell.test.keypairs.alice;
    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    register_account(alice, auth_descriptor);

    val event = create_test_event(
        create_event_struct(
            user_kp = alice,
            question = "Will CHR hit $5 next month?",
            expires_in_days = 30,
            bet_amount = 10
        )
    );

    create_test_bet(alice, event, choice_values.YES);

    rell.test.tx()
        .op(ft_auth_operation_for(alice.pub))
        .op(place_bet(event, choice_values.NO))
        .sign(alice)
        .run_must_fail();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the last test, we deliberately place a different choice (NO), but it still has to fail—because a single account shouldn’t be able to place two bets on the same event. That’s exactly what our composite key is here to prevent.&lt;/p&gt;

&lt;p&gt;That’s a wrap for today. Time to exhale. I’m not yet sure what we’ll build next, but we’re definitely going to keep pushing forward. From now on, I’ll start skipping detailed test descriptions so we can focus more on the core logic. But don’t worry, you can always find all the tests in the repo if you need them. Let’s keep moving.&lt;/p&gt;

&lt;p&gt;👈 &lt;a href="https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-3-creating-events-and-writing-your-first-operation-1m6h"&gt;Previous step&lt;/a&gt; | 💻 &lt;a href="https://github.com/Zahumennov/oddly" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>cryptocurrency</category>
      <category>chromia</category>
    </item>
    <item>
      <title>Building a Prediction Market on Chromia | Step 3 — Creating Events and Writing Your First Operation</title>
      <dc:creator>Dmytro Zahumennov</dc:creator>
      <pubDate>Mon, 05 May 2025 09:54:45 +0000</pubDate>
      <link>https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-3-creating-events-and-writing-your-first-operation-1m6h</link>
      <guid>https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-3-creating-events-and-writing-your-first-operation-1m6h</guid>
      <description>&lt;p&gt;Today we’ll create our first entity, learn how to create events, write a simple query to fetch all events, and of course, test everything to make sure it actually works.&lt;/p&gt;

&lt;p&gt;Let’s start with the basics. We’ll define our first &lt;code&gt;entity&lt;/code&gt;, which is essentially just a SQL table. In it, we need to specify the field names and their types. The first &lt;code&gt;entity&lt;/code&gt; will be called &lt;strong&gt;event&lt;/strong&gt;. It will store all the core info about each event: the account of the user who created it, the question text, creation time, expiration time, a flag that shows whether the event is closed, and the result. I want to directly link each event to its creator’s account. For that, we’ll use the type &lt;code&gt;ft4 account&lt;/code&gt;, which represents a user in the FT4 system. We import it from the FT4 library in &lt;code&gt;oddly/module.rell&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module;

import lib.ft4.core.accounts.{ ft4_account: account };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;created_at&lt;/code&gt; field represents the &lt;code&gt;timestamp&lt;/code&gt; of when the event was created. In Rell, the recommended type for this is &lt;code&gt;timestamp&lt;/code&gt;. It stores time in milliseconds since the UNIX epoch, which is the standard way to handle time in blockchain environments. This makes it easy to sort, filter, and compare events by time. We don’t want to pass this value manually every time, so we’ll use a function that automatically sets the current time during creation. I’ll show you how that function looks a bit later.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;is_closed&lt;/code&gt; field is a boolean that tells us whether the event has already ended. By default, we’ll set it to &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;expires_at&lt;/code&gt; field is another &lt;code&gt;timestamp&lt;/code&gt;, which defines the expiration time for the event. The user will provide the number of days until expiration, and we’ll calculate the exact &lt;code&gt;timestamp&lt;/code&gt; from that.&lt;/p&gt;

&lt;p&gt;As for the &lt;code&gt;result&lt;/code&gt; field, we’ll need something more flexible. We’ll define an &lt;code&gt;enum&lt;/code&gt; to represent the possible outcomes of the event: yes, no, or unresolved. To do that, we create a file called &lt;code&gt;oddly/enums.rell&lt;/code&gt; and drop in the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enum result_values {
    YES,
    NO,
    UNRESOLVED
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UNRESOLVED&lt;/code&gt; is the default value. It means the event is still open and the result hasn’t been decided yet.&lt;/p&gt;

&lt;p&gt;Now let’s create the &lt;code&gt;entities.rell&lt;/code&gt; file where we’ll define our first entity. It’s going to look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;entity event {
    creator: ft4_account;
    question: text;
    created_at: timestamp = utils.last_known_time();
    is_closed: boolean = false;
    expires_at: timestamp;
    result: result_values = result_values.UNRESOLVED;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boom. The entity is ready. Now let’s move on to operations. Inside the &lt;code&gt;oddly&lt;/code&gt; directory, we create a new file called &lt;code&gt;operations.rell&lt;/code&gt;. And to start things off, we’ll write a basic stub for our operation. It will take two parameters: the question text (&lt;code&gt;question&lt;/code&gt;) and the number of days until the event expires (&lt;code&gt;expires_in_days&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation create_event(question: text, expires_in_days: integer) {

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before we can actually create the event, we need to know who is creating it. That’s where the authenticate function from the FT4 library comes in. But for everything to work properly, we need to do three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Import &lt;code&gt;auth&lt;/code&gt; from FT4.&lt;/li&gt;
&lt;li&gt;Add an &lt;code&gt;auth_handler&lt;/code&gt; that defines what permissions a user needs to perform certain actions (in our case, creating an event).&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;auth.authenticate()&lt;/code&gt; inside the operation to extract the account of the user who triggered it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In &lt;code&gt;module.rell&lt;/code&gt; we add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module;

import lib.ft4.core.accounts.{ ft4_account: account };
import lib.ft4.core.auth;

@extend(auth.auth_handler)
function () = auth.add_auth_handler(
    flags = ["T"]
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in &lt;code&gt;operations.rell&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation create_event(question: text, expires_in_days: integer) {
    val account = auth.authenticate();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we know exactly who’s calling the operation, and we can safely link the event to its creator.&lt;/p&gt;

&lt;p&gt;Next up is validation. First, we want to make sure the question isn’t too short or too long. Second, we’ll check the expiration period — it needs to be a positive number and within a sensible range. A year sounds like a decent cap for now, but we can adjust it later.&lt;/p&gt;

&lt;p&gt;So let’s set up the basics. Inside the &lt;code&gt;oddly&lt;/code&gt; directory, create a subfolder called &lt;code&gt;utils&lt;/code&gt;. In there, as usual, start with &lt;code&gt;module.rell&lt;/code&gt; and keep it simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now add a new file called &lt;code&gt;constants.rell&lt;/code&gt;, where we’ll define all the validation limits. This way, if you ever need to tweak the rules, you won’t be hunting through scattered code — just update them here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val MIN_EXPIRATION_DAYS = 1;
val MAX_EXPIRATION_DAYS = 365;
val MIN_QUESTION_LENGTH = 10;
val MAX_QUESTION_LENGTH = 280;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll also need two helper functions for handling time: one to determine when the event was created, and one to calculate when it should expire. Sure, we could use something like &lt;code&gt;System.currentTimeMillis()&lt;/code&gt; — but that’s not how things work on a blockchain. Nodes don’t share a synchronized clock, and the only &lt;code&gt;timestamp&lt;/code&gt; you can truly rely on is the one written into the block itself.&lt;/p&gt;

&lt;p&gt;So we create a new file called &lt;code&gt;time.rell&lt;/code&gt; inside utils, and define two functions there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function last_known_time() = if (op_context.exists) op_context.last_block_time else block @ {} (@max .timestamp) ?: 0;

function days_from_now(days: integer) {
    return last_known_time() + (days * 24 * 60 * 60 * 1000);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;last_known_time()&lt;/code&gt; is a solid all-purpose helper. It returns the most recent known &lt;code&gt;timestamp&lt;/code&gt; in the blockchain. During an operation, it uses &lt;code&gt;op_context.last_block_time;&lt;/code&gt; otherwise, it fetches the maximum &lt;code&gt;timestamp&lt;/code&gt; from existing blocks. If no blocks exist yet (e.g., during initialization), it returns 0. This provides a safe and consistent way to get a reliable &lt;code&gt;timestamp&lt;/code&gt; in any context.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;days_from_now(days)&lt;/code&gt; takes the current time and adds &lt;code&gt;n&lt;/code&gt; days to it, returning the result in milliseconds. Perfect for setting expiration dates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now don’t forget to import utils in &lt;code&gt;oddly/module.rell&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import .utils;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now let’s get back to our operation and drop in those require checks to make sure the input values stay within bounds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation create_event(question: text, expires_in_days: integer) {
    val account = auth.authenticate();

    require(
        question.size() &amp;gt;= utils.MIN_QUESTION_LENGTH and question.size() &amp;lt;= utils.MAX_QUESTION_LENGTH,
        "Question must be between 10 and 280 characters."
    );
    require(
        expires_in_days &amp;gt; utils.MIN_EXPIRATION_DAYS and expires_in_days &amp;lt; utils.MAX_EXPIRATION_DAYS,
        "Expiration period must be between 1 and 365 days."
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next comes the time logic. We need to figure out exactly when the event is supposed to expire. Thanks to the &lt;code&gt;days_from_now(expires_in_days)&lt;/code&gt; function we just built in utils, we can easily calculate a future &lt;code&gt;timestamp&lt;/code&gt; by adding the number of days to the current block time. The result is a millisecond-based &lt;code&gt;timestamp&lt;/code&gt;, which we’ll store in the &lt;code&gt;expires_at&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;On the frontend, we’ll later convert it into a nice human-readable format like “May 20, 2025 at 1:00 PM”.&lt;/p&gt;

&lt;p&gt;Now we pass everything to the function and here’s the final version of our operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation create_event(question: text, expires_in_days: integer) {
    val account = auth.authenticate();

    require(
        question.size() &amp;gt;= utils.MIN_QUESTION_LENGTH and question.size() &amp;lt;= utils.MAX_QUESTION_LENGTH,
        "Question must be between 10 and 280 characters."
    );
    require(
        expires_in_days &amp;gt; utils.MIN_EXPIRATION_DAYS and expires_in_days &amp;lt; utils.MAX_EXPIRATION_DAYS,
        "Expiration period must be between 1 and 365 days."
    );

    val expires_at = utils.days_from_now(expires_in_days);
    create event ( creator = account, question, expires_at );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final touch for today is to add a simple query that returns all events as they are. Later on, we’ll definitely enhance it with pagination, sorting, filtering by status (active, closed), and maybe even keyword search. But for now, the goal is just to fetch a full list of events for debugging.&lt;/p&gt;

&lt;p&gt;So in &lt;code&gt;oddly/queries.rell&lt;/code&gt;, let’s add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query get_events(): list&amp;lt;event&amp;gt; {
    return event @* { };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alright, let’s jump into testing — because without tests, we can’t really be sure that anything works the way it’s supposed to. First, in &lt;code&gt;development.rell&lt;/code&gt;, fix the existing import and, for now, bring in everything we’ve created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import oddly.*;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in &lt;code&gt;tests/module.rell&lt;/code&gt;, add the &lt;code&gt;ft_auth_operation_for&lt;/code&gt; import from FT4 to handle authentication inside the tests. The whole file should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@test module;

import development.*;

import lib.ft4.test.core.{ create_auth_descriptor, ft_auth_operation_for };
import lib.ft4.external.accounts.{ get_account_by_id };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Awesome. Below is a test suite that clearly checks whether everything works the way we intended. One positive case and four negative ones. The main point here is that we don’t just throw some data into &lt;code&gt;create_event&lt;/code&gt; — we walk through the full flow: register an account, authenticate it, run the operation, and then make sure the result matches what we expect. One thing to highlight — notice how we always call &lt;code&gt;ft_auth_operation_for&lt;/code&gt; before invoking &lt;code&gt;create_event&lt;/code&gt;. Without that, the &lt;code&gt;auth.authenticate()&lt;/code&gt; call in your operation just won’t work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function test_create_event() {
    val alice = rell.test.keypairs.alice;

    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());
    rell.test.tx()
        .op(open_strategy.ras_open(auth_descriptor))
        .op(strategies.register_account())
        .sign(alice)
        .run();

    rell.test.tx()
        .op(ft_auth_operation_for(alice.pub))
        .op(create_event("Will CHR be $10 in a week?", 7))
        .sign(alice)
        .run();

    val events = get_events();

    assert_equals(events[0].creator.id, alice.pub.hash());
    assert_equals(events[0].question, "Will CHR be $10 in a week?");
    assert_true(events[0].created_at &amp;gt; 0);
    assert_equals(events[0].is_closed, false);
    assert_true(events[0].expires_at &amp;gt; events[0].created_at);
    assert_equals(events[0].result, result_values.UNRESOLVED);
}

function test_create_event_too_short_question_must_fail() {
    val alice = rell.test.keypairs.alice;
    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    rell.test.tx()
        .op(open_strategy.ras_open(auth_descriptor))
        .op(strategies.register_account())
        .sign(alice)
        .run();

    rell.test.tx()
        .op(ft_auth_operation_for(alice.pub))
        .op(create_event("Too short", 7))
        .sign(alice)
        .run_must_fail("Question must be between 10 and 280 characters.");
}

function test_create_event_too_long_question_must_fail() {
    val alice = rell.test.keypairs.alice;
    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    rell.test.tx()
        .op(open_strategy.ras_open(auth_descriptor))
        .op(strategies.register_account())
        .sign(alice)
        .run();

    val long_question = "A".repeat(300);

    rell.test.tx()
        .op(ft_auth_operation_for(alice.pub))
        .op(create_event(long_question, 7))
        .sign(alice)
        .run_must_fail("Question must be between 10 and 280 characters.");
}

function test_create_event_exceeds_max_expiration_must_fail() {
    val alice = rell.test.keypairs.alice;
    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    rell.test.tx()
        .op(open_strategy.ras_open(auth_descriptor))
        .op(strategies.register_account())
        .sign(alice)
        .run();

    rell.test.tx()
        .op(ft_auth_operation_for(alice.pub))
        .op(create_event("Will CHR be $100?", 400))
        .sign(alice)
        .run_must_fail("Expiration period must be between 1 and 365 days.");
}

function test_create_event_zero_days_must_fail() {
    val alice = rell.test.keypairs.alice;
    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());

    rell.test.tx()
        .op(open_strategy.ras_open(auth_descriptor))
        .op(strategies.register_account())
        .sign(alice)
        .run();

    rell.test.tx()
        .op(ft_auth_operation_for(alice.pub))
        .op(create_event("Will CHR reach $1 today?", 0))
        .sign(alice)
        .run_must_fail("Expiration period must be between 1 and 365 days.");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nice, that’s a wrap — our basic Prediction Market just took its first breath. Here’s what we accomplished today:&lt;/p&gt;

&lt;p&gt;✅ created the first entity&lt;br&gt;
✅ implemented the event creation logic&lt;br&gt;
✅ added validation for user input&lt;br&gt;
✅ wrote a basic query to fetch all events&lt;br&gt;
✅ covered it all with proper unit tests&lt;/p&gt;

&lt;p&gt;Next time we’ll:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;refactor the tests and clean up duplicate logic&lt;/li&gt;
&lt;li&gt;add a new entity called bet where users can place predictions&lt;/li&gt;
&lt;li&gt;expand &lt;code&gt;get_events&lt;/code&gt; to include filtering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s keep building.&lt;/p&gt;

&lt;p&gt;👈 &lt;a href="https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-2-adding-accounts-and-user-registration-3c32"&gt;Previous step&lt;/a&gt; | 💻 &lt;a href="https://github.com/Zahumennov/oddly" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; | &lt;a href="https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-4-bets-moderation-and-validation-rules-ng3"&gt;Next step&lt;/a&gt; 👉&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>cryptocurrency</category>
      <category>web3</category>
      <category>chromia</category>
    </item>
    <item>
      <title>Building a Prediction Market on Chromia | Step 2 — Adding Accounts and User Registration</title>
      <dc:creator>Dmytro Zahumennov</dc:creator>
      <pubDate>Tue, 29 Apr 2025 10:23:05 +0000</pubDate>
      <link>https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-2-adding-accounts-and-user-registration-3c32</link>
      <guid>https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-2-adding-accounts-and-user-registration-3c32</guid>
      <description>&lt;p&gt;Now let’s work on one of the core parts of any dapp — accounts. Accounts allow you to interact with the dapp, sign transactions, hold assets, send and receive tokens, and so on. So we need to add the ability to create accounts in our dapp, and for that we’ll use the FT4 library — it simplifies integration and takes care of everything I just mentioned.&lt;/p&gt;

&lt;p&gt;FT4 supports multiple account strategies. We’ll use two of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For development&lt;/strong&gt; — the &lt;code&gt;open strategy&lt;/code&gt;. This one allows anyone to create accounts without restrictions, which will make our life easier during development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For production&lt;/strong&gt; — &lt;code&gt;open strategy&lt;/code&gt; isn’t a great idea, since someone could spam the chain. So we’ll use at least the &lt;strong&gt;transfer open strategy&lt;/strong&gt;. With this one, the user needs to make a transfer (e.g., 10 CHR) to create an account (we can configure the amount). Later, the user can use those tokens to create an event, for example. More details about FT4 strategies you can find &lt;a href="https://docs.chromia.com/ft4/backend/accounts/overview" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, the first thing we do is go to the &lt;code&gt;src&lt;/code&gt; directory and rename &lt;code&gt;main.rell&lt;/code&gt; to &lt;code&gt;development.rell&lt;/code&gt;. This helps us separate development and production environments — so we can use some functions and operations only during development to speed up the process. Then we create a new directory called &lt;code&gt;oddly&lt;/code&gt;. Inside it, we create a file &lt;code&gt;module.rell&lt;/code&gt; with a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file declares the entire directory as a module, so we don’t need to add &lt;code&gt;module;&lt;/code&gt; in every other file inside it. We’ll also keep all the imports here.&lt;/p&gt;

&lt;p&gt;Next, let’s update the &lt;code&gt;chromia.yml&lt;/code&gt; file. We’ll rename the blockchain and point it to the new main module. Later we’ll add a separate production blockchain with its own module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blockchains:
  oddly_development:
    module: development
compile:
  rellVersion: 0.14.5
database:
  schema: schema_oddly
test:
  modules:
    - test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let’s install the FT4 library. To do that, we add the &lt;code&gt;libs&lt;/code&gt; section to our config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blockchains:
  oddly_development:
    module: development
compile:
  rellVersion: 0.14.5
database:
  schema: schema_oddly
test:
  modules:
    - test
libs:
  ft4:
    registry: https://gitlab.com/chromaway/ft4-lib.git
    path: rell/src/lib/ft4
    tagOrBranch: v1.1.0r
    rid: x"FEEB0633698E7650D29DCCFE2996AD57CDC70AA3BDF770365C3D442D9DFC2A5E"
    insecure: false
  iccf:
    registry: https://gitlab.com/chromaway/core/directory-chain
    path: src/lib/iccf
    tagOrBranch: 1.87.0
    rid: x"9C359787B75927733034EA1CEE74EEC8829D2907E4FC94790B5E9ABE4396575D"
    insecure: false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then we run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chr &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;remote: Enumerating objects: 1776
remote: Counting objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;1776/1776&lt;span class="o"&gt;)&lt;/span&gt;
remote: Compressing objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;1356/1356&lt;span class="o"&gt;)&lt;/span&gt;
Receiving objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;1776/1776&lt;span class="o"&gt;)&lt;/span&gt;
Resolving deltas: 100% &lt;span class="o"&gt;(&lt;/span&gt;861/861&lt;span class="o"&gt;)&lt;/span&gt;
Checking out files: 100% &lt;span class="o"&gt;(&lt;/span&gt;614/614&lt;span class="o"&gt;)&lt;/span&gt;
remote: Enumerating objects: 6445
remote: Counting objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;6445/6445&lt;span class="o"&gt;)&lt;/span&gt;
remote: Compressing objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;2879/2879&lt;span class="o"&gt;)&lt;/span&gt;
Receiving objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;6445/6445&lt;span class="o"&gt;)&lt;/span&gt;
Resolving deltas: 100% &lt;span class="o"&gt;(&lt;/span&gt;4797/4797&lt;span class="o"&gt;)&lt;/span&gt;
Checking out files: 100% &lt;span class="o"&gt;(&lt;/span&gt;464/464&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boom — the library is installed. Now to complete the integration for the &lt;strong&gt;open strategy&lt;/strong&gt;, all we need to do is import it. Let’s add the imports to &lt;code&gt;development.rell&lt;/code&gt; and also import the whole &lt;code&gt;oddly&lt;/code&gt; module for now. The file should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module;

import oddly;
import open_strategy: lib.ft4.core.accounts.strategies.open;
import strategies: lib.ft4.external.accounts.strategies;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. Time to test. First, rename the &lt;code&gt;test&lt;/code&gt; folder to &lt;code&gt;tests&lt;/code&gt;, and inside it, create a folder called &lt;code&gt;development_tests&lt;/code&gt;. Inside that, create a &lt;code&gt;module.rell&lt;/code&gt; file with this content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@test module;

import development.*;

import lib.ft4.test.core.{ create_auth_descriptor };
import lib.ft4.external.accounts.{ get_account_by_id };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@test module;&lt;/code&gt; - declares the entire directory as a test module.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;import lib.ft4.test.core.{ create_auth_descriptor };&lt;/code&gt; - we need this import to create an auth descriptor for testing. Auth descriptors are a mechanism that defines the authorized key pairs and their associated permissions for interacting with accounts on the blockchain. More details you can find &lt;a href="https://docs.chromia.com/ft4/account-management/auth-descriptors" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;import lib.ft4.external.accounts.{ get_account_by_id };&lt;/code&gt; - query to check if the account exists after registration.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;import development.*;&lt;/code&gt; - imports everything from the dev environment. This isn’t very safe, and it’s better to avoid such imports. We’ll fix it later.&lt;/p&gt;

&lt;p&gt;Now let’s write a basic test case to check that account registration works. In &lt;code&gt;src/tests/development_tests&lt;/code&gt;, create a file called &lt;code&gt;account_test.rell&lt;/code&gt; and write next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function test_account_creation() {
    val alice = rell.test.keypairs.alice;

    val auth_descriptor = create_auth_descriptor(alice.pub, ["A", "T"], null.to_gtv());
    rell.test.tx()
        .op(open_strategy.ras_open(auth_descriptor))
        .op(strategies.register_account()).sign(alice)
        .run();

    val alice_account_id = alice.pub.hash();
    val alice_account = get_account_by_id(alice_account_id);

    assert_not_null(alice_account);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we create a test keypair and auth descriptor for Alice’s account. Then we call two operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ras_open&lt;/code&gt; — an operation that initializes the open strategy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;register_account&lt;/code&gt; — an operation that registers the account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since the &lt;code&gt;account ID&lt;/code&gt; is a hash of the &lt;code&gt;public key&lt;/code&gt; (or EVM address, but we’ll get to that later), we use &lt;code&gt;get_account_by_id&lt;/code&gt; to fetch the account and verify it with &lt;code&gt;assert_not_null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One more thing, we need to add an admin &lt;code&gt;public key&lt;/code&gt; to &lt;code&gt;chromia.yml&lt;/code&gt;. Even though we don’t need it for this test, FT4 requires it to run tests properly.&lt;/p&gt;

&lt;p&gt;Let's open terminal and generate a keypair with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chr keygen &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;".admin_keypair"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the public key and paste it into the config like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;blockchains:
  oddly_development:
    module: development
compile:
  rellVersion: 0.14.5
database:
  schema: schema_oddly
&lt;span class="nb"&gt;test&lt;/span&gt;:
  modules:
    - tests
  moduleArgs:
    lib.ft4.core.admin:
      admin_pubkey: x&lt;span class="s2"&gt;"03D57B4ED50601A3E331BA97D6CB9035D7FF90F016831FD15D450596BB4EFE78B1"&lt;/span&gt;
libs:
  ft4:
    registry: https://gitlab.com/chromaway/ft4-lib.git
    path: rell/src/lib/ft4
    tagOrBranch: v1.1.0r
    rid: x&lt;span class="s2"&gt;"FEEB0633698E7650D29DCCFE2996AD57CDC70AA3BDF770365C3D442D9DFC2A5E"&lt;/span&gt;
    insecure: &lt;span class="nb"&gt;false
  &lt;/span&gt;iccf:
    registry: https://gitlab.com/chromaway/core/directory-chain
    path: src/lib/iccf
    tagOrBranch: 1.87.0
    rid: x&lt;span class="s2"&gt;"9C359787B75927733034EA1CEE74EEC8829D2907E4FC94790B5E9ABE4396575D"&lt;/span&gt;
    insecure: &lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s also check that the structure of our project looks correct:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmawxsmvyrm9rtsal478a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmawxsmvyrm9rtsal478a.png" alt="Project stucture" width="562" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And now we can run the test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chr &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And boom - everything works! The account was created successfully 🎉 You should see something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;====================&lt;/span&gt;Running unit &lt;span class="nv"&gt;tests&lt;/span&gt;&lt;span class="o"&gt;====================&lt;/span&gt;
TEST: tests.development_tests:test_account_creation
OK: tests.development_tests:test_account_creation &lt;span class="o"&gt;(&lt;/span&gt;0.871s&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nt"&gt;------------------------------------------------------------------------&lt;/span&gt;
TEST RESULTS:

OK tests.development_tests:test_account_creation &lt;span class="o"&gt;(&lt;/span&gt;0.871s&lt;span class="o"&gt;)&lt;/span&gt;

SUMMARY: 0 FAILED / 1 PASSED / 1 TOTAL &lt;span class="o"&gt;(&lt;/span&gt;0.871s&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;*****&lt;/span&gt; OK &lt;span class="k"&gt;*****&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accounts are up and running! Next, we’ll start shaping the core of our prediction market — building entities and operations one by one. Let’s keep pushing forward! 🔥&lt;/p&gt;

&lt;p&gt;👈 &lt;a href="https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-1-project-setup-53eh"&gt;Previous step&lt;/a&gt; | 💻 &lt;a href="https://github.com/Zahumennov/oddly" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; | &lt;a href="https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-3-creating-events-and-writing-your-first-operation-1m6h"&gt;Next step&lt;/a&gt; 👉&lt;/p&gt;

</description>
      <category>web3</category>
      <category>blockchain</category>
      <category>cryptocurrency</category>
      <category>chromia</category>
    </item>
    <item>
      <title>Building a Prediction Market on Chromia | Step 1 — Project Setup</title>
      <dc:creator>Dmytro Zahumennov</dc:creator>
      <pubDate>Mon, 28 Apr 2025 10:14:42 +0000</pubDate>
      <link>https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-1-project-setup-53eh</link>
      <guid>https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-1-project-setup-53eh</guid>
      <description>&lt;p&gt;Hi! I’m Dmytro, a Dev Advocate at Chromia. Recently, I’ve been feeling a strong urge to start building my own fun side projects, and Chromia’s infrastructure fits perfectly for that. No gas fees, a simple and compact relational programming language (Rell), access to a public testnet, good &lt;a href="https://docs.chromia.com/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, and a &lt;a href="https://learn.chromia.com/" rel="noopener noreferrer"&gt;learning platform&lt;/a&gt; — all of this gives us the chance to build and launch our first MVP pretty fast. To start, we’ll keep things very simple and focus only on the minimum functionality. Then step-by-step, we’ll add more features and develop the dapp further. I think with time, we could even integrate some AI features into the project.&lt;/p&gt;

&lt;p&gt;So, let’s build a Prediction Market on Chromia! The idea is: a user can create a simple event like “Will Chromia hit $10 tomorrow?” and allow others to vote “Yes” or “No” using our own LOL token. Here’s the rough functionality for the MVP (Phase 1):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create events&lt;/li&gt;
&lt;li&gt;Stake LOL tokens to vote&lt;/li&gt;
&lt;li&gt;Manual resolution of event outcomes (admin)&lt;/li&gt;
&lt;li&gt;Distribute rewards among winners&lt;/li&gt;
&lt;li&gt;Very simple UI: a homepage with a list of active markets&lt;/li&gt;
&lt;li&gt;User profiles with voting history&lt;/li&gt;
&lt;li&gt;Deploy dapp on Chromia testnet&lt;/li&gt;
&lt;li&gt;Open access for early testers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s get started with the environment setup! I’m working on MacOS, so I’ll explain things from that perspective. If you are using a different system, you can find instructions on installing and setting everything up in the Chromia documentation and learning platform.&lt;/p&gt;

&lt;p&gt;Chromia is a relational blockchain — it stores data in a relational format, which makes it very unique and developer-friendly. So first, we need to setup PostgreSQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;postgresql@16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew services start postgresql@16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;createuser &lt;span class="nt"&gt;-s&lt;/span&gt; postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"CREATE DATABASE postchain WITH TEMPLATE = template0 LC_COLLATE = 'C.UTF-8' LC_CTYPE = 'C.UTF-8' ENCODING 'UTF-8';"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"CREATE ROLE postchain LOGIN ENCRYPTED PASSWORD 'postchain'; GRANT ALL ON DATABASE postchain TO postchain;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we need the Chromia CLI, which will make it much easier to deploy, test, and launch our dapp to the testnet.&lt;br&gt;
Install it using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/bin/bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap chromia/core https://gitlab.com/chromaway/core-tools/homebrew-chromia.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;chromia/core/chr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that everything installed correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chr &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without thinking too much about the name, let’s call our project Oddly, and create it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chr create-rell-dapp oddly &lt;span class="nt"&gt;--template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;plain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the project in Cursor (or your preferred editor) — and boom, the basic template is ready!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvysj4r8a7msuk3sa3saj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvysj4r8a7msuk3sa3saj.png" alt="Image description" width="542" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I won’t dive into all the details just yet, but let’s quickly review the project structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the &lt;code&gt;src&lt;/code&gt; directory, you’ll find the main module &lt;code&gt;main.rell&lt;/code&gt;, which contains the core logic of the dapp. Over time, we’ll expand it and break it into multiple files for better structure.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;tests&lt;/code&gt; folder is pretty self-explanatory.&lt;/li&gt;
&lt;li&gt;Files like &lt;code&gt;.rell_format&lt;/code&gt; and &lt;code&gt;.rell_lint&lt;/code&gt; are configuration files for formatting and linting — we’ll leave them with default settings for now.&lt;/li&gt;
&lt;li&gt;The key file is &lt;code&gt;chromia.yml&lt;/code&gt;, which handles all the dapp configurations. We’ll refer to it often, and I’ll explain the important parts as we go.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My suggestion: don’t get stuck in the tiny details at this point. Let’s focus on building a working project first — the deep understanding will come naturally through the process.&lt;/p&gt;

&lt;p&gt;Let’s keep moving! 🚀&lt;/p&gt;

&lt;p&gt;💻 &lt;a href="https://github.com/Zahumennov/oddly" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; | &lt;a href="https://dev.to/zahumennov/building-a-prediction-market-on-chromia-step-2-adding-accounts-and-user-registration-3c32"&gt;Next step&lt;/a&gt; 👉&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>chromia</category>
      <category>cryptocurrency</category>
    </item>
  </channel>
</rss>
