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:
module;In entities.rell, define a user account entity that stores the subscription's expiration date:
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:
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:
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:
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:
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:
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:
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:
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:
struct module_args { warehouse_id: integer;}Then put the create_report query in queries.rell, also in the src/digital_warehouse_chain directory:
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:
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:
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:
@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:
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
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.ymlFinal version of the chromia.yml
chromia.ymlyaml 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"