Our Second Dragonchain Smart Contract: a Node.js Example

Welcome back!

In our last post, we went through the start-to-finish process for deploying a simple, shell script-based, automatically invoked Dragonchain smart contract. It was neat.

Let’s do something neater.

In this post (it’ll be a doozy), we’ll take a look at a much more interesting smart contract example that:

  • We’ll invoke on-demand by creating a transaction
  • Will be written in Node.js
  • Will make use of the Dragonchain SDK to interact with our own node
  • Will use the “secrets” feature of Dragonchain to store an API key we don’t want to expose
  • Will create a custom index on part of our payload so we can do things like searching and sorting later
  • Will demonstrate a neat method for saving an “extra” transaction when we want to
  • Will be completely contrived and mostly useless

Let’s jump in.

Get the Git

You’ll definitely want to checkout the Community Github and example source code for this particular walk through. I won’t post the entire contract script within this post (though I’ll pull out interesting bits and pieces), so open it up in a new tab if you’d like to follow along in the code.

Contract Overview

Let’s talk about what this smart contract is actually going to do.

In the example from the last post, we wrote a script that would automatically record the current price of $DRGN tokens based on a cron schedule.

This time, let’s make our contract do the same thing when invoked, not on an automatic schedule.

We’ll also make it accept the “slug” or name of a cryptocurrency to look up rather than hard coding the Dragonchain ID from CoinMarketCap.

Finally, let’s add a second function to record the moment-in-time price difference from the last recorded price.

Like I said, not extremely useful, but it does let us explore some neat aspects of writing smart contracts.

I’m going to write the contract in Node.js for a couple of reasons:

  1. I’m very familiar with writing Javascript
  2. Using Node.js means I can utilize the already-built Dragonchain SDK for Node.js

Alright. Let’s look at some code.

Note that I built my Node.js contract example using the smart contract templates available in a variety of languages from the Dragonchain team on their Github here:

Get the Dragonchain Smart Contract Templates

Clients, Secrets, and Input Parsing, Oh My!

First interesting block of code of the day:

// Create the Dragonchain client //
const client = await dcsdk.createClient();

// Fetch our coinmarketcap API key from secrets (only works after deploying the smart contract!) //
const apiKey = await client.getSmartContractSecret({"secretName":"cmcapikey"});
      
// To test our contract code BEFORE deploying the contract for the first time, I'll just hard code the apiKey here //
// Note: Just be careful (talking to myself here) about accidentally git committing with secret info in the source code... //
//const apiKey = "YOURAPIKEYHERE";

// Parse the request //
let inputObj = JSON.parse(input);

So the first thing we do is create an instance of the Dragonchain SDK client.

“But we don’t provide any credentials of any kind!” you object. And you’re right.

The Dragonchain SDK pulls the necessary credentials from environment variables set in our Docker container automatically by the smart contract installation process. Neat.

Next, we use the client.getSmartContractSecret method to pull our CoinMarketCap API key that’s somehow saved as a secret of some kind.

I’ll show you how we add that secret when we get to deploying the contract. For now, know that you’ll HAVE TO HAVE DEPLOYED your contract AND be running the code INSIDE the contract (not testing locally) to be able to actually get that secret value, so I’ve included commented out code just below it that we can use when testing our script.

Finally, we parse the input text that was passed into our handler function.

Let’s talk about what actually gets sent to our smart contract as input when we invoke it manually.

Remember that we invoke a smart contract by creating a normal transaction and passing any required information to the smart contract via the payload.

Here’s an example showing what that request transaction might look like in our case:

{
  "version": "2",
  "dcrn": "Transaction::L1::FullTransaction",
  "header": {
    "txn_type": "node_example",
    "dc_id": "uDVMagYWemvWH281ry7zdX6kap7e3dBZhJjuCbDyh4qU",
    "txn_id": "b0c050dd-7a3a-4dcc-acda-5d468e34ddda",
    "block_id": "27773028",
    "timestamp": "1571103356",
    "tag": "",
    "invoker": ""
  },
  "payload": {
    "method": "create_price_snapshot",
    "parameters": {
      "currencySlug": "bitcoin"
    }
  },
  "proof": {
    "full": "0J0GwJLEF4Ov55qhoiQTVKMl5O8SD93exi3bFgUby+o=",
    "stripped": "MEQCIE5KFnZmjItZZuSHKsrkP+tj/6ReIZMATR/9eetXv6GbAiBfaY8MgrwA+7wWU6RuKjPhUNPL+0wHEMVhUZ8URqsYng=="
  }
}

