And we’re back. It’s time to start exploring the core of our asset tracking blockchain solution.

(In case you missed the last post, I showed a demo and explained the design thinking behind this project: it’ll be very helpful to understanding what I discuss below.)

Let’s take a look at the smart contract that controls our assets.

Get the Git

This is where you’ll probably want to have the code handy, so clone or just take a look over on Github:

dragonchain-asset-tracker (this link opens in a new window) by Dragonchain-Community (this link opens in a new window)

A NodeJS smart contract, API server, web demo, and NFC-enabled Flutter mobile app demonstrating blockchain-enabled asset tracking.

Overview

The smart contract for this solution was written in Node.js (just like my previous example that explores the basics of smart contracts in detail). This affords me the use of the Dragonchain SDK that’s already ready to rock for Node.js.

If you’re familiar with Javascript at all, the vast majority of my code should be pretty easily understandable (I tend to be very verbose in my code anyway).

I started from the same template provided by the Dragonchain team because why reinvent the wheel? You can get those starter contracts on Dragonchain’s Github.

I won’t be going over every single method I implemented in detail here, but I will cover at least enough to explain some important features we’re making use of. Namely:

  1. The smart contract object heap
  2. Custom indexes

Before we get to that, let’s look at deploying the contract and setting up the API server and web demo, then look at a list of all the methods that are implemented in the contract.

Deployment Process

If you haven’t already learned the very basics of deploying any smart contract to a Dragonchain Level 1 business node, definitely check out my last tutorial series.

Once you have DCTL ready to rock and wired up to your L1, here’s the sample contract create command included in the documentation:

dctl c c asset_tracker \
mydockerhub/asset-tracker:latest \
node index.js \
-s \
-r MYBASE64ENCODEDUSERNAMEPASSWORD \
--customIndexes '[{"fieldName":"response_type","path":"$.response.type","type":"tag"},{"fieldName":"custodian_type","path":"$.response.custodian.type","type":"tag"},{"fieldName":"custodian_external_data_custodian_id","path":"$.response.custodian_external_data.custodianId","type":"tag"},{"fieldName":"custodian_external_data_external_id","path":"$.response.custodian_external_data.external_data.id","type":"tag"},{"fieldName":"asset_assigned_asset_group_id","path":"$.response.asset.assetGroupId","type":"tag"},{"fieldName":"asset_external_data_asset_id","path":"$.response.asset_external_data.assetId","type":"tag"},{"fieldName":"asset_external_data_external_id","path":"$.response.asset_external_data.external_data.id","type":"tag"},{"fieldName":"asset_transfer_asset_id","path":"$.response.asset_transfer.assetId","type":"tag"},{"fieldName":"asset_transfer_asset_transfer_authorization_id","path":"$.response.asset_transfer.assetTransferAuthorizationId","type":"tag"},{"fieldName":"asset_transfer_from_custodian_id","path":"$.response.asset_transfer.fromCustodianId","type":"tag"},{"fieldName":"asset_transfer_to_custodian_id","path":"$.response.asset_transfer.toCustodianId","type":"tag"},{"fieldName":"asset_transfer_authorization_asset_id","path":"$.response.asset_transfer_authorization.assetId","type":"tag"},{"fieldName":"asset_transfer_authorization_from_custodian_id","path":"$.response.asset_transfer_authorization.fromCustodianId","type":"tag"},{"fieldName":"asset_transfer_authorization_to_custodian_id","path":"$.response.asset_transfer_authorization.toCustodianId","type":"tag"}]'

You’ll note right off that we’re including a bunch of custom indexes. I’ll cover more on those in a bit.

The next thing to note is the -s flag. The -s flag sets the contract to execute in serial mode (as opposed to the default parallel mode).

This means that if two requests come in at virtually the same instant, the first received transaction will complete before the second is processed. This is important to prevent race conditions in certain methods, particularly if someone wanted to batch-process adding assets and so forth.

Other than that, the install command is pretty much the same as it was in the previous demos.

Remember you can always get help for a DCTL command with the “help” argument, like the following examples:

dctl contract help create
dctl contract help update
dctl contract object help get

