truffle-config.js
contracts/Migrations.sol
(can be ignored)migrations/1_initial_migration.js
(can be ignored)test/.gitkeep
(can be ignored)@truffle/hdwallet-provider
mocha
and chai
ganache-cli
@openzeppelin
contracts
, test-environment
and test-helpers
truffle-config.js
mocha
:This article is the first in a series of articles that describe, in exacting detail, how I built a series of solidity contracts. I’m not going to go into a lot of theory, and I’m not going to try to sell you on how awesome ethereum, bitcoin, blockchain, etc are. There are plenty of other articles that do that. This series of articles will be a straightforward description of what was built, how it works, and why I built it the way I did.
One thing to understand — writing solidity contracts is very different from writing other software for two important reasons:
TLDR: Testing your contract is essential. Test Driven Development is the way to go here, and this series of articles will use that approach.
In this article we’ll concentrate on building a fully tested NFT contract using OpenZeppelin. It’ll be quite easy because OpenZeppelin has already written all of the code and tests for the contract, so we’ll simply use that code and those tests. We’ll take some time to explore some of the OpenZeppelin code along the way.
You’ll find several files in the top level directory of the GitHub repositiory that are part of my development environment. I describe them here for completeness.
I use nix to manage the packages on my development computer. This nix expression makes node version 16 available to the project.
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = [ pkgs.nodejs-16_x ];
}
There are a series of shell scripts in this directory that are added to my path when I cd
into the directory. They’re there to help me rember how to do specific commands that I need to do. There is also a help
script that prints out a simple description of what each script in devbin
does.
Any log files that I may want review go here. For this project, this is mainly the ganache-cli
log.
I use direnv to set up the specific environment necessary to work on this project when I cd
into it.
use_nix ①
PATH_add devbin ②
export NFTCAR_DIR=$PWD ③
use-nix
is a direnv
directive that tells direnv
to invoke the shell.nix
expression.devbin
path to my path.I use vscode as my IDE. The .vscode
directory contains all of the configuration files for the IDE.
As I mentioned in the previous section, I use nix
to curate my development environment for this project. You are free to use anything you like. npm -g
or nvm
are both fine choices. I’ll assume you have node
version 16 available to you on the command line along with npm
and npx
.
You can probably use any version of node beyond 8.9.4. The truffle installation documentation calls out this requirement specifically.
npm init -y # ①
cat >> .gitignore # ②
node_modules/
build/
^d ③
npm install --save-dev truffle # ④
.gitignore
to so that we don’t push compile results or node_modules
to our git repository.cat
command before typing the next command.npx truffle init
This command initializes a new and empty ethereum project in the local directory. It creates a series of files that we’ll examine below.
truffle-config.js
This is the most important file that was created by npx truffle init
. It’s heavily commented, but the uncommented version is shown below so you can get a sense of what you’re dealing with.
module.exports = {①
networks: { ②
},
mocha: {③
},
compilers: {④
solc: {
}
},
db: {
enabled: false ⑤
}
};
networks
, mocha
, compilers
, and db
.mocha
testing library for testing Ethereum contracts using truffle. You can set default mocha
options here. A good one to set is the timeout
option as shown in the example file (just uncomment it). Without it any breakpoints in your test code will fail your test due to a timeout.compilers
section is where you configure your compiler. We’ll get back to this in a minute.contracts/Migrations.sol
(can be ignored)This is a solidity contract that truffle uses as part of the process of migrating (think installing) your contract to the Ethereum block chain. I’ve never modified this file.
migrations/1_initial_migration.js
(can be ignored)This is a javascript file that truffle also uses as part of the migration process. I’ve never modified this file either.
test/.gitkeep
(can be ignored)This file just exists so that the test
directory, which would otherwise be empty, is stored in your git repository. It never changes and can be deleted once you have tests in the test
directory.
Modify the truffle-config.js
file as follows. If you’re not used to reading the output of the diff
command, this snippet is essentially saying to change the 0.5.1
to 0.8.6
in the compilers.solc
object. Solidity 0.8.6 is the latest version as of this writing.
diff --git a/truffle-config.js b/truffle-config.js
index a707377..8e8d0b4 100644
--- a/truffle-config.js
+++ b/truffle-config.js
@@ -82,7 +82,7 @@ module.exports = {
// Configure your compilers
compilers: {
solc: {
- // version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version)
+ version: "0.8.6", // Fetch exact version from solc-bin (default: truffle's version)
// docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
// settings: { // See the solidity docs for advice about optimization and evmVersion
// optimizer: {
Note that you can use the patch
command to make these changes. Copy the text above to your clipboard, then type patch -p1
at the command line. Paste the lines into your terminal then hit [ctrl][d]. The changes should be made without using an editor. If you’ve already heavily modified the truffle-config.js
file, this won’t work.
There are several more NPM modules we need to install. We install them below.
@truffle/hdwallet-provider
npm install --save-dev @truffle/hdwallet-provider
Truffle’s hdwallet-provider is a Web3 provider used to signb transactions for addresses derived from 12 or 24 word mnemonics.
mocha
and chai
npm install --save-dev mocha chai
Mocha is a JavaScript test framework for Node. Chai is a BDD / TDD assertion library.
ganache-cli
npm install --save-dev ganache-cli
ganache-cli is part of the Truffle suite. It is the command-line version of Ganache and provides a test blockchain on your local machine.
@openzeppelin
contracts
, test-environment
and test-helpers
npm install --save-dev @openzeppelin/contracts @openzeppelin/test-environment @openzeppelin/test-helpers
truffle-config.js
diff --git a/truffle-config.js b/truffle-config.js
index 8e8d0b4..d4666dc 100644
--- a/truffle-config.js
+++ b/truffle-config.js
@@ -42,11 +42,11 @@ module.exports = {
// tab if you use this network and you must also set the `host`, `port` and `n
// options below to some value.
//
- // development: {
- // host: "127.0.0.1", // Localhost (default: none)
- // port: 8545, // Standard Ethereum port (default: none)
- // network_id: "*", // Any network (default: none)
- // },
+ development: {
+ host: "127.0.0.1", // Localhost (default: none)
+ port: 8545, // Standard Ethereum port (default: none)
+ network_id: "*", // Any network (default: none)
+ },
// Another network with more advanced options...
// advanced: {
// port: 8777, // Custom port
The ganache-cli
environment will allow us to run automated tests on our own machine very quickly. Much faster than it would take to run them against mainnet or any of the testnets. With ganache, we also do not need any real or test ethereum.
mocha
:diff --git a/package.json b/package.json
index ac7b362..87dee76 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "truffle compile && mocha --exit --recursive"
},
"keywords": [],
"author": "",
This change will allow us to run truffle test
on the command line.
If you’ve made it this far, you have a perfect starting point for any new solidity project you want! The only thing missing is an actual contract and some tests. That’s what the next section is all about!
OpenTruffle has done most of the work that needs to be done for our contract, and we’re going to use that great work to our advantage. In the node_modules
folder of your project you’ll find several ethereum contracts. The one we’re looking for is:
node_modules
@openzeppelin
contracts
token
ERC721
presets
It’s well worth studying this contract and all of the contracts imported by it.
Our simple contract imports the ERC721PresetMinterPauserAutoId contract and has enough boilerplate to make it work. 100% of our ERC721 contract is provided the OpenZeppelin preset.contracts/OpenZeppelinNft.sol
// SPDX-License-Identifier: MIT ①
pragma solidity ^0.8.0; ②
import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; ③
contract OpenZeppelinNft is ERC721PresetMinterPauserAutoId { ④
constructor(string memory name, string memory symbol, string memory baseTokenURI) ⑤
ERC721PresetMinterPauserAutoId(name, symbol, baseTokenURI) ⑥
{
}
}
note: don’t forget to remove the circled numbers (①, etc) or your contract won’t compile.
pragma solidity
tells the build system which version of the compiler to use. Read the doc for detailed information.OpenZeppelinNft
. Its single parent (inheritance) contract is ERC721PresetMinterPauserAutoId.OpenZeppelinNft
contract has a constructor
that takes the same arguments as its parent contract.OpenZeppelinNft
simply passes the arguments to its parent.In the previous section we wrote a fully functional ERC721 contract by extending a Preset
. The OpenZeppelin team has already written tests for this contract and all of its consituant pieces. Using the table below, you can review some of the components of the contract and their matching tests. After that, we’ll build a test of our own based on one of the existant tests.
The first first thing we’re going to do is make a slightly modified version of the source of the ERC721PersetMinterPauserAutioId.test.js
file from github, and use it as the basis of our new OpenZeppelinNft.test.js
file.
There are a couple of things to notice about this:
SupportedInterfaces
. Those lines are not usefull for our purposes and would make this process more complicated.ERC721PresetMinterPauserAutoId
to our contract name, OpenZeppelinNft
.We use ganache-cli as our local ethereum test network. We’ve already configured it in truffle-config.js
, so now it’s time to start it.
I like to be able to review ganache-cli
logs, so I write them to a file in the devlogs
folder.
mkdir devlogs
npx ganache-cli --deterministic 2>&1 > devlogs/ganache-cli.log &
We can actually run our tests now, and if everything works well the solidity contracts will compile and the tests will run.
% npx truffle test # ①
② Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/OpenZeppelinNft.sol
> Compiling @openzeppelin/contracts/access/AccessControl.sol
> Compiling @openzeppelin/contracts/access/AccessControlEnumerable.sol
> Compiling @openzeppelin/contracts/security/Pausable.sol
> Compiling @openzeppelin/contracts/token/ERC721/ERC721.sol
> Compiling @openzeppelin/contracts/token/ERC721/IERC721.sol
> Compiling @openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
> Compiling @openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol
> Compiling @openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol
> Compiling @openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol
> Compiling @openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol
> Compiling @openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol
> Compiling @openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol
> Compiling @openzeppelin/contracts/utils/Address.sol
> Compiling @openzeppelin/contracts/utils/Context.sol
> Compiling @openzeppelin/contracts/utils/Counters.sol
> Compiling @openzeppelin/contracts/utils/Strings.sol
> Compiling @openzeppelin/contracts/utils/introspection/ERC165.sol
> Compiling @openzeppelin/contracts/utils/introspection/IERC165.sol
> Compiling @openzeppelin/contracts/utils/structs/EnumerableSet.sol
> Artifacts written to /tmp/test--65532-dHZ8ChnGRkd7
> Compiled successfully using:
- solc: 0.8.6+commit.11564f7e.Emscripten.clang
③ Contract: OpenZeppelinNft
✓ token has correct name
✓ token has correct symbol (51ms)
✓ deployer has the default admin role (71ms)
✓ deployer has the minter role (56ms)
✓ minter role admin is the default admin (40ms)
minting
✓ deployer can mint tokens (176ms)
✓ other accounts cannot mint tokens (334ms)
pausing
✓ deployer can pause (191ms)
✓ deployer can unpause (170ms)
✓ cannot mint while paused (207ms)
✓ other accounts cannot pause (126ms)
✓ other accounts cannot unpause (138ms)
burning
✓ holders can burn their tokens (235ms)
13 passing (4s)
npx
to run truffle. If you have truffle installed globally, you will run truffle test
.I hope you found this article useful! It lays out a lot of groundwork for future articles that dive into the guts of solidity, testing with mocha and building a UI for our contract. The OpenZeppelinNft
contract we built in this article leverages the OpenZeppelin
community’s contracts for 100% of its functionality, but in the next article we’ll write some code of our own!
If you found this article useful I’d love to hear from you! Drop me an email at: john.lombardo@gmail.com
Originally posted on GitHub