04 / 06Intermediate
Subscription chain
The next step is the Subscription Chain. It needs two entities: account, which holds the account balance, and receipt, which records digital receipts for logging. Start by configuring a new blockchain in chromia.yml.
blockchains: # ↓↓↓ Delete this code snippet ↓↓↓ digital_warehouse_example: module: main # ↑↑↑ Delete this code snippet ↑↑↑ # ↓↓↓ Add this code snippet ↓↓↓ subscription_chain: module: subscription_chain# ↑↑↑ Add this code snippet ↑↑↑Then put the following code in entities.rell inside the src/subscription_chain directory:
entity account { key id: pubkey; mutable balance: integer = 10000;}@logentity receipt { // We don't reference the account entity here since this entity may be removed account_id: pubkey; amount: integer; period; blockchain_rid: byte_array; index account_id, blockchain_rid;}An account is an entity with an ID and a mutable balance. To keep the example simple, every new account starts with 10,000 tokens.
The receipt entity carries the @log annotation, so its entries can never be changed once written. This immutability matters when you need a transparent record of monetary transactions. Each receipt points to its account through account_id rather than a direct reference to the account entity. That way, you can still delete an account if legal reasons require it.
A receipt stores the amount paid, the period purchased, and the blockchain RID (Resource IDentifier) of the Digital Warehouse Chain that the payment was meant for. With this information, the Warehouse Chain can confirm the payment and guard against the same payment being spent on more than one chain. An index on account_id and blockchain_rid speeds up queries.
Account creation
In this example, an account is created on the fly the first time a user subscribes. Add the following function, which handles that, to functions.rell in the src/subscription_chain folder.
function get_or_create_account(id: pubkey) { require(op_context.is_signer(id)); return account @? { id } ?: create account(id);}Subscription
Subscribing is handled by an operation named subscribe. It takes the blockchain RID of the chain where the transaction will be confirmed, along with 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 );}The operation fetches the user's account or creates one if none exists, subtracts the cost from the balance, and writes a receipt.
Queries
For monitoring, you will want to look up receipts by user or by transaction. To finish the dapp, add these queries to queries.rell in the src/subscription_chain directory.
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);You can use this query to filter receipts by account_id, blockchain_rid, and timestamp.
Unit test
Testing this blockchain takes little effort. Create a file called subscription_chain_test.rell in the src/test directory and paste in this code:
@test module;import subscription_chain.{ subscribe, subscription, period, account, period_price, period, get_receipts };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);}You also need to extend chromia.yml with the following configuration:
blockchains: subscription_chain: module: subscription_chain # ↓↓↓ Add this code snippet ↓↓↓ test: modules: - test.subscription_chain_test # ↑↑↑ Add this code snippet ↑↑↑# ↓↓↓ Delete this code snippet ↓↓↓test: modules: - test# ↑↑↑ Delete this code snippet ↑↑↑The test subscribes the test user Alice and verifies that the resulting receipt has the expected amounts and target blockchain.
Run the test with:
chr testFor reference, here is the project structure and the full chromia.yml file as they should look after these changes. Use them to check your work at this point in the course:
Project structure
digital_warehouse_example/├── build/├── src/│ ├── lib/│ ├── subscription_chain/│ │ ├── entities.rell│ │ ├── functions.rell│ │ ├── module.rell│ │ ├── operations.rell│ │ └── queries.rell│ ├── test/│ │ └── subscription_chain_test.rell│ └── main.rell├── .gitignore└── chromia.ymlFinal version of the chromia.yml file
chromia.yml fileyaml title="chromia.yml"blockchains: subscription_chain: module: subscription_chain test: modules: - test.subscription_chain_test digital_warehouse_chain: module: digital_warehouse_chain config: gtx: modules: - net.postchain.d1.iccf.IccfGTXModule moduleArgs: digital_warehouse_chain: warehouse_id: 1 test: modules: - test.digital_warehouse_chain_testcompile: rellVersion: 0.14.9database: schema: schema_digital_warehouse_examplelibs: iccf: registry: https://gitlab.com/chromaway/core/directory-chain path: src/lib/iccf tagOrBranch: 1.87.0 rid: x"9C359787B75927733034EA1CEE74EEC8829D2907E4FC94790B5E9ABE4396575D" iccf_test: registry: https://gitlab.com/chromaway/core/directory-chain path: src/lib/test/iccf tagOrBranch: 1.87.0 rid: x"873D71557531E35E453587FA71C2D3CCB674F0EB49D18B30FA838C52C1155EB3"