Creating Truffle plugins

One of the final Truffle updates of 2018 included the addition of Truffle plugins - the possibility to extend Truffle's functionality with new Truffle CLI commands. While it has been out for over half a year now, there has not been a lot of innovation in the form of new Truffle plugins. In fact, the list of awesome Truffle plugins currently counts just two plugins: truffle-plugin-verify and truffle-security.

Needless to say, it is time for more useful plugins to be developed. But the remaining questions are where do we start, and what even are the possibilities of these Truffle plugins? In this article we discuss how to create a Truffle plugin, and we discuss what these plugins can be used for.

Using Truffle plugins

Before being able to create Truffle plugins we need to understand how to use them and how to integrate one into an existing Truffle project. Luckily this process is very simple, as Truffle plugins are simply NPM packages that serve a very specific function - extending Truffle. After setting up a Truffle project, any plugin can be integrated into this project with a few simple steps.

First, all Truffle plugins can be installed into a project with NPM.

npm install truffle-plugin-verify

Next, these plugins need to be enabled by adding them to your truffle-config.js or truffle.js file under the plugins field.

module.exports = {
  /* ... rest of truffle-config */

  plugins: [
    'truffle-plugin-verify'
  ]
}

Any plugin should be usable with just these two steps, but some plugins might require additional steps, such as adding API keys or other additional parameters. So always be sure to check the README of a specific plugin.

module.exports = {
  /* ... rest of truffle-config */

  api_keys: {
    etherscan: 'MY_API_KEY'
  }
}

When a plugin is fully installed and enabled, it can be used from the Truffle CLI.

truffle run verify Casino --network rinkeby

How do Truffle plugins work?

All Truffle plugins are structured in the same way. They include a truffle-plugin.json file that maps one or more CLI commands to their corresponding JS files.

{
    "commands": {
        "hello": "hello.js"
    }
}

These JS files then have a single function as their default export. This function has a config object as a parameter that is used inside the function to implement the plugin's functionality.

module.exports = async (config) => {
  if (config.help) {
    console.log(`Usage: truffle run hello [name]`);
    done(null, [], []);    
    return;
  }

  let name = config._.length > 1 ? config._[1] : 'World!';
  console.log(`Hello, ${name}`);
}

The config object

The config object contains all the information you will ever need to start creating useful Truffle CLI plugins. The most important fields in this object are outlined below.

Config {
  // Information about the CLI
  truffle_directory: string, // Truffle executable location
  working_directory: string, // CWD
  _: string[], // Raw positional command line arguments

  // Any additional command line flags / arguments
  myString: string // --myString hello
  myint: number // --myInt 5

  // Information included in the truffle-config.js file
  networks: { rinkeby: object, ropsten: object },
  compilers: { solc: object, vyper: {} },
  plugins: string[],
  api_keys: { etherscan: string },
  /* ... any other fields defined in truffle-config.js */

  // Network config
  network: string, // --network rinkeby|ropsten|etc
  network_id: number // Current network id
  network_config: object // Current network config (from truffle-config.js)

  // Directory information
  contracts_directory: string, // ./contracts
  build_directory: string, // ./build
  contracts_build_directory: string, // ./build/contracts
  migrations_directory: string, // ./migrations
  migrations_file_extension_regexp: RegExp,
  test_directory: string, // ./test
  test_file_extension_regexp: RegExp,
}

The first thing we see in this config object is information about the CLI, including working directory and command line arguments. The object also includes any passed in command line flags, which gives the truffle plugin any functionality you would expect from other CLI-based tools.

The next thing that's included is all the information included in your truffle-config.js or truffle.js file. This includes things like network configuration and compiler settings, but also any other user-defined fields, such as the api_keys field that is used in truffle-plugin-verify. Besides the raw information, there's some additional Truffle magic included in the additional network information that is added based on the --network parameter passed into the CLI.

The final information in the config object is directory information. This is what really enables interesting functionality. It gives access to all migrations files and test files, which allows you to run these migrations or tests inside your plugin, or to inspect their source code. But what really opens doors are the contracts_directory and contracts_build_directory strings.

Retrieving contract information & using contracts

The contracts_directory field gives us access to all the contract source files. This can be used for things like code flattening, linting, compiling, static analysis, or any other kind of source code analysis. But for most use cases, this contracts_directory field is not even necessary, as the contracts_build_directory grants access to all Truffle artifacts produced by Truffle's compile process. These artifacts include the contract's source paths, pointing to contract's source files as well.

