Smart Contract Development with Truffle: From a Local Development Node to Moonbeam MainNet¶
January 10, 2023 | by Erin Shaben
Introduction¶
For this tutorial, we'll be going through the smart contract development life cycle with Truffle. As we're starting to develop our contracts, we'll use a Moonbeam Development Node so we can quickly iterate on our code as we build it and test it. Then we'll progress to using the Moonbase Alpha TestNet so we can test our contracts on a live network with tokens that do not hold any real value, so we don't have to worry about paying for any mistakes. Finally, once we feel confident in our code, we'll deploy our contracts to Moonbeam MainNet.
For the purposes of this tutorial, we'll create a simple NFT marketplace to list and sell a NFT collection that we'll call Dizzy Dragons. We'll create two contracts in our Truffle project: the NFT marketplace contract where we'll list the Dizzy Dragon NFTs and a Dizzy Dragons contract that we'll use to mint the NFTs. Then we'll use Truffle's built-in testing features to test our contracts and ensure they work as expected before deploying them to each network. Please note that the contracts we'll be creating today are for educational purposes and should not be used in a production environment.
Checking Prerequisites¶
For this tutorial, you'll need the following:
- Have Docker installed
- An account funded with DEV tokens to be used on the Moonbase Alpha TestNet and GLMR tokens to be used on Moonbeam MainNet. You can get DEV tokens for testing on Moonbase Alpha once every 24 hours from the Moonbase Alpha Faucet
- Your own endpoint and API key for Moonbeam, which you can get from one of the supported Endpoint Providers
- To generate a Moonscan API key that will be used to verify our contracts
Create a Truffle Project¶
To quickly get started with Truffle, we're going to use the Moonbeam Truffle Box, which provides a boilerplate setup for developing and deploying smart contracts on Moonbeam.
The Moonbeam Truffle Box comes pre-configured for a local Moonbeam development node and Moonbase Alpha. We'll need to add support for Moonbeam so when we're ready to deploy our contracts to MainNet, we'll be all set!
It also comes with a couple of plugins: the Moonbeam Truffle plugin and the Truffle verify plugin. The Moonbeam Truffle plugin will help us quickly get started with a local Moonbeam development node. The Truffle verify plugin will allow us to verify our smart contracts directly from within our Truffle project. We'll just need to configure a Moonscan API key to be able to use the Truffle verify plugin!
Note
If you haven't done so already, you can follow the instructions to generate a Moonscan API key. Your Moonbeam Moonscan API key will also work on Moonbase Alpha, but if you want to deploy to Moonriver, you will need a Moonriver Moonscan API key.
Without further ado, let's create our project:
-
You can either install Truffle globally or clone the Moonbeam Truffle Box repository:
npm install -g truffle mkdir moonbeam-truffle-box && cd moonbeam-truffle-box truffle unbox PureStake/moonbeam-truffle-box
To avoid globally installing Truffle, you can run the following command and then access the Truffle commands by using
npx truffle <command>
:git clone https://github.com/PureStake/moonbeam-truffle-box cd moonbeam-truffle-box
-
Install the dependencies that come with the Moonbeam Truffle Box:
npm install
-
Open the
truffle-config.js
file, where you'll find the network configurations for a local development node and Moonbase Alpha. You'll need to add the Moonbeam configurations and your Moonscan API key here:... networks: { ... moonbeam: { provider: () => { ... return new HDWalletProvider( 'PRIVATE_KEY_HERE', // Insert your private key here 'INSERT_RPC_API_ENDPOINT' // Insert your RPC URL here ) }, network_id: 1284 // (hex: 0x504), }, }, ... api_keys: { moonscan: 'MOONSCAN_API_KEY_HERE' }, ...
Note
With the release of Solidity v0.8.20, support for the Shanghai hard fork has been introduced, which includes
PUSH0
opcodes in the generated bytecode. Support for thePUSH0
opcode on Moonbeam hasn't been rolled out yet. As such, if you'd like to use Solidity v0.8.20, you'll need to update thecompilers
config to use the London compiler:compilers: { solc: { version: '^0.8.0', settings: { evmVersion: 'london', }, }, },
If you attempt to use the default compiler of Solidity v0.8.20, you will see the following error:
"Migrations" -- evm error: InvalidCode(Opcode(95)).
After you edit your config, you may need to force a compile by running:
truffle compile --all
Now we should have a Truffle project that is configured for each of the networks we'll be deploying smart contracts to in this guide.
For the sake of this guide, we can remove the MyToken.sol
contract and the associated tests that come with the project:
rm contracts/MyToken.sol test/test_MyToken.js
Contract Setup¶
The contracts in the following sections import contracts from OpenZeppelin. If you followed the steps in the Create a Truffle Project section, the Moonbeam Truffle box comes with the openzeppelin/contracts
dependency already installed. If you created your project a different way, you'll need to install the dependency yourself. You can do so using the following command:
npm i @openzeppelin/contracts
Add Simple NFT Marketplace Contract¶
As the goal is to go over the development life cycle, let's start off with a simple NFT marketplace contract with minimal functionality. We'll create this marketplace specifically for our new Dizzy Dragons NFT collection.
The marketplace contract will have two functions that allow a Dizzy Dragon NFT to be listed and purchased: listNft
and purchaseNft
. Ideally, an NFT marketplace would have additional functionality such as the ability to fetch a listing, update or cancel listings, and more. However, this contract and our Dizzy Dragon NFT collection is just for demonstration purposes.
We'll add our contract to the contracts
directory:
touch contracts/NftMarketplace.sol
In the NftMarketplace.sol
file, we'll add the following example contract:
// Inspired by https://github.com/PatrickAlphaC/hardhat-nft-marketplace-fcc/blob/main/contracts/NftMarketplace.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
error NotOwner();
error PriceMustBeAboveZero();
error NotApprovedForMarketplace();
error PriceNotMet(address nftAddress, uint256 tokenId, uint256 price);
contract NftMarketplace is ReentrancyGuard {
struct Listing {
uint256 price;
address seller;
}
// Map the NFT address to the listing information
mapping(address => mapping(uint256 => Listing)) private s_listings;
event NftListed(
address indexed seller,
address indexed nftAddress,
uint256 indexed tokenId,
uint256 price
);
event NftPurchased(
address indexed buyer,
address indexed nftAddress,
uint256 indexed tokenId,
uint256 price
);
modifier isOwner(
address nftAddress,
uint256 tokenId,
address spender
) {
// Ensure that only the owner of an NFT can list it
IERC721 nft = IERC721(nftAddress);
address owner = nft.ownerOf(tokenId);
if (spender != owner) {
revert NotOwner();
}
_;
}
function listNft(
address nftAddress,
uint256 tokenId,
uint256 price
)
external
isOwner(nftAddress, tokenId, msg.sender)
{
if (price <= 0) {
revert PriceMustBeAboveZero();
}
IERC721 nft = IERC721(nftAddress);
// Make sure that the marketplace has been approved to transfer the NFT
if (nft.getApproved(tokenId) != address(this)) {
revert NotApprovedForMarketplace();
}
// Save the NFT to state
s_listings[nftAddress][tokenId] = Listing(price, msg.sender);
emit NftListed(msg.sender, nftAddress, tokenId, price);
}
function purchaseNft(address nftAddress, uint256 tokenId)
external
payable
nonReentrant
{
Listing memory listedItem = s_listings[nftAddress][tokenId];
// Make sure the payment received is not less than the listing price
if (msg.value < listedItem.price) {
revert PriceNotMet(nftAddress, tokenId, listedItem.price);
}
// Remove the NFT from state
delete (s_listings[nftAddress][tokenId]);
// Transfer the NFT to the buyer
IERC721(nftAddress).safeTransferFrom(listedItem.seller, msg.sender, tokenId);
emit NftPurchased(msg.sender, nftAddress, tokenId, listedItem.price);
// Send the payment to the seller
(bool success, ) = payable(listedItem.seller).call{value: msg.value}("");
require(success, "Transfer failed");
}
}
Challenge
Try to create a getListing
function that, given the address of an NFT and its token ID, returns the listing.
Add NFT Contract¶
In order to test our NFT Marketplace contract, we'll need to mint a Dizzy Dragon NFT. To do so, we'll create a simple NFT contract named DizzyDragons.sol
:
touch contracts/DizzyDragons.sol
In the DizzyDragons.sol
file, we'll add the following example contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract DizzyDragons is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
address nftMarketplace;
event NftMinted(uint256);
constructor(address _nftMarketplace) ERC721("DizzyDragons", "DDRGN") {
nftMarketplace = _nftMarketplace;
}
function mint(string memory _tokenURI) public {
// Increase the token ID counter
_tokenIds.increment();
// Save the token ID to be used to safely mint a new NFT
uint256 newTokenId = _tokenIds.current();
_safeMint(msg.sender, newTokenId);
// Set the token URI metadata for the NFT
_setTokenURI(newTokenId, _tokenURI);
// Approve the NFT marketplace to transfer the NFT
approve(nftMarketplace, newTokenId);
emit NftMinted(newTokenId);
}
}
Compile the Contracts¶
Now that we have created our contracts, we can go ahead and compile them, which will automatically generate artifacts for each contract. We'll use the artifacts later on when we're writing our tests and deploying our contracts.
To compile our contracts, we can run the following command:
npx truffle compile
The artifacts will be written to the build/contracts
directory.
Setup Deployment Script¶
Let's update the deployment migratio script so that later on we can jump straight into deploying our contracts. We'll deploy the NftMarketplace
contract followed by the DizzyDragons
contract, as we'll need to pass in the address of the marketplace to the constructor of the DizzyDragons
contract.
To update the deployment script, open up the migrations/2_deploy_contracts.js
migration file and replace it with the following:
var NftMarketplace = artifacts.require('NftMarketplace');
var DizzyDragons = artifacts.require('DizzyDragons');
module.exports = async function (deployer) {
// Deploy the NFT Marketplace
await deployer.deploy(NftMarketplace);
const nftMarketplace = await NftMarketplace.deployed();
// Deploy the Dizzy Dragons contract
await deployer.deploy(DizzyDragons, nftMarketplace.address);
};
Start up the Development Node¶
Before we jump into writing our tests, let's take some time now to start up our develpoment node so that we can run our tests against it.
Since the Moonbeam Truffle box comes with the Moonbeam Truffle plugin, starting up a development node is a breeze. All you need is to have Docker installed. If you are all set with Docker, you need to fetch the latest Moonbeam Docker image by running:
npx truffle run moonbeam install
Then you can start the node:
npx truffle run moonbeam start
Once your node has been successfully started, you should see the following output in your terminal:
You can check out all of the available commands in the Using the Moonbeam Truffle Plugin to Run a Node section of our Truffle docs.
You can also set up a development node without the Moonbeam Truffle plugin, to do so, please refer to the Getting Started with a Moonbeam Development Node guide.
Write Tests¶
Before sending our code out into the wild, we'll want to test our smart contracts to ensure they function as expected. Truffle provides the option of writing tests in JavaScript, TypeScript, or Solidity. It also comes with out-of-the-box support for Mocha and Chai.
For this guide, we'll write our tests in JavaScript so we can take advantage of the built-in support for Mocha and Chai.
If you're familiar with Mocha, you're probably used to using the describe
function to group tests and the it
function for each individual test. When writing tests with Truffle, you'll replace describe
with contract
. The contract
function is exactly like describe
, but it includes additional functionality that will re-deploy your migrations at the beginning of every test file, providing a clean-room environment. You'll still use the it
function like you normally would for the individual tests.
Truffle also makes testing easier by including a web3
instance in each test file that is configured for the correct network, so you don't have to configure anything yourself. You'll simply run npx truffle test --network <network-name>
.
Test Setup¶
To get started with our tests, we can add our test file, which will start with test_
to indicate that it's a test file:
touch test/test_NftMarketplace.js
Now that we can set up our test file, let's take a minute to review what we'll need to do next:
- Import the artifacts for the
NftMarketplace
andDizzyDragons
contracts using Truffle'sartifacts.require()
, which provides an abstraction instance of a contract - Create a
contract
function to group our tests. Thecontract
function will also provide us with our account we have setup in ourtruffle-config.js
file. As we used the Moonbeam Truffle box, our development account has been set up for us. When we move on to deploy and test our contracts on Moonbase Alpha and Moonbeam, we'll need to configure our accounts - For each test, we're going to need to deploy our contracts and mint an NFT. To do this, we can take advantage of the
beforeEach
hook provided by Mocha - As we'll be minting an NFT for each test, we'll need to have a
tokenUri
. ThetokenUri
that we'll use for our examples will be for Daizy, our first Dizzy Dragon NFT. ThetokenUri
will be set to'https://gateway.pinata.cloud/ipfs/QmTCib5LvSrb7sshLhLvzmV7wdSdmSt3yjB4dqQaA58Td9'
, which was created specifically for this tutorial and is for educational purposes only
You can enter the tokenUri
into your web browser to view the metadata for Daizy and the image for Daizy has also been pinned so you can easily see what Daizy looks like!
So, now that we have a game plan, let's implement it! In the test_NftMarketplace.js
file, we can add the following code:
const NftMarketplace = artifacts.require('NftMarketplace');
const DizzyDragons = artifacts.require('DizzyDragons');
contract('NftMarketplace', (accounts) => {
const tokenUri =
'https://gateway.pinata.cloud/ipfs/QmTCib5LvSrb7sshLhLvzmV7wdSdmSt3yjB4dqQaA58Td9';
let nftMarketplace;
let dizzyDragonsNft;
let mintedNft;
beforeEach(async () => {
// Deploy the marketplace
nftMarketplace = await NftMarketplace.deployed();
// Deploy a Dizzy Dragon NFT
dizzyDragonsNft = await DizzyDragons.deployed();
// Mint Daizy the Dizzy Dragon NFT
mintedNft = await dizzyDragonsNft.mint(tokenUri);
});
// TODO: Add tests here
});
Note
You don't have to import Mocha or Web3 as both packages comes out-of-the-box with Truffle!
Now we're ready to start writing our tests!
Test Minting NFTs¶
For our first test, let's make sure that we are minting our new Dizzy Dragon NFT as expected. We'll use the event logs from the transaction to ensure that the NftMinted
event of the DizzyDragons
contract has been emitted. The event logs will return the arguments passed to the NftMinted
event, which will be the token ID:
event NftMinted(uint256);
The JavaScript event logs for the minting transaction will resemble the following:
{
...
event: 'NftMinted',
args: {
'0': BN // Token ID
}
}
We'll need to convert the BN to a number using toNumber()
and then we can also test that the token ID of the NFT is 1
. Since we are deploying the DizzyDragons
contract fresh before each test, it should always be the first NFT minted.
In place of the // TODO: Add tests here
comment, you can add the following test:
it('should mint a new Dizzy Dragon NFT', async () => {
// Access the logs of the mint transaction
// Remember: mintedNft was created in the beforeEach function!
// We grab the 2nd index here, because in the mint function _safeMint is
// called, which emits a Transfer event. Then the approve function is called,
// which emits an Approval event, and lastly the NftMinted event is emitted
const nftMintedLog = mintedNft.logs[2];
// We'll use these variables from the event logs in our tests
const event = nftMintedLog.event;
const tokenId = nftMintedLog.args[0].toNumber();
// Use Mocha's assert to test that the NftMinted event was emitted
// and the token ID of the NFT is 1
assert.equal(event, 'NftMinted');
assert.equal(tokenId, 1);
});
Assuming your Moonbeam development node is up and running, you can run the test with the following command:
npx truffle test --network dev
Test Listing NFTs¶
For our next test, we're going to test that we can successfully list our freshly minted NFT using the listNft
function of the NftMarketplace
contract. So, again we'll use our event logs to test that the NftListed
event has been emitted along with the correct state variables such as the seller and token ID. The event logs will return the arguments passed to the NftMinted
event, which will be the seller's address, the NFT's address, the token ID and the listing price:
event NftListed(
address indexed seller,
address indexed nftAddress,
uint256 indexed tokenId,
uint256 price
);
So, our event logs should resemble the following:
{
...
event: 'NftListed',
args: {
...
seller: '0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b',
nftAddress: '0x4D73053013F876e319f07B27B59158Cca01A64C5',
tokenId: [BN],
price: [BN]
}
}
With this in mind, we can tackle our next test. So, after the first test, we can go ahead and the following:
it('should list a new Dizzy Dragon NFT', async () => {
// Access the logs of the mint transaction so we can grab
// the contract address of the NFT and the token ID
const nftMintedLog = mintedNft.logs[2];
// Assemble the arguments needed to list the NFT that was minted
// in the beforeEach function
const price = await web3.utils.toWei('1', 'ether'); // Set the price of the NFT to 1 ether
const nftAddress = nftMintedLog.address;
const mintTokenId = nftMintedLog.args[0].toNumber();
// Call the listNft function of the NftMarketplace contract with the
// address of the NFT, the token ID, and the price
const listResult = await nftMarketplace.listNft(
nftAddress,
mintTokenId,
price
);
// We'll use these variables from the event logs in our tests. Use
// the 0 index because the NftListed event is the only event emitted
const event = listResult.logs[0].event;
const seller = listResult.logs[0].args.seller;
const tokenId = listResult.logs[0].args.tokenId.toNumber();
// Use Mocha's assert to test that the NftListed event was emitted
// with the correct arguments for the seller and token ID
assert.equal(event, 'NftListed');
assert.equal(seller, accounts[0]);
assert.equal(tokenId, mintTokenId);
});
Again, you can run the tests to make sure that the tests pass as expected.
Test Purchasing NFTs¶
Finally, let's test that an NFT on our marketplace can be purchased using the purchaseNft
function of the NftMarketplace
contract. Similarly to our previous tests, we'll use the event logs to test that the NftPurchased
event has been emitted along with the correct state variables such as the buyer and token ID. The event logs will return the arguments passed to the NftPurchased
event, which will be the buyer's address, the NFT's address, the token ID and the purchase price:
event NftListed(
address indexed buyer,
address indexed nftAddress,
uint256 indexed tokenId,
uint256 price
);
So, our event logs should resemble the following:
{
...
event: 'NftPurchased',
args: {
...
buyer: '0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b',
nftAddress: '0xDdd543E793D91AD9282ACde331ac250A445C9079',
tokenId: [BN],
price: [BN]
}
}
Let's jump into writing the next test by adding the following to our test file:
it('should buy a new Dizzy Dragon NFT', async () => {
const nftMintedLog = mintedNft.logs[2];
// List the NFT first
const price = await web3.utils.toWei('1', 'ether');
const nftAddress = nftMintedLog.address;
const mintTokenId = nftMintedLog.args[0].toNumber();
await nftMarketplace.listNft(
nftAddress,
mintTokenId,
price
);
// Purchase the NFT using the purchaseNft function and passing in the
// address of the NFT and the token ID. We'll also send a payment along
// for the asking price of the NFT
const purchaseNft = await nftMarketplace.purchaseNft(
nftAddress,
mintTokenId,
{ value: price }
);
// We'll use these variables from the event logs in our tests. Use
// the 0 index because the NftPurchased event is the only event emitted
const event = purchaseNft.logs[0].event;
const buyer = purchaseNft.logs[0].args.buyer;
const tokenId = listResult.logs[0].args.tokenId.toNumber();
// Use Mocha's assert to test that the NftPurchased event was emitted
// with the correct argument for the buyer and token ID
assert.equal(event, 'NftPurchased');
assert.equal(buyer, accounts[0]);
assert.equal(tokenId, mintTokenId);
});
That's it for the tests! To run them all, go ahead and run:
npx truffle test --network dev
With Mocha, you have the flexbility to test for a variety of edge cases and don't have to use assert.equal
as we did in our examples. Since support for the Chai assertion library is included, you can also use Chai's assert
API or their expect
and should
APIs. For example, you can also assert for failures using Chai's assert.fail
method.
Challenge
Try adding a test that uses a tokenUri
for an NFT that hasn't approved the NftMarketplace
contract to transfer it. You should assert that the call will fail.
When you're done testing on the Moonbeam development node, don't forget to stop and remove the node! You can do so by running:
npx truffle run moonbeam stop && \
npx truffle run moonbeam remove
Deploy to Moonbase Alpha TestNet¶
Now that we've been able to rapidly develop our contracts with our Moonbeam development node and feel confident with our code, we can move on to testing it on the Moonbase Alpha TestNet.
First, you'll need to update your truffle-config.js
file and add in the private key of your account on Moonbase Alpha. The privateKeyMoonbase
variable already exists, you just need to set it to your private key. This is just for demonstration purposes only, never store your private keys in a JavaScript file.
Once you've set your account up, you can run your tests on Moonbase Alpha to make sure they work as expected on a live network:
npx truffle test --network moonbase
Note
To avoid hitting rate limits with the public endpoint, you can get your own endpoint from one of the supported Endpoint Providers.
Since we already updated our migration script, we're all set to deploy our contracts using this command:
npx truffle migrate --network moonbase
You should see the transaction hashes for the deployment of each contract in your terminal and that a total of three deployments have been made.
With our contracts deployed, we could begin to build a dApp with a frontend that interacts with these contracts, but it's out of scope for this tutorial.
Once you've deployed your contracts, don't forget to verify them! You will run the run verify
command and pass in the deployed contracts' names and the network where they've been deployed to:
npx truffle run verify NftMarketplace DizzyDragons --network moonbase
For reference, you can check out how the verified contracts will look on Moonscan for the NftMarketplace and DizzyDragons contracts.
Feel free to play around and interact with your contracts on the TestNet! Since DEV tokens have no real monetary value, now is a good time to work out any kinks before we deploy our contracts to Moonbeam MainNet where the tokens do have value!
Deploy to Production on Moonbeam MainNet¶
We thought we felt confident before, but now that we've tested our contracts on Moonbase Alpha we feel even more confident. So let's deploy our contracts on Moonbeam!
Again, you'll need to update your truffle-config.js
and add in the private key of your account on Moonbeam. If you're interested in a secure way to add your private keys, you can check out the Truffle Dashboard, which allows you to connect to your MetaMask wallet without any configuration. For more information, please refer to the Truffle docs on using the Truffle Dashboard. Whatever you do, never store your private keys in a JavaScript file.
Since the Moonbeam Truffle box doesn't come with the Moonbeam network configurations, you'll need to add them:
...
module.exports = {
networks: {
...
moonbeam: {
provider: () => {
...
return new HDWalletProvider(
'PRIVATE_KEY_HERE', // Insert your private key here
'INSERT_RPC_API_ENDPOINT' // Insert your RPC URL here
)
},
network_id: 1284 // (hex: 0x504),
},
}
}
If you're using Truffle Dashboard, you'll need to add the host/port configuration for your dashboard.
You can deploy your contracts using this command:
npx truffle migrate --network moonbeam
You should see the transaction hashes for the deployment of each contract in your terminal and that a total of three deployments have been made.
Again, don't forget to verify the contracts! You will run the run verify
command and pass in the deployed contracts' names and moonbeam
as the network:
npx truffle run verify NftMarketplace DizzyDragons --network moonbeam
Note
If you're using a Truffle Dashboard, you'll replace --network moonbeam
with --network dashboard
for any of the Truffle commands.
For reference, you can check out how the verified contracts will look on Moonscan for the NftMarketplace and DizzyDragons contracts.
And that's it! You've successfully deployed your contracts to Moonbeam MainNet after thoroughly testing them out on a local Moonbeam development node and the Moonbase Alpha TestNet! Congrats! You've gone through the entire development life cycle using Truffle!
| Created: May 22, 2023