Blog.



Extra contracts in Truffle tests

(August 26, 2020)

Sometimes you just need a Truffle contract object. If you're developing a full-blown Truffle project, this is easy for your "core" contracts, the ones you keep in your contracts/ directory. Just artifacts.require and call it a day. But what if you want to interact with a different contract?

I ran into this while working on meta-tooling around smart contracts, specifically scripts for running upgrades of proxied contracts. To this end, I wanted to write unit tests that would use minimal example contracts with edge cases that might not easily appear "in nature". I want to keep these dummy contracts in a test resources directory rather than cluttering up the main contracts/.

Unfortunately, not all parts of the Truffle API are very well documented, but here's how I got my sweet, sweet Truffle contract wrappers.

Getting the contract object

import truffleContract = require('truffle-contract')
const Contract = truffleContract({
    abi: artifact.abi,
    unlinked_binary: artifact.bytecode,
})
where artifact is a standard solc build artifact, say parsed from a JSON file that truffle compile outputs. The thing to note is that the bytecode is under an unlinked_binary property.

Making it usable

Before you go run off playing with your new Contract, a few last housekeeping things to take care of. If you try, say, deploying this contract with Contract.new(), you'll get a nasty error message saying

Error: Contract error: Please call setProvider() first before calling new().

Actually not that nasty since it tells you what you need to do to fix things! Call Contract.setProvider() with a standard Web3 provider. If you already have a Web3 instance lying around,

Contract.setProvider(web3.currentProvider)
should do.

Last things last, at this point you would start seeing the following:

Error: Contract has no network id set, cannot lookup artifact data. Either set
the network manually using Contract.setNetwork(), run Contract.detectNetwork(),
or use new(), at() or deployed() as a thenable which will detect the network
automatically.
I resolved this by calling Contract.setNetwork('development'), given that this was in a unit tests context and would always execute against the 'development' Truffle network. You might need to pass in a different network name, or feel free to look into one of the other solutions suggested by the above error message.

At this point you'll be able to call Contract.new() to get a handle to a freshly deployed contract, or Contract.at(address) to interact with an already deployed instance of the contract.

TL;DR

Here's the little helper function I wrote to construct these Truffle objects:

import truffleContract = require('truffle-contract')

const makeTruffleContract = (artifact) => {
  const Contract = truffleContract({
    abi: artifact.abi,
    unlinked_binary: artifact.bytecode,
  })
  Contract.setProvider(web3.currentProvider)
  Contract.setNetwork('development')

  return Contract
}

Final note on linking

If you're using any linked libraries in your contract, first deploy the libraries, then link them with Contract.link('LibraryName', libraryAddress).

If you have any questions or comments about this post or site in general, feel free to email me.