Skip to main content

Subscription Chain

Let's implement the Subscription Chain. For this, we will need two entities; account to store the account balance and receipt to store digital receipts for logging purposes. We do this by configuring a new blockchain in chromia.yml.

blockchains:
subscription-chain:
module: subscription_chain

and add the two entities to a new file src/subscription_chain.rell.

src/subscription_chain.rell
module;

import subscription.*;

entity account {
key id: pubkey;
mutable balance: integer = 10000;
}

@log
entity receipt {
// We don't reference account entity here since this entity may be removed
account_id: pubkey;
amount: integer;
period;
blockchain_rid: byte_array;
index account_id, blockchain_rid;
}

As seen here, the account is just modeled by an entity containing an ID and a mutable balance. For simplicity, we give the users 10000 tokens upon account creation.

note

Managing money on the blockchain should be done carefully, and the FT token library should be used.

We also have an entity receipt, which we mark with the @log annotation. This makes all entities in this table permanently immutable. This serves us well since we want anyone to monitor monetary transactions. For this purpose, we add the account_id as the account ID. We could have referenced the account entity directly, but this makes it impossible to delete an account if that is needed for legal reasons. In the receipt, we also add the payment amount, the purchased period, and which blockchain rid of the digital warehouse chain the payment was for. This is done so that the Warehouse Chain can check that the payment was made towards it to prevent double-spending across chains. We also added an index on the account ID and blockchain rid to improve query speed.

Account creation

For this simple example, we create accounts dynamically when a user wants to subscribe. Let's implement a simple function to handle this for us:

function get_or_create_account(id: pubkey) {
require(op_context.is_signer(id));
return account @? { id } ?: create account(id);
}

Subscription

To create a new subscription, we create a new operation, subscribe, which takes the blockchain rid of the chain for which we want to confirm this transaction. We also include the subscription metadata:

operation subscribe(blockchain_rid: byte_array, subscription) {
val account = get_or_create_account(subscription.account_id);
val subscription_fee = period_price(subscription.period);
require(account.balance >= subscription_fee, "Insufficient funds");
account.balance-=subscription_fee;
create receipt (
account_id = subscription.account_id,
subscription_fee,
subscription.period,
blockchain_rid
);
}

This operation gets or creates a new account, deducts the account balance, and creates a receipt.

Queries

For monitoring purposes, being able to query receipts for a specific user or from a transaction might be helpful. Let's finish off this dapp implementation by adding a set of queries:

query get_receipts(account_id: pubkey?, blockchain_rid: byte_array?, from: timestamp?)
= (receipt, account) @* {
if (account_id??) account.id == account_id else true,
account.id == receipt.account_id,
if (blockchain_rid??) .blockchain_rid == blockchain_rid else true,
if (from??) .transaction.block.timestamp >= from else true
} (
receipt_id = .transaction.tx_rid,
account_id = account.id,
period = .period,
payment_amount = .amount,
blockchain_rid = .blockchain_rid,
timestamp = .transaction.block.timestamp
);

This query allows us to filter the receipts on account_id, blockchain_rid, and timestamp.

Unit test

Writing unit tests for this blockchain is straightforward. We do this by creating a new file, src/subscription_chain_test.rell and configuring it in our chromia.yml:

chromia.yml
blockchains:
subscription-chain:
module: subscription_chain
test:
modules:
- subscription_chain_test
src/subscription_chain_test.rell
@test module;

import subscription_chain.*;

val TEST_WAREHOUSE_CHAIN = x"ABAB";

function test_create_subscription() {

rell.test.tx()
.op(subscribe(TEST_WAREHOUSE_CHAIN, subscription(rell.test.pubkeys.alice, period.WEEK)))
.sign(rell.test.keypairs.alice)
.run();

val test_account = account @? { rell.test.pubkeys.alice };

assert_not_null(test_account);
assert_equals(test_account.balance, 10000 - period_price(period.WEEK));
val receipts = get_receipts(test_account.id, null, null);
assert_equals(receipts.size(), 1);
assert_equals(receipts[0].payment_amount, 30);
assert_equals(receipts[0].blockchain_rid, TEST_WAREHOUSE_CHAIN);
}

The test creates a new subscription for the test user, Alice, and verifies that a receipt was created with the correct amounts and blockchain target. We can run the test using the chr test command.