Using Waffle & Mars to Deploy to Moonbeam¶
Introduction¶
Waffle is a library for compiling and testing smart contracts, and Mars is a deployment manager. Together, Waffle and Mars can be used to write, compile, test, and deploy Ethereum smart contracts. Since Moonbeam is Ethereum compatible, Waffle and Mars can be used to deploy smart contracts to a Moonbeam development node or the Moonbase Alpha TestNet.
Waffle uses minimal dependencies, has syntax that is easy to learn and extend, and provides fast execution times when compiling and testing smart contracts. Furthermore, it is TypeScript compatible and uses Chai matchers to make tests easy to read and write.
Mars provides a simple, TypeScript compatible framework for creating advanced deployment scripts and staying in sync with state changes. Mars focuses on infrastructure-as-code, allowing developers to specify how their smart contracts should be deployed and then using those specifications to automatically handle state changes and deployments.
In this guide, you'll be creating a TypeScript project to write, compile, and test a smart contract using Waffle, then deploy it on to the Moonbase Alpha TestNet using Mars.
Checking Prerequisites¶
You will need to have the following:
- MetaMask installed and connected to Moonbase Alpha
- An account with funds. You can get DEV tokens for testing on Moonbase Alpha once every 24 hours from the Moonbase Alpha Faucet
- To test out the examples in this guide on Moonbeam or Moonriver, you will need to have your own endpoint and API key, which you can get from one of the supported Endpoint Providers
Once you've created an account you'll need to export the private key to be used in this guide.
Create a TypeScript Project with Waffle & Mars¶
To get started, you'll create a TypeScript project and install and configure a few dependencies.
-
Create the project directory and change to it:
mkdir waffle-mars && cd waffle-mars
-
Initialize the project. Which will create a
package.json
in the directory:npm init -y
-
Install the following dependencies:
npm install ethereum-waffle ethereum-mars ethers \ @openzeppelin/contracts typescript ts-node chai \ @types/chai mocha @types/mocha
- Waffle - for writing, compiling, and testing smart contracts
- Mars - for deploying smart contracts to Moonbeam
- Ethers - for interacting with Moonbeam's Ethereum API
- OpenZeppelin Contracts - the contract you'll be creating will use OpenZeppelin's ERC-20 base implementation
- TypeScript - the project will be a TypeScript project
- TS Node - for executing the deployment script you'll create later in this guide
- Chai - an assertion library used alongside Waffle for writing tests
- @types/chai - contains the type definitions for chai
- Mocha - a testing framework for writing tests alongside Waffle
- @types/mocha - contains the type definitions for mocha
-
Create a TypeScript configuration file:
touch tsconfig.json
-
Add a basic TypeScript configuration:
{ "compilerOptions": { "strict": true, "target": "ES2019", "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, "module": "CommonJS", "composite": true, "sourceMap": true, "declaration": true, "noEmit": true } }
Now, you should have a basic TypeScript project with the necessary dependencies to get started building with Waffle and Mars.
Add a Contract¶
For this guide, you will create an ERC-20 contract that mints a specified amount of tokens to the contract creator. It's based on the OpenZeppelin ERC-20 template.
-
Create a directory to store your contracts and a file for the smart contract:
mkdir contracts && cd contracts && touch MyToken.sol
-
Add the following contract to
MyToken.sol
:pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MyToken is ERC20 { constructor() ERC20("MyToken", "MYTOK") {} function initialize(uint initialSupply) public { _mint(msg.sender, initialSupply); } }
In this contract, you are creating an ERC-20 token called MyToken with the symbol MYTOK, that allows you, as the contract creator, to mint as many MYTOKs as desired.
Use Waffle to Compile and Test¶
Compile with Waffle¶
Now that you have written a smart contract, the next step is to use Waffle to compile it. Before diving into compiling your contract, you will need to configure Waffle:
-
Go back to the root project directory and create a
waffle.json
file to configure Waffle:cd .. && touch waffle.json
-
Edit the
waffle.json
to specify compiler configurations, the directory containing your contracts, and more. For this example, we'll usesolcjs
and the Solidity version you used for the contract, which is0.8.0
:{ "compilerType": "solcjs", "compilerVersion": "0.8.0", "compilerOptions": { "optimizer": { "enabled": true, "runs": 20000 } }, "sourceDirectory": "./contracts", "outputDirectory": "./build", "typechainEnabled": true }
-
Add a script to run Waffle in the
package.json
:"scripts": { "build": "waffle" },
That is all you need to do to configure Waffle, now you're all set to compile the MyToken
contract using the build
script:
npm run build
After compiling your contracts, Waffle stores the JSON output in the build
directory. Since the contract in this guide is based on OpenZeppelin's ERC-20 template, relevant ERC-20 JSON files will appear in the build
directory too.
Test with Waffle¶
Before deploying your contract and sending it off into the wild, you should test it first. Waffle provides an advanced testing framework and has plenty of tools to help you with testing.
You'll be running tests against the Moonbase Alpha TestNet and will need the corresponding RPC URL to connect to it: https://rpc.api.moonbase.moonbeam.network
.
To configure your project for Moonbeam or Moonriver, you will need to have your own endpoint and API key, which you can get from one of the supported Endpoint Providers.
Since you will be running tests against the TestNet, it might take a couple minutes to run all of the tests. If you want a more efficient testing experience, you can spin up a Moonbeam development node using instant seal
. Running a local Moonbeam development node with the instant seal
feature is similar to the quick and iterative experience you would get with Hardhat Network.
-
Create a directory to contain your tests and a file to test your
MyToken
contract:mkdir test && cd test && touch MyToken.test.ts
-
Open the
MyToken.test.ts
file and setup your test file to use Waffle's Solidity plugin and use Ethers custom JSON-RPC provider to connect to Moonbase Alpha:import { use, expect } from 'chai'; import { Provider } from '@ethersproject/providers'; import { solidity } from 'ethereum-waffle'; import { ethers, Wallet } from 'ethers'; import { MyToken, MyTokenFactory } from '../build/types'; // Tell Chai to use Waffle's Solidity plugin use(solidity); describe ('MyToken', () => { // Use custom provider to connect to Moonbase Alpha let provider: Provider = new ethers.providers.JsonRpcProvider( 'https://rpc.api.moonbase.moonbeam.network' ); let wallet: Wallet; let walletTo: Wallet; let token: MyToken; beforeEach(async () => { // Logic for setting up the wallet and deploying MyToken will go here }); // Tests will go here })
-
Before each test is run, you'll want to create wallets and connect them to the provider, use the wallets to deploy an instance of the
MyToken
contract, and then call theinitialize
function once with an initial supply of 10 tokens:beforeEach(async () => { // This is for demo purposes only. Never store your private key in a JavaScript/TypeScript file const privateKey = 'INSERT_PRIVATE_KEY' // Create a wallet instance using your private key & connect it to the provider wallet = new Wallet(privateKey).connect(provider); // Create a random account to transfer tokens to & connect it to the provider walletTo = Wallet.createRandom().connect(provider); // Use your wallet to deploy the MyToken contract token = await new MyTokenFactory(wallet).deploy(); // Mint 10 tokens to the contract owner, which is you let contractTransaction = await token.initialize(10); // Wait until the transaction is confirmed before running tests await contractTransaction.wait(); });
-
Now you can create your first test. The first test will check your initial balance to ensure you received the initial supply of 10 tokens. However, to follow good testing practices, write a failing test first:
it('Mints the correct initial balance', async () => { expect(await token.balanceOf(wallet.address)).to.equal(1); // This should fail });
-
Before you can run your first test, you'll need to go back to the root direction and add a
.mocharc.json
Mocha configuration file:cd .. && touch .mocharc.json
-
Now edit the
.mocharc.json
file to configure Mocha:{ "require": "ts-node/register/transpile-only", "timeout": 600000, "extension": "test.ts" }
-
You'll also need to add a script in the
package.json
to run your tests:"scripts": { "build": "waffle", "test": "mocha" },
-
You're all set to run the tests, simply use the
test
script you just created and run:npm run test
Please note that it could take a few minutes to process because the tests are running against Moonbase Alpha, but if all worked as expected, you should have one failing test.
-
Next, you can go back and edit the test to check for 10 tokens:
it('Mints the correct initial balance', async () => { expect(await token.balanceOf(wallet.address)).to.equal(10); // This should pass });
-
If you run the tests again, you should now see one passing test:
npm run test
-
You've tested the ability to mint tokens, next you'll test the ability to transfer the minted tokens. If you want to write a failing test first again that is up to, however the final test should look like this:
it('Should transfer the correct amount of tokens to the destination account', async () => { // Send the destination wallet 7 tokens await (await token.transfer(walletTo.address, 7)).wait(); // Expect the destination wallet to have received the 7 tokens expect(await token.balanceOf(walletTo.address)).to.equal(7); });
Congratulations, you should now have two passing tests! Altogether, your test file should look like this:
import { use, expect } from 'chai';
import { Provider } from '@ethersproject/providers';
import { solidity } from 'ethereum-waffle';
import { ethers, Wallet } from 'ethers';
import { MyToken, MyTokenFactory } from '../build/types';
use(solidity);
describe('MyToken', () => {
let provider: Provider = new ethers.providers.JsonRpcProvider(
'https://rpc.api.moonbase.moonbeam.network'
);
let wallet: Wallet;
let walletTo: Wallet;
let token: MyToken;
beforeEach(async () => {
// For demo purposes only. Never store your private key in a JavaScript/TypeScript file
const privateKey = 'INSERT_PRIVATE_KEY';
wallet = new Wallet(privateKey).connect(provider);
walletTo = Wallet.createRandom().connect(provider);
token = await new MyTokenFactory(wallet).deploy();
let contractTransaction = await token.initialize(10);
await contractTransaction.wait();
});
it('Mints the correct initial balance', async () => {
expect(await token.balanceOf(wallet.address)).to.equal(10);
});
it('Should transfer the correct amount of tokens to the destination account', async () => {
await (await token.transfer(walletTo.address, 7)).wait();
expect(await token.balanceOf(walletTo.address)).to.equal(7);
});
});
If you want to write more tests on your own, you could consider testing transfers from accounts without any funds or transfers from accounts without enough funds.
Use Mars to Deploy to Moonbase Alpha¶
After you compile your contracts and before deployment, you will have to generate contract artifacts for Mars. Mars uses the contract artifacts for typechecks in deployments. Then you'll need to create a deployment script and deploy the MyToken
smart contract.
Remember, you will be deploying to Moonbase Alpha and will need to use the TestNet RPC URL:
https://rpc.api.moonbase.moonbeam.network
To configure your project for Moonbeam or Moonriver, you will need to have your own endpoint and API key, which you can get from one of the supported Endpoint Providers.
The deployment will be broken up into three sections: generate artifacts, create a deployment script, and deploy with Mars.
Generate Artifacts¶
Artifacts need to be generated for Mars so that typechecks are enabled within deployment scripts.
-
Update existing script to run Waffle in the
package.json
to include Mars:"scripts": { "build": "waffle && mars", "test": "mocha" },
-
Generate the artifacts and create the
artifacts.ts
file needed for deployments:npm run build
If you open the build
directory, you should now see an artifacts.ts
file containing the artifact data needed for deployments. To continue on with the deployment process, you'll need to write a deployment script. The deployment script will be used to tell Mars which contract to deploy, to what network, and which account is to be used to trigger the deployment.
Create a Deployment Script¶
Now you need to configure the deployment for the MyToken
contract to the Moonbase Alpha TestNet.
In this step, you'll create the deployment script which will define how the contract should be deployed. Mars offers a deploy
function that you can pass options to such as the private key of the account to deploy the contract, the network to deploy to, and more. Inside of the deploy
function is where the contracts to be deployed are defined. Mars has a contract
function that accepts the name
, artifact
, and constructorArgs
. This function will be used to deploy the MyToken
contract with an initial supply of 10 MYTOKs.
-
Create a
src
directory to contain your deployment scripts and create the script to deploy theMyToken
contract:mkdir src && cd src && touch deploy.ts
-
In
deploy.ts
, use Mars'deploy
function to create a script to deploy to Moonbase Alpha using your account's private key:import { deploy } from 'ethereum-mars'; // For demo purposes only. Never store your private key in a JavaScript/TypeScript file const privateKey = 'INSERT_PRIVATE_KEY'; deploy( { network: 'https://rpc.api.moonbase.moonbeam.network', privateKey }, (deployer) => { // Deployment logic will go here } );
-
Set up the
deploy
function to deploy theMyToken
contract created in the previous steps:import { deploy, contract } from 'ethereum-mars'; import { MyToken } from '../build/artifacts'; // For demo purposes only. Never store your private key in a JavaScript/TypeScript file const privateKey = 'INSERT_PRIVATE_KEY'; deploy({ network: 'https://rpc.api.moonbase.moonbeam.network', privateKey }, () => { contract('myToken', MyToken); });
-
Add a deploy script to the
scripts
object in thepackage.json
:"scripts": { "build": "waffle && mars", "test": "mocha", "deploy": "ts-node src/deploy.ts" }
So far, you should have created a deployment script in deploy.ts
that will deploy the MyToken
contract to Moonbase Alpha, and added the ability to easily call the script and deploy the contract.
Deploy with Mars¶
You've configured the deployment, now it's time to actually deploy to Moonbase Alpha.
-
Deploy the contract using the script you just created:
npm run deploy
-
In your Terminal, Mars will prompt you to press
ENTER
to send your transaction
If successful, you should see details about your transaction including it's hash, the block it was included in, and it's address.
Congratulations! You've deployed a contract to Moonbase Alpha using Waffle and Mars!
Example Project¶
If you want to see a completed example of a Waffle and Mars project on Moonbeam, check out the moonbeam-waffle-mars-example created by the team behind Waffle and Mars, EthWorks.
| Created: May 18, 2021