There are a lot of great features built into DCTL to explore. Get after it.

API and Web Demo Server Setup

Once the contract has been deployed, the next step will be to setup the API server.

In api-server/config.js, you’ll need to paste in the ID of the smart contract (the one given when creating it with the DCTL command above).

Next, run the following to initialize the contract with the authority custodian:

node ./api-server/initialize-dc-node.js

Copy the authority custodian’s ID that will be displayed, then edit web-demo/public/asset-tools.js and paste the authority ID.

Finally, to fire everything up, run the following in two different terminal windows/tabs (assuming you start from the asset-tracker main folder):

cd api-server && npm start

node ./web-demo/server.js

Then you should be able to go to http://127.0.0.1:3005 to use the web demo. Woo!

On to the code.

The Methods

create_custodian

  • Restrictions: Only the authority may create custodians. Only one authority-type custodian may be created per contract instance.
  • Notes: Accepts optional parameter “custodian.external_data”

create_asset_group

  • Restrictions: Only the authority may create asset groups.

create_asset

  • Restrictions: Only the authority may create assets.
  • Notes: Accepts optional parameter “asset.external_data”

set_custodian_external_data

  • Restrictions: Only the authority or the custodian may set a custodian’s external data.

set_asset_external

  • Restrictions: Only the authority may set an asset’s external data directly.

add_asset_external_data_as_custodian

  • Restrictions: Only the current custodian of an asset may add external data.
  • Notes: This method was created to allow the current custodian of an asset to add necessary additional information (like the NFC tag ID in our case that is only available when the custodian has claimed ownership of the asset). If a key already exists in the asset’s external data, the method fails.

authorize_asset_transfer

  • Restrictions: Only the current custodian of an asset may authorize its transfer.
  • Notes: the receiving custodian parameter is optional. Without this parameter, any custodian may claim ownership of this asset.

accept_asset_transfer

  • Restrictions: Only the receiving custodian of an asset may accept its transfer.
  • Notes: If no receiving custodian is specified, any custodian may accept an asset transfer.

That’s all there is to it.

There are a couple of other methods I can think of that might be useful in a production system or as optional functionality I just didn’t need (like the ability to revoke a transfer authorization before it’s been accepted), but I’ll leave such extensions as exercises for you.

The Code Flow

You’ll notice that we set our “start” command when installing the contract to node index.js.

In looking at the index.js file, we see that we’re grabbing what’s passed to STDIN, then providing that value to our arbitrarily named “handler.”

In contract/handler.js, we parse the entire JSON stringified transaction that was passed in, then get to work.

  1. We instantiate a Dragonchain SDK client and assign it to our “tracker” object
  2. We get the authenticated custodian if provided or set it to null if not
  3. We use Reflection to call the appropriate method on our tracker object and save the output
    1. Remember that our payload will include a method name to call and parameters to pass to it
    2. We also include the request transaction’s ID and the authenticatedCustodian object to keep our actual contract method code as “pure” as possible for better testing.
  4. We return the output back to index.js where it gets written to STDOUT

All the rest of the code magic is done in contract/asset-tracker.js, so let’s take a look at an actual contract method and start digging a little deeper.

Example Method: Create a Custodian

Let’s take a look at an example method to give us an idea of what the code looks like. Then we’ll use this same example to talk about the two important features I mentioned above (the heap and our custom indexing).