So when we get to the JSON.parse line in the contract code, THAT’S what we’re parsing, and what we’re interested in (in this case) is the payload object with our method and parameters.

Let’s continue.

Our Methods, or, “The Stuff That Does Stuff”

Our first method option that we’ve coded for is create_price_snapshot which will do exactly what our shell script did: use the CoinMarketCap API (provided as a module in contract/coinmarketcap.js) to lookup the specified currency’s current price and record it to the chain:

getprice(apiKey, inputObj.payload.parameters.currencySlug)
    .then(response => {
        // Get the numeric cryptocurrencyId from the response object
        const keys = Object.keys(response.data);
        const cryptocurrencyId = keys[0];

        // Pass the price quote object back to the callback function //
        callback(undefined, {
            snapshot: {
                slug: response.data[cryptocurrencyId].slug, 
                quote: response.data[cryptocurrencyId].quote.USD
            }
        });

    }).catch(err => {
        // Pass the error back to our callback function //
        callback(err, undefined);
    })

Pretty straightforward. If we get a good response from the API, we parse out the price quote and then pass a “snapshot” object back to the callback function sent to our handler (with an “undefined” for the error message argument in the callback function).

Next, we create the new method, record_current_diff. The first interesting bit there is where we use the Dragonchain SDK’s client.queryTransactions method to look up the last recorded transaction for the specified currency:

// Look up last recorded price //
const lastTransaction = await client.queryTransactions(
    {
        transactionType: "node_example", 
        redisearchQuery: `@snapshot_slug:${inputObj.payload.parameters.currencySlug}`,
        limit: 1,
        sortBy: "timestamp",
        sortAscending: false
    });     

Note that for queryTransactions, we have to provide the transactionType which, in a smart contract, will always be the smart contract’s name.

Then we specify a redisearchQuery, and things get more interesting. The “@snapshot_slug” is a field name referring to a custom indexed field.

By default, you can’t query based on any information contained anywhere in a transaction object. This would make for a super bloated indexing system, so instead, we have a few fields that ARE always indexed.

The fields that are always indexed are:

  1. timestamp
  2. block_id
  3. tag

If we want to query on something in our payloads, we need to create a custom index for those fields. I’ll show you how to do that in a bit.

For now, my redisearchQuery translates to something like snapshot_slug == “dragonchain”

For more on redisearch query syntax, keep this link handy.

Next, let’s skip to a nifty bit that prevents creating the response transaction altogether if it isn’t needed.

Remember the normal transaction flow for manually invoking a smart contract:

  1. You’ll create an “invocation request” transaction that names the smart contract it’s calling and passes any arguments required as the “payload” of the transaction
  2. The smart contract will be looked up by the node, passed the required info, and left to do its thing (your code) and then output any response that’s required.
  3. The smart contract’s output will be used to create a NEW transaction which references the invocation request transaction from step #1 and includes the response as its payload.

It’s that step 3 that we’re addressing. If there’s an error condition (or simply no need to output a result transaction), how do we do that?

With the magic JSON object {“OUTPUT_TO_CHAIN”:false}:

if (lastTransaction.response.results && lastTransaction.response.results.length > 0)
{
    // We got a transaction, so do stuff //
    // ...
} else {
    // No previous transaction was found //
    // Do something neat: log the error AND prevent an extra transaction from getting created for nothing //
    console.error(`No previous transaction found for slug ${inputObj.payload.parameters.currencySlug}`);
    callback(undefined, {"OUTPUT_TO_CHAIN":false});
}

Note that I wrote to console.error INSTEAD of just passing the error message back to the main script because (given how that script is currently written) if I pass an error message, it returns it immediately. This could certainly be handled differently by modifying the main script’s code.

For now, just note that I want to log the error AND make the “payload” for my smart contract response that special OUTPUT_TO_CHAIN:false object. This will prevent it from creating that final response transaction (that would also have a transaction fee, of course). Very handy in certain situations.

And that’s pretty much all there is to say of note about the example contract code! The rest is just basic Javascript/Node.js stuff, so let’s move on to testing, building, and deploying this sucka.

Testing Our Contract Locally

Note that the transaction request payload is going to be sent to our smart contract code via stdin (and that it will then write the response to stdout).

So to test our code, we need to pass in a test payload using stdin. The simplest way I know of so far to do so is with a line like this:

echo '{"payload":{"method": "create_price_snapshot", "parameters":{"currencySlug":"dragonchain"}}}' | node index.js

