Creating an Ethereum-enabled command line tool with Truffle 3.0
Truffle 3 is out, and it switched to a less opinionated build process. In Truffle 2, the default app from truffle init
included a frontend example with build process. Now, there's nothing other than a build
folder for your JSON contract artifacts. This opens up the door for testing and building other (cough command line cough) types of applications!
Intended Audience
This is written for those familiar with Truffle and Ethereum, who want to learn how to create a testable, Ethereum-enabled command line application with Truffle.
For this tutorial, we'll be building a command line tool that interacts with the Ethereum Name Service (ENS).
Getting Started
If you were using Truffle beta 3.0.0-9 or below, do not immediately upgrade. Read these release notes and the upgrade guide first. Next, make a new folder and run the following command:
$ truffle init
You should see the test
, build
, and migrations
directories were created for you – but no app
. Additionally, the build step in the truffle.js
file is mysteriously absent. That's ok! This means Truffle is getting out of your way and letting you control the build process.
Let's Look at the Example
Check out the example app as a reference. I'll be referring to it often so give it a look over if you haven't already.
The first thing to look at is the build process. Since Truffle now puts us in control of the build, I've added some custom scripts within package.json
to handle building for us:
...
"description": "CLI for ENS deployment",
"scripts": {
"ens": "babel-node ./bin/ensa.js",
"lint": "eslint ./"
},
"author": "Douglas von Kohorn",
"license": "MIT",
...
I've defined two scripts here: lint
for linting my Javascript to keep my codebase so fresh and so clean, and ens
for transpiling my Javascript command line tool (using Babel) and running it. That's it! That's the build process. You can run it via npm run lint
and npm run ens
. Now that that's out of the way, let's dig into how and why I structured my app this way.
Building our App: Separation of concerns
In order to take advantage of all parts of Truffle while building this tool, we need to separate our code into two distinct pieces. First, we need to write a library which will constitute the bulk of the app, and allow us to perform actions against the ENS contracts quickly and easily from Javascript. Next is the CLI; the CLI will take advantage of the library itself, and will be the user's interface to our application.
The Library
The library is lib/ens_registrar.js. Let's take a look at the constructor:
...
constructor (AuctionRegistrar, Deed, registrarAddress, provider, fromAddress) {
this.web3 = new Web3(provider)
this.Deed = Deed
this.Deed.setProvider(provider)
AuctionRegistrar.setProvider(provider)
AuctionRegistrar.defaults({
from: fromAddress,
gas: 400000
})
this.registrar = AuctionRegistrar.at(registrarAddress)
}
...
The library constructor requires a few things:
- the contract interfaces (
AuctionRegistrar
&Deed
) - the address on the network of the registrar
- a web3 provider that will be used to connect to the desired Ethereum network
- and the account address that will provide gas for transactions.
It's crucial that the library remain ignorant of the creation of these variables if we want to take advantage of both Truffle's testing pipeline and a CLI. As I see it, there are two ways to use the library:
- Through Truffle, which manages contract addresses for testing against different networks (e.g. local, private, ropsten). Truffle makes testing our library easy.
- Once we've convinced ourselves that the library is well tested and works properly, we'll want to tell the library where to find our own contracts, provider, and account on the mainnet through the CLI.
Let's take a look at how the library is used in both cases.
Use #1: Truffle Tests
Here's test/ENS.js, a Truffle test that uses the library:
import { default as ENSAuctionLib } from '../lib/ens_registrar'
const Registrar = artifacts.require('./Registrar.sol')
const Deed = artifacts.require('./Deed.sol')
contract('ENS integration', (accounts) => {
let auctionRegistrar
before('set up auction registrar', (done) => {
Registrar.deployed().then((instance) => {
auctionRegistrar = new ENSAuctionLib(
Registrar,
Deed,
instance.address,
web3.currentProvider,
accounts[0]
)
}).then(() => done())
})
it('demonstrates that the domain name is available', (done) => {
auctionRegistrar.available('test')
.then((isAvailable) => {
assert.isTrue(isAvailable)
done()
})
})
...
}
Truffle injects a global artifacts.require
function, a helper for finding the right compiled contract artifacts within the test environment. The test then finds a deployed instance of the Registrar on the test network via Registrar.deployed()
. Now, with the addition of accounts
, which is passed in via the contract
wrapper (see here), we have enough to instantiate the library and use it to test that the domain name 'test'
is available for auction.
Use #2: the CLI
Here's index.js, which provides our command line tool, using the library:
import { default as ENSAuctionLib } from './lib/ens_registrar'
import { default as Web3 } from 'web3'
import { default as contract } from 'truffle-contract'
const AuctionRegistrar = contract(require('./build/contracts/Registrar.json'))
const Deed = contract(require('./build/contracts/Deed.json'))
export default function (host, port, registrarAddress, fromAddress) {
let provider = new Web3.providers.HttpProvider(`http:\/\/${host}:${port}`)
return new ENSAuctionLib(
AuctionRegistrar,
Deed,
registrarAddress,
provider,
fromAddress
)
}
Here I'm using the same library, truffle-contract
, that artifacts.require
uses under the hood. Because I can't rely on the Truffle framework within the CLI, I have to include the compiled contract artifacts manually. The rest is passed in through the CLI in bin/ensa.js
:
import { default as initializeLib } from '../index'
...
let command = argv._[0]
if (command === 'bid') {
let { name, host, max, port, registrar, account, secret } = argv
let auctionRegistrar = initializeLib(host, port, registrar, account)
auctionRegistrar.createBid(name, account, max, secret)
.then(() => console.log('Created bid for ' + name))
}
Usage
You can use the command line tool against any network that has an ENS registrar deployed. First, choose a command:
$ npm run -s ens
Usage: bin/ensa.js [command] [options]
Commands:
winner Current winner of bid
bid Place a bid on a domain name
reveal Reveal your bid on a domain name
Options:
--help Show help
Then specify the correct options, including the account
and registrar
:
$ npm run -s ens -- winner -n 'NewDomain'
bin/ensa.js winner
Options:
--help Show help [boolean]
--host, -h HTTP host of Ethereum node [default: "testrpc"]
--port, -p HTTP port [default: "8545"]
--registrar, -r The address of the registrar [string] [required]
--name, -n The name you want to register [string] [required]
--account, -a The address to register the domain name [string] [required]
Missing required arguments: account, registrar
Recap
You might be thinking to yourself, "That was a short tutorial". That's because it doesn't need to be much longer: Creating a command line application with Truffle is very similar to creating a web application, but you have to do things a bit differently. For instance, instead of a build process, your command line tool needs to grab your contract artifacts and make them ready for use. Additionally, if you want to take advantage of Truffle's tests, you need to separate your code into a command line interface and a library – a good practice anyway – so you can use Truffle's testing framework to test your code.
Stay Tuned
Truffle 3.0 now makes it easier than ever to write any Ethereum-enabled application, and not just web apps. Stay tuned for more examples in the future where we explore Desktop and Mobile applications, too. Cheers!