create_custodian: async function (requestTxnId, parameters, authenticatedCustodian) {        
        const inCustodian = parameters.custodian;
        const inCustodianExternalData = typeof parameters.custodian.external_data !== "undefined" ? 
                                            parameters.custodian.external_data : undefined;
        
        if (inCustodian.type == "authority")
        {
            // Be sure there isn't already an authority record //
            let authority = null;

            try {                
                authority = await this.getAuthorityCustodianID();
            } catch (e) {}

            if (authority != null)                
                throw "An authority record already exists for this contract instance.";               
        
        } else if (authenticatedCustodian == null)
        {
            throw "The authenticated authority custodian must be provided.";
        } else {
            if (authenticatedCustodian.type != "authority")
                throw "Only the authority custodian may create additional custodians.";
        }

        let responseObj = {
            "type": "custodian",                
            "custodian": {
                "id": requestTxnId,                    
                "type": inCustodian.type
            }
        }

        if (inCustodianExternalData)
        {
            responseObj.type += ",custodian_external_data";
            responseObj.custodian_external_data = {
                "id": requestTxnId,
                "custodianId": requestTxnId,                    
                "external_data": inCustodianExternalData
            }
        }

        const custodianKey = `custodian-${requestTxnId}`;

        const custodian = {
            "id": requestTxnId,                        
            "type": inCustodian.type,
            "current_external_data": typeof responseObj.custodian_external_data !== "undefined" ? 
                                         responseObj.custodian_external_data.external_data : null,
            "assets": []
        }

        let finalResponse = {
            "response": responseObj,
            [custodianKey]: custodian
        };

        if (responseObj.custodian.type == "authority")
            finalResponse["authority-custodian-id"] = responseObj.custodian.id;

        return finalResponse;
    }

Fairly straightforward, I think.

Notice the “final response” object that we’re sending back. It has two top level properties: response and a property that will end up looking like custodian-3dc5641e-6db1-4abc-812e-193f0e8f4ecd. Weird.

In fact, here’s an actual final output payload from creating a custodian:

{
    "response": {
      "type": "custodian,custodian_external_data",
      "custodian": {
        "id": "3dc5641e-6db1-4abc-812e-193f0e8f4ecd",
        "type": "owner"
      },
      "custodian_external_data": {
        "id": "3dc5641e-6db1-4abc-812e-193f0e8f4ecd",
        "custodianId": "3dc5641e-6db1-4abc-812e-193f0e8f4ecd",
        "external_data": {
          "id": "1234",
          "data": {
            "name": "John Doe",
            "description": ""
          }
        }
      }
    },
    "custodian-3dc5641e-6db1-4abc-812e-193f0e8f4ecd": {
      "id": "3dc5641e-6db1-4abc-812e-193f0e8f4ecd",
      "type": "owner",
      "current_external_data": {
        "id": "1234",
        "data": {
          "name": "John Doe",
          "description": ""
        }
      },
      "assets": []
    }
}

So what’s that all about?

We’re making use of Dragonchain’s smart contract object heap feature.

The Smart Contract Object Heap

For every output transaction your smart contract creates, the top-level properties of the output payload will become objects on the heap keyed by their property name.

So starting with the response property, what this means is that there will always be an object with the key “response” that we can pull from the smart contract object heap, and it will always be the most recent response object output by our smart contract.

That’s cool, but not particularly useful.

The second property of the payload object, though, has the key “custodian-3dc5641e-6db1-4abc-812e-193f0e8f4ecd,” an entirely unique key that won’t get overwritten unless we explicitly output that object again.

This means that we can effectively maintain stateful data that’s still deterministic (since it exists as the result of the normal payload output).

In this case, we’ve created the “current state” of custodian John Doe. If John Doe later receives an asset, his heap object will be output again with the ID of the asset added to his “assets” array.

This is ridiculously amazingly powerful as it means that we can always get the current state of John Doe (or any custodian, asset group, or asset) in the system without having to “calculate” its current state by iterating over all of its previous transactions.

Now, a few notes on the smart contract heap:

  1. The only way to “write to” the heap is by including whatever you want to exist as an object as a top-level property of your output payload. If we could just write to it all willy-nilly, we’d lose the deterministic nature of the data in our system (there’d be no proof of what the output should be or how it got there).
  2. The smart contract heap is file system based and highly durable (we’re not talking about “heap” as in “heap memory”). Use with confidence.
  3. If you’re trying to decide between storing an object in the heap and querying your Dragonchain using fancy custom indexing, you should probably go with the heap. It’s a LOT faster and less resource intensive.

Speaking of custom indexes: the heap is great, but if we need to do more complex lookups, we’ll need to have some indexing. Let’s take a look at that next.

Custom Indexes

What if we wanted to get a list of every custodian in the system? Or what if we wanted to look up a custodian by its external ID (the one that might be used in a more traditional database-backed application like a CRM)?