Note that on the Dragonchain node itself, the full transaction will actually be passed to the smart contract, not JUST the payload. That’s not important in our case, but it’s useful to know if you want to be able to use other information in the transaction object (like the timestamp, tags, etc.) in a more real-world smart contract.

Also remember, as I alluded to earlier, that you won’t be able to pull your api key from the secrets testing locally at all, so be sure you “hard code” your api key (or set it as an environment variable or something if you prefer) to test locally.

Finally, we obviously won’t have a transaction to pull to test the second method until we’ve deployed our contract and run the first method. You could hard code a payload to test with there if you wanted, but just something to be aware of otherwise.

Now let’s get this thing deployed.

Deploying Our Node.js Contract

Building and pushing to Docker Hub is as simple as it was for the last example, but let’s look at a few differences in the Dockerfile (as provided in the contract template):

FROM node:alpine

WORKDIR /home/app

# Get and install dependencies
COPY package.json .
COPY yarn.lock .
RUN yarn --frozen-lockfile --non-interactive --production

# Copy the actual code
COPY . .
RUN chown 1000:1000 -R /home/app

USER 1000:1000

Fairly self explanatory. We pull the node-enabled alpine base image, change our working directory, copy our yarn package specification files and install the necessary packages, then copy our actual code into the working directory, change the owner for those files, and set our user to the proper owner.

Now we’ll just build and push the image as normal so we can have our contract pull it:

docker build -t johnwantsmore/node-example:latest .
docker push johnwantsmore/node-example:latest

Next, let’s create the smart contract (and then talk about a couple of important changes in this contract create command from the shell example):

dctl c c node_example \
johnwantsmore/node-example:latest \
node index.js \
-r MYBASE64USERNAMEPASSWORDFORDOCKERHUB \
-S '{"cmcapikey":"MYAPIKEY"}' \
--customIndexes '[{"fieldName":"snapshot_slug", "path":"snapshot.slug","type":"text","options":{"sortable":true}}]'

Note that our “command to run” is, very simply, node index.js, and that the actual script grabs the payload from stdin magically set by the Dragonchain node itself when the transaction is created.

Next, note that we set our CoinMarketCap apiKey “secret” with the -S flag.

Finally, note how we’re creating our custom index so that we can search based on the currency “slug” that we create as part of our output payloads. Let’s look at it in prettier JSON format:

[
    {
        "fieldName":"snapshot_slug",
        "path":"snapshot.slug",
        "type":"text",
        "options":{"sortable":true}
    }
]

Fairly straightforward. We give our indexed field a name (which is the name I used in my redisearchQuery in the code).

Then we describe the JSON path to the field we want to index starting inside our payload object. Given the following payload that we’re creating:

{
    "snapshot": {
        "slug": "bitcoin",
        "quote": {
            "price": 8373.79039269,
            "volume_24h": 15540586422.6081,
            "percent_change_1h": -0.313873,
            "percent_change_24h": 0.70351,
            "percent_change_7d": 1.19747,
            "market_cap": 150665214295.715,
            "last_updated": "2019-10-15T01:34:32.000Z"
        }
    }
} 

Our JSON path for the slug becomes snapshot.slug (or, alternatively $.snapshot.slug).

To test your potential JSON paths to be sure they work, you can use this tool.

Then the rest of our custom index definition just sets the field type and the sortable option. You can get more information on the custom indexes and types in the Dragonchain documentation here.

Major note on using custom indexes:

You can ONLY create a custom indexed field at the time of smart contract or transaction type creation! You cannot currently add or change indexed fields later.

The only solution right now if you NEED to add more indexed fields is to delete the smart contract (or transaction type) and create it again.

Major note on deleting smart contracts or transaction types:

Once you’ve deleted a transaction type or a smart contract (which is just a transaction type + magic), you can no longer query for past transactions using that type, even if you recreate the transaction type or contract.

Those transactions WILL still exist, but you’ll have to look them up by transaction ID or manually process blocks of transactions to find them.

Okay, now that the scary stuff is out of the way and our contract has been created, let’s play with it!

Invoking Our Smart Contract

In the first example contract, we setup a cron schedule to automatically invoke our contract.

This time, we’ll only ever MANUALLY invoke our contract, and we do that by creating a normal transaction with the transaction type set to our smart contract’s name (node_example in this case) and passing the JSON object with our method and parameters as the payload. That looks like this using DCTL:

