Skip to main content

Digital Warehouse Chain

The digital warehouse chain should track inventory for a specific warehouse. A user gets authenticated by showing proof of payment on the Subscription Chain. They can then use the system as much as they want until the subscription has ended. We model a user account as a simple entity in src/digital_warehouse_chain.rell that holds an expiration date for the subscription:

src/digital_warehouse_chain.rell
module;

entity account {
key id: pubkey;
mutable valid_until: timestamp = op_context.last_block_time;
}

Inventory model

Let's model our inventory as a single entity that tracks the stock level of a certain product ID:

src/digital_warehouse_chain.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;
}

We also add a logged entity, inventory_log to keep track of any changes in the inventory. We then allow our users to register a new product category and update inventory by adding the following operations:

src/digital_warehouse_chain.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
);
}

function require_authenticated_signer() {
require(op_context.get_signers().size() == 1, "Require exactly one signature");
val valid_until = 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");
}
struct inventory_dto {
product_category: integer;
amount: integer;
comment: text;
}

The function require_authenticated ensures a user exists, has signed the transaction, and has a valid subscription. The update_inventory operation updates the current inventory stock and adds a log for how and why it changed. inventory_dto is a structure where we collect information about the inventory change.

Authentication

Authentication of a user will be done by ICCF. We, therefore, configure our Digital Warehouse Chain to use ICCF by adding the following to our chromia.yml:

chromia.yml
blockchains:
...
digital-warehouse-chain:
module: digital_warehouse_chain
config:
gtx:
modules:
- net.postchain.d1.iccf.IccfGTXModule

We then import the ICCF module and define the following operation:

src/digital_warehouse_chain.rell
import lib.iccf;
import subscription.*;

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 account = get_or_create_account(subscription.account_id);
val new_expiration_date = max(op_context.last_block_time, account.valid_until) + period_to_millis(subscription.period);
account.valid_until = new_expiration_date;
}

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

This operation takes a gtx_transaction, which contains the transaction info about the transaction we want to confirm with ICCF. It then forces the transaction to be uniquely verified by this blockchain, by storing a hash of the transaction, before verifying and extracting the operation arguments from the transaction. The first argument of the transaction should contain the blockchain rid for which the payment was made, and the second argument should contain the subscription metadata. We verify that the blockchain rid is correct and decode the subscription metadata.

Finally, we get or create a new account and add the subscription period to the account's expiration date. This way, it does not matter when a subscription is bought; it will just add the period to the expiration date.

Creating a report

Finally, we define a query for which one can create a report and history log of all inventory updates that has happened. Since this smart contract only represents a single warehouse, we add a module argument containing information about the warehouse to include in the report.

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

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));
}

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;
}

The query collects the current inventory and the historical updates and collects them in a tuple together with the warehouse ID. We configure the warehouse ID in the chromia.yml using moduleArgs:

chromia.yml
blockchains:
digital-warehouse-chain:
...
moduleArgs:
digital_warehouse_chain:
warehouse_id: 1
tip

Spawning multiple warehouses is as easy as defining several blockchains in the chromia.yml with different module arguments:

blockchains:
digital-warehouse-chain-1:
module: digital_warehouse_chain
...
moduleArgs:
digital_warehouse_chain:
warehouse_id: 1
digital-warehouse-chain-2:
module: digital_warehouse_chain
...
moduleArgs:
digital_warehouse_chain:
warehouse_id: 2

Unit tests

To test this application, we start by creating a new file, src/digital_warehouse_chain_test.rell, and configure it in our YAML file:

chromia.yml
blockchains:
digital-warehouse-chain-1:
...
test:
modules:
- digital_warehouse_chain_test
src/digital_warehouse_chain_test.rell
@test module;

import digital_warehouse_chain.*;

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(gtx_operation(name = "iccf_proof", args = [x"AB".to_gtv(), gtx.hash().to_gtv(), x"".to_gtv()]).to_test_op())
.op(authorize(gtx))
.sign(rell.test.keypairs.alice)
.run();

val account = account @? { rell.test.pubkeys.alice };
assert_not_null(account);
assert_equals(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");
}

This test creates a dummy gtx_transaction for the alice pubkey. It then forcefully sets the next block time to 1 and runs an empty block before calling the authorize operation. This is to get a nice value for the block times when verifying the results.

As seen by the final transaction we run, we insert a gtx_operation calling iccf_proof with some dummy values except for the gtx.hash().to_gtv(). This is because the test framework will not verify any proofs, so it is enough to construct the transaction and pass the correct tx-hash. We verify that the account was created successfully, register an inventory update, and verify the report it produces.

The tests are again executed with chr test.