The Truffle artifacts grant access to much more though, as they include all the information about the contracts, including bytecode, ABI, source code, and even the contract's Abstract Syntax Tree (AST), allowing you to add some compiler-related operations to your Truffle plugins.

Furthermore, artifacts include the addresses of contract instances that have been deployed using Truffle's migrate functionality. This allows you to analyse these deployed instances by looking at their transaction history, emitted events, or gather information on these contracts through public APIs such as Etherscan's.

But it doesn't stop there, since the artifact also includes the contract's ABI, allowing the plugin to send transactions or call functions on this contract. And because the config object also includes the network configuration, the plugin can even use the contract on behalf of the user's account when they are using something like truffle-hdwallet-provider or truffle-privatekey-provider.

Creating a Truffle plugin

Now that we know how a Truffle plugin works technically, what information it gets, and what can be derived from this information, it is time to develop a useful Truffle plugin. In this article we walk through the creation of a simple, but not trivial Truffle plugin that can have production value.

truffle-plugin-store

The contract we will create is called truffle-plugin-store. It reads the the artifact file of a passed contract name, and sends this artifact to a storage server. We then query the storage server to get the number of stored artifacts for the specific contract name.

For the purpose of this tutorial, I have created a repository with boilerplate code that includes a simple storage server, a basic Truffle project, and the boilerplate code for a Truffle plugin. This repository can be found on GitHub. The repository's master branch contains only this boilerplate code, while the reference branch contains the fully implemented plugin.

Prerequisites

Before starting, you need to have Node.js, Truffle, and MongoDB installed locally.

  1. Install Node.js.
  2. Install Truffle
    npm install -g truffle
    
  3. Install MongoDB

Setting up the repository

After cloning the repository, there are three components that need to be set up. The first is the artifact storage server, which needs to be running in a separate terminal window during the rest of the development process.

cd artifact-storage-server
npm install
npm start

The second component is truffle-plugin-store which contains the boilerplate code for a Truffle plugin. The third component is the simplestorage Truffle project that contains a very simple smart contract, and already has truffle-plugin-store included as a dependency. Its dependencies need to be installed, and the contracts need to be compiled.

cd simplestorage
npm install
truffle compile
truffle run store SimpleStorage

In case of the boilerplate code, this simply outputs 'hello' to the console.

Implementing truffle-plugin-store

The boilerplate code contains just the following code inside the store.js file.

module.exports = async (config) => {
  console.log('hello')
}

The first steps to actually implementing this plugin is figuring out the data we need to receive from the config object. In this case we want to retrieve the contract name, which is a positional argument. Then we want to retrieve this contract's artifact by reading the correct JSON file.

module.exports = async (config) => {
  const contractName = config._[1]
  const contractsBuildDir = config.contracts_build_directory
  const artifactPath = `${contractsBuildDir}/${contractName}.json`
  const artifact = require(artifactPath)
}

From here on we need to communicate with the artifact storage server. This server has two different endpoints: POST /artifacts which uploads a JSON artifact to the server and GET /artifacts/:contractName which retrieves the artifact count for a specified contract name. To communicate with the server we use the 'axios' library.

npm install axios
const axios = require('axios')

module.exports = async (config) => {
  const contractName = config._[1]
  const contractsBuildDir = config.contracts_build_directory
  const artifactPath = `${contractsBuildDir}/${contractName}.json`
  const artifact = require(artifactPath)
  
  const url = 'http://localhost:3000/artifacts'
  
  const { data } = await axios.post(url, { artifact })
  console.log(`Stored ${contractName} artifact with id ${data.insertedId}`)
  
  const { data: count } = await axios.get(`${url}/${contractName}`)
  console.log(`${count} ${contractName} artifacts stored in total`)
}

With these axios calls implemented the plugin now stores the compiled artifacts every time truffle run store SimpleStorage is run, as well as outputting the number of stored artifacts. This concludes the plugins functionality, but there are improvements that can be made to this code, such as an optional URL parameter --url that allows people to use any storage server. A proper plugin also contains extensive error handling, so that users understand when they misuse the plugins.

Conclusion

Truffle plugins can be used to add new functionality or commands to the Truffle CLI. The Truffle CLI provides the plugin with a config object with different parameters that can be used by these Truffle plugins. The truffle-plugin-store example shows the steps required to create a minimal but non-trivial plugin. With this you should be all set to start creating more complex plugins for your own use cases.


If you used this guide to create an awesome Truffle plugin of your own, add a PR to the awesome-truffle-plugins and let me know about it in the comments below. If you know 10x developers that want to get started with Truffle plugins, don't forget to share this with them on Facebook, Twitter, and LinkedIn.