dctl t c node_example '{"method":"create_price_snapshot", "parameters":{"currencySlug":"bitcoin"}}'

And the resulting transactions:

{
  "version": "2",
  "dcrn": "Transaction::L1::FullTransaction",
  "header": {
    "txn_type": "node_example",
    "dc_id": "uDVMagYWemvWH281ry7zdX6kap7e3dBZhJjuCbDyh4qU",
    "txn_id": "b0c050dd-7a3a-4dcc-acda-5d468e34ddda",
    "block_id": "27773028",
    "timestamp": "1571103356",
    "tag": "",
    "invoker": ""
  },
  "payload": {
    "method": "create_price_snapshot",
    "parameters": {
      "currencySlug": "bitcoin"
    }
  },
  "proof": {
    "full": "0J0GwJLEF4Ov55qhoiQTVKMl5O8SD93exi3bFgUby+o=",
    "stripped": "MEQCIE5KFnZmjItZZuSHKsrkP+tj/6ReIZMATR/9eetXv6GbAiBfaY8MgrwA+7wWU6RuKjPhUNPL+0wHEMVhUZ8URqsYng=="
  }
}

{
  "version": "2",
  "dcrn": "Transaction::L1::FullTransaction",
  "header": {
    "txn_type": "node_example",
    "dc_id": "uDVMagYWemvWH281ry7zdX6kap7e3dBZhJjuCbDyh4qU",
    "txn_id": "63b34f8a-ec2d-4b07-b731-1851c74a79d4",
    "block_id": "27773028",
    "timestamp": "1571103356",
    "tag": "",
    "invoker": "b0c050dd-7a3a-4dcc-acda-5d468e34ddda"
  },
  "payload": {
    "snapshot": {
      "slug": "bitcoin",
      "quote": {
        "price": 8373.79039269,
        "volume_24h": 15540586422.6081,
        "percent_change_1h": -0.313873,
        "percent_change_24h": 0.70351,
        "percent_change_7d": 1.19747,
        "market_cap": 150665214295.715,
        "last_updated": "2019-10-15T01:34:32.000Z"
      }
    }
  },
  "proof": {
    "full": "BHS7k9SrpqxSExsO9brAYf10MINkSUrpoATx2pi7qFE=",
    "stripped": "MEUCIQCV5EGqYJGWhXNGClzAGI97+94mBLsYT3B4Z45rhvOwewIgRex5rHfryXAQiLbzSgw0H6NAGc+INP5OkMWppVIC5lA="
  }
}

Sweet!

Now let’s try the other method to create a record of the current price difference:

dctl t c node_example '{"method":"record_current_diff", "parameters":{"currencySlug":"bitcoin"}}'

Results in a response of:

{
  "version": "2",
  "dcrn": "Transaction::L1::FullTransaction",
  "header": {
    "txn_type": "node_example",
    "dc_id": "uDVMagYWemvWH281ry7zdX6kap7e3dBZhJjuCbDyh4qU",
    "txn_id": "27f4b97b-d761-4682-ac0d-44a2f1a49a86",
    "block_id": "27773044",
    "timestamp": "1571103434",
    "tag": "",
    "invoker": "edef80e8-eebe-478a-bc53-2c681c016bf6"
  },
  "payload": {
    "diff_snapshot": {
      "slug": "bitcoin",
      "diff": {
        "price_diff": 0.9200491499996133,
        "percent_diff": 0.010987248388767662
      }
    }
  },
  "proof": {
    "full": "D2qE+EJPLl63Shjlob637M5asvX7KkGyfksqc8ua4qE=",
    "stripped": "MEQCIEukfJyHXL1OOE0ZQDgjBLjKuKEp5I5qoFm5zXV5keI9AiAgzIzQHEfJTKhKSoPfnZIwvNy1XTdTAgm0kMtFGciJmQ=="
  }
}

Wicked.

And there you have it! We’ve now explored how to write and deploy a Node.js smart contract, how to invoke it, and how to incorporate features like the Dragonchain SDK to talk to our own node (or even other nodes), how to use contract secrets and custom indexes, and how to control the response transaction output when needed.

That’s going to wrap things up for this first tutorial series looking at how to create and deploy smart contracts on Dragonchain! I hope this series has been helpful.

The next MAJOR project I’m working on is a “real world” project that I’ll be submitting to hopefully collect a Dragonchain bounty (I’ve got a couple of such projects in the works, actually), and I’ll document those processes, as well. Thanks for reading along!

Until next time,
John

Join the Conversation

2 Comments

Leave a comment

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