Chromia
Start Building

05 / 06Intermediate

Digital warehouse chain

The digital warehouse chain keeps track of inventory for one particular warehouse. To authenticate, a user presents proof of a payment made on the Subscription Chain. That proof gives the user full access to the system until the subscription runs out.

Start by creating a directory called digital_warehouse_chain. Inside it, add a module.rell file containing this definition:

src/digital_warehouse_chain/module.rell
module;

In entities.rell, define a user account entity that stores the subscription's expiration date:

src/digital_warehouse_chain/entities.rell
entity warehouse_account {    key id: pubkey;    mutable valid_until: timestamp = op_context.last_block_time;}

Inventory model

One entity represents the inventory by holding the stock level for a given product ID. A second, logged entity, inventory_log, records every change made to that inventory. Put the following code in entities.rell:

src/digital_warehouse_chain/entities.rell
entity inventory {    key product_category: integer;    UNIT;    mutable stock: integer = 0;}enum UNIT {    LITRE, PIECE, KILOGRAM}@log entity inventory_log {    // We don't reference the inventory entity here since this entity may be removed    product_category: integer;    amount: integer;    comment: text;}struct inventory_dto {    product_category: integer;    amount: integer;    comment: text;}

Users now need a way to register new product categories and to change inventory levels.

update_inventory is the operation that adjusts the current stock and records how and why the stock changed. inventory_dto is a structure that collects the details of an inventory change.

Place this code in the operations.rell file in src/digital_warehouse_chain:

src/digital_warehouse_chain/operations.rell
operation register_product_category(category: integer, UNIT) {    require_authenticated_signer();    create inventory(product_category = category, UNIT);}operation update_inventory(inventory_dto) {    require_authenticated_signer();    update inventory @ { .product_category == inventory_dto.product_category } (stock += inventory_dto.amount);    create inventory_log(        product_category = inventory_dto.product_category,        amount = inventory_dto.amount,        comment = inventory_dto.comment        );}

You also need a helper function in the functions.rell file, located in the src/digital_warehouse_chain directory.

require_authenticated checks three things: the user exists, the user signed the transaction, and the user's subscription is still valid:

src/digital_warehouse_chain/functions.rell
function require_authenticated_signer() {    require(op_context.get_signers().size() == 1, "Require exactly one signature");    val valid_until = warehouse_account @? { op_context.get_signers()[0]}.valid_until;    require(exists(valid_until), "No account found");    require(op_context.last_block_time <= valid_until!!, "Subscription has expired");}

Authentication

User authentication relies on ICCF. To turn ICCF on for the Digital Warehouse Chain, add this configuration to chromia.yml:

chromia.yml
blockchains:  # ↓↓↓ Add this code snippet ↓↓↓  digital_warehouse_chain:    module: digital_warehouse_chain    config:      gtx:        modules:          - net.postchain.d1.iccf.IccfGTXModule# ↑↑↑ Add this code snippet ↑↑↑

Next, import the ICCF module in module.rell:

src/digital_warehouse_chain/module.rell
import lib.iccf;import subscription_chain.{ subscription, period, period_to_millis };

Now define the authorize operation. It accepts a gtx_transaction holding the transaction data that ICCF must confirm. To guarantee each transaction is used only once, the operation stores the transaction hash, then verifies the transaction and extracts the operation arguments. The first argument must be the RID of the blockchain where the payment took place; the second must be the subscription metadata. The operation checks that the blockchain RID is correct and decodes the metadata.

Put the operation in operations.rell inside the src/digital_warehouse_chain directory:

src/digital_warehouse_chain/operations.rell
operation authorize(tx: gtx_transaction) {    iccf.make_transaction_unique(tx);    val args = iccf.extract_operation_args(tx, "subscribe", verify_signers = true);    require(byte_array.from_gtv(args[0]) == chain_context.blockchain_rid, "Wrong blockchain proof, found %s".format(chain_context.blockchain_rid));    val subscription = subscription.from_gtv(args[1]);    val warehouse_account = get_or_create_account(subscription.account_id);    val new_expiration_date = max(op_context.last_block_time, warehouse_account.valid_until) + period_to_millis(subscription.period);    warehouse_account.valid_until = new_expiration_date;}

The last step fetches the account, or creates it if it doesn't exist, and pushes its expiration date forward by the subscription period. Because the period is added to the current expiration date, the timing of a purchase never shortens a user's access.

Add this code to functions.rell in the src/digital_warehouse_chain directory:

src/digital_warehouse_chain/functions.rell
function get_or_create_account(id: pubkey) {    require(op_context.is_signer(id));    return warehouse_account @? { id } ?: create warehouse_account(id);}

Creating a report

To finish, you write a query that produces a report together with a history of every inventory update. The smart contract covers only one warehouse, so a module argument supplies the warehouse information that appears in the report.

Add this struct to entities.rell in the src/digital_warehouse_chain directory:

src/digital_warehouse_chain/entities.rell
struct module_args {    warehouse_id: integer;}

Then put the create_report query in queries.rell, also in the src/digital_warehouse_chain directory:

src/digital_warehouse_chain/queries.rell
query create_report(from: timestamp?, to: timestamp?) {    val current_inventory = inventory @* {} ($.to_struct());    val history = inventory_log @* {        if (from??).transaction.block.timestamp >= from else true,        if (to??).transaction.block.timestamp < to else true,        .product_category in current_inventory @* {}( .product_category )        }(        $.to_struct()    );    return (warehouse_id = chain_context.args.warehouse_id, inventory = current_inventory, history = group_logs_by_product(history));}

The group_logs_by_product function belongs in functions.rell in the src/digital_warehouse_chain folder.

The query gathers the current inventory and its update history, then returns them in a tuple alongside the warehouse ID:

src/digital_warehouse_chain/functions.rell
function group_logs_by_product(value: list<(struct<inventory_log>)>) {    val result = map<integer, list<struct<inventory_log>>>();    for (v in value) {        if (v.product_category not in result) result[v.product_category] = [];        result[v.product_category].add(v);    }    return result;}

Set the warehouse ID by adding configuration (moduleArgs) to chromia.yml:

chromia.yml
blockchains:  digital_warehouse_chain:    # ↓↓↓ Add this code snippet ↓↓↓    moduleArgs:      digital_warehouse_chain:        warehouse_id: 1    # ↑↑↑ Add this code snippet ↑↑↑

Unit tests

Tests for this app go in a new file, src/digital_warehouse_chain_test.rell:

src/test/digital_warehouse_chain_test.rell
@test module;import digital_warehouse_chain.{ subscription, period, authorize, warehouse_account, period_to_millis, register_product_category, update_inventory, inventory_dto, create_report, UNIT };import lib.iccf_test.iccf_test_lib.{ iccf_proof_for };function test_grant_access() {    val certificate = subscription(        account_id = rell.test.pubkeys.alice,        period = period.WEEK    );    val tx = rell.test.tx();    val gtx = gtx_transaction(        body = gtx_transaction_body(            blockchain_rid = x"AB",            operations = [gtx_operation(name = "subscribe", args = [chain_context.blockchain_rid.to_gtv(), certificate.to_gtv()])],            signers = [rell.test.pubkeys.alice.to_gtv()]        ),        signatures = []    );    rell.test.set_next_block_time(1);    rell.test.block().run();    rell.test.tx()        .op(iccf_proof_for(x"AB", gtx))        .op(authorize(gtx))        .sign(rell.test.keypairs.alice)        .run();    val warehouse_account = warehouse_account @? { rell.test.pubkeys.alice };    assert_not_null(warehouse_account);    assert_equals(warehouse_account.valid_until, 1 + period_to_millis(period.WEEK));    rell.test.tx()        .op(register_product_category(101, UNIT.LITRE))        .op(update_inventory(inventory_dto(product_category = 101, amount = 5000, "Received milk shipment from barn")))        .sign(rell.test.keypairs.alice)        .run();    val report = create_report(null, null);    assert_equals(report.warehouse_id, 1);    assert_equals(report.inventory[0].stock, 5000);    assert_equals(report.history[101].size(), 1);    assert_equals(report.history[101][0].comment, "Received milk shipment from barn");}

Then add the test definition and its settings to chromia.yml:

chromia.yml
blockchains:  digital_warehouse_chain:    # ↓↓↓ Add this code snippet ↓↓↓    test:      modules:        - test.digital_warehouse_chain_test    # ↑↑↑ Add this code snippet ↑↑↑

The test builds a dummy gtx_transaction for the alice pubkey. Before calling the authorize operation, it forces the next block time to 1 and runs an empty block, so the block time values are predictable when you check the results.

The final transaction contains a gtx_operation that calls iccf_proof with placeholder arguments, except for gtx.hash().to_gtv(). The test framework can't verify proofs directly, so the test works around this by building the transaction and passing its correct hash. The test then confirms the account was created, records an inventory update, and checks the resulting report.

Run the tests with the chr test command.

For reference, here are the project structure and the full chromia.yml configuration after these changes. Use them to compare against your own project at this point in the course:

Project structure

text
digital_warehouse_example/├── build/├── src/│   ├── digital_warehouse_chain/│   │   ├── entities.rell│   │   ├── functions.rell│   │   ├── module.rell│   │   ├── operations.rell│   │   └── queries.rell│   ├── lib/│   ├── subscription_chain/│   │   ├── entities.rell│   │   ├── functions.rell│   │   ├── module.rell│   │   ├── operations.rell│   │   └── queries.rell│   ├── test/│   │   ├── subscription_chain_test.rell│   │   └── digital_warehouse_chain_test.rell│   └── main.rell├── .gitignore└── chromia.yml

Final version of the chromia.yml

text
yaml 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"

Written for this site from Chromia’s public course. Code samples are the course’s own.