Let’s examine that second query (looking up a custodian by its external ID) in closer detail.

I’ve created a custom index on the “external ID” field that looks like this (prettified):

{
   "fieldName":"custodian_external_data_external_id", 
   "path":"$.response.custodian_external_data.external_data.id",
   "type":"tag"
}

What that means is that I can now query all of the transactions on my Dragonchain node with something like the following using DCTL (the syntax for the redisearch query is identical using the SDK):

dctl transaction query asset_tracker "@custodian_external_data_external_id:{1234}"

And get back the custodian creation transaction where I assigned that external ID of 1234 to the custodian:

{
  "status": 200,
  "response": {
    "total": 1,
    "results": [
      {
        "version": "2",
        "dcrn": "Transaction::L1::FullTransaction",
        "header": {
          "txn_type": "asset_tracker",
          "dc_id": "uDVMagYWemvWH281ry7zdX6kap7e3dBZhJjuCbDyh4qU",
          "txn_id": "8f83be73-e57d-4d7f-9611-5c46da819ba5",
          "block_id": "28288879",
          "timestamp": "1573682613",
          "tag": "",
          "invoker": "3dc5641e-6db1-4abc-812e-193f0e8f4ecd"
        },
        "payload": {
          "response": {
            "type": "custodian,custodian_external_data",
            "custodian": {
              "id": "3dc5641e-6db1-4abc-812e-193f0e8f4ecd",
              "type": "owner"
            },
            "custodian_external_data": {
              "id": "3dc5641e-6db1-4abc-812e-193f0e8f4ecd",
              "custodianId": "3dc5641e-6db1-4abc-812e-193f0e8f4ecd",
              "external_data": {
                "id": "1234",
                "data": {
                  "name": "John Doe",
                  "description": ""
                }
              }
            }
          },
          "custodian-3dc5641e-6db1-4abc-812e-193f0e8f4ecd": {
            "id": "3dc5641e-6db1-4abc-812e-193f0e8f4ecd",
            "type": "owner",
            "current_external_data": {
              "id": "1234",
              "data": {
                "name": "John Doe",
                "description": ""
              }
            },
            "assets": []
          }
        },
        "proof": {
          "full": "EFo1j8eASGi+rW5L+R88/bRL2FrbeACQS7DpJ925Zyw=",
          "stripped": "MEQCID1NTxqfFUW2+H7RXT1z+gLZCbeWXxIlhHrZhf4HbnkyAiA55D+EzoLzk8Q4JliTyZClqyLZMOdovum5JyWrj6bzrg=="
        }
      }
    ]
  },
  "ok": true
}

Note that the “response” object in the result of my querying is actually an array of results, so if I searched for, say, any “handler” type custodians (@custodian_type:{handler}), I might get an array of 5 or 50 such custodians.

This kind of querying can only be done by creating custom indexes on the fields we might need to query on.

It works very well, but keep in mind a few notes on custom indexes:

  1. Indexing is “expensive” in terms of resources and compute time, so only use it when absolutely necessary
    1. Very complex querying scenarios may need to be broken into smaller steps or even handled by a more traditional database outside of Dragonchain.
  2. I chose to use the “tag” type for all of my indexes because a) I know I’d only ever need to search for exact values like an ID, b) “tag” fields are much less resource intensive than “string” fields, and c) the normal “string” type (meant normally for searching full documents with all kinds of stemming and crazy such features) just wasn’t working with the data I WAS searching on. Escaping strings is tricky in redisearch.
  3. Again, if you have a choice between using the contract heap and indexing another field, go with the heap.

Redisearch syntax can be super tricky, so be sure again to keep this link handy for your custom indexing and search query building needs.

Wrapping Up

Phew! That was a long-ass post, but there was a lot to cover.

In the next post (part 2 on the smart contract), I want to explore testing our smart contract outside of what used to be my normal method: “deploy it and see if it works.” That method gets expensive in blockchain world, but testing is here to save the day.

Until next time,
John

Leave a comment

Your email address will not be published. Required fields are marked *