Using Hardhat with the Metal C-Chain
Introduction
Metal Blockchain is an open-source platform for launching decentralized applications and enterprise blockchain deployments in one interoperable, highly scalable ecosystem. Metal Blockchain gives you complete control on both the network and application layers—helping you build anything you can imagine.
The Metal Network is composed of many blockchains. One of these blockchains is the C-Chain (Contract Chain), which is an Ethereum Virtual Machine instance. The C-Chain's API is almost identical to an Ethereum node's API. Metal Blockchain offers the same interface as Ethereum but with higher speed, higher throughput, lower fees and lower transaction confirmation times. These properties considerably improve the performance of DApps and the user experience of smart contracts.
The goal of this guide is to lay out best practices regarding writing, testing and deployment of smart contracts to Metal Blockchain's C-Chain. We'll be building smart contracts with development environment Hardhat.
Prerequisites
NodeJS and Yarn
First, install the LTS (long-term support) version of
NodeJS. This is 18.x
at the time of writing. NodeJS
bundles npm
.
Next, install yarn:
npm install -g yarn
MetalGo and Metal Network Runner
AvalancheGo is an Metal node implementation written in Go. Metal Network Runner is a tool to quickly deploy local test networks. Together, you can deploy local test networks and run tests on them.
Solidity and Avalanche
It is also helpful to have a basic understanding of Solidity and Metal Blockchain.
Dependencies
Clone the quickstart
repository and
install the necessary packages via yarn
.
git clone https://github.com/MetalBlockchain/metal-smart-contract-quickstart.git
cd metal-smart-contract-quickstart
yarn
Write Contracts
Edit the ExampleERC20.sol
contract in contracts/
. ExampleERC20.sol
is an
Open Zeppelin
ERC20 contract. ERC20 is a popular
smart contract interface. You can also add your own contracts.
Hardhat Config
Hardhat uses hardhat.config.js
as the configuration file. You can define
tasks, networks, compilers and more in that file. For more information see
here.
In our repository we use a pre-configured file hardhat.config.ts. This file configures necessary network information to provide smooth interaction with Metal MetalBlockchain. There are also some pre-defined private keys for testing on a local test network.
The port in this tutorial uses 9650. Depending on how you start your local network, it could be different. Please check here to see how to retrieve the port numbers.
Hardhat Tasks
You can define custom hardhat tasks in
hardhat.config.ts.
There are two tasks included as examples: accounts
and balances
. Both have
scripts in
package.json.
"accounts": "npx hardhat accounts",
"balances": "npx hardhat balances"
yarn accounts
prints the list of accounts. yarn balances
prints the list of
AVAX account balances. As with other yarn
scripts you can pass in a
--network
flag to hardhat tasks.
Accounts
Prints a list of accounts on the local Metal Network Runner network.
$ yarn accounts --network local
yarn run v1.22.4
npx hardhat accounts --network local
0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC
0x9632a79656af553F58738B0FB750320158495942
0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430
0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4
0x0B891dB1901D4875056896f28B6665083935C7A8
0x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2
0x78A23300E04FB5d5D2820E23cc679738982e1fd5
0x3C7daE394BBf8e9EE1359ad14C1C47003bD06293
0x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB
0x0Fa8EA536Be85F32724D57A37758761B86416123
Balances
Prints a list of accounts and their corresponding METAL balances on the local Metal Network Runner network.
$ yarn balances --network local
yarn run v1.22.4
npx hardhat balances --network local
0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC has balance 50000000000000000000000000
0x9632a79656af553F58738B0FB750320158495942 has balance 0
0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430 has balance 0
0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4 has balance 0
0x0B891dB1901D4875056896f28B6665083935C7A8 has balance 0
0x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2 has balance 0
0x78A23300E04FB5d5D2820E23cc679738982e1fd5 has balance 0
0x3C7daE394BBf8e9EE1359ad14C1C47003bD06293 has balance 0
0x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB has balance 0
0x0Fa8EA536Be85F32724D57A37758761B86416123 has balance 0
Notice that the first account is already funded. This is because this address is pre-funded in the local network genesis file.
Hardhat Help
Run yarn hardhat
to list Hardhat's version, usage instructions, global options and available tasks.
Typical Metal Network Runner Workflow
Run Metal Network Runner
First confirm you have the latest MetalGo built.
cd /path/to/metalgo
git fetch -p
git checkout master
./scripts/build.sh
(Note that you can also download pre-compiled AvalancheGo binaries rather than building from source.)
Confirm you have Metal Network Runner installed by following the steps listed here
Start Metal Network Runner and run a script to start a new local network.
Start the Server
$ cd /path/to/Metal-Network-Runner
$ metal-network-runner server \
--log-level debug \
--port=":8080" \
--grpc-gateway-port=":8081"
Start a New Metal Network with Five Nodes
# replace execPath with the path to MetalGo on your machine
# e.g., ${HOME}/go/src/github.com/MetalBlockchain/metalgo/build/metalgo
$ METALGO_EXEC_PATH="metalgo"
$ metal-network-runner control start \
--log-level debug \
--endpoint="0.0.0.0:8080" \
--number-of-nodes=5 \
--avalanchego-path ${METALGO_EXEC_PATH}
Now you're running a local Metal network with 5 nodes.
Fund Accounts
Transfer 1,000 METAL from the X-Chain to each of the 10 accounts in
hardhat.config.ts
with the script
fund-cchain-addresses
.
Funding these accounts is a prerequisite for deploying and interacting with
smart contracts.
Note: If you see Error: Invalid JSON RPC response: "API call rejected because
chain is not done bootstrapping"
, you need to wait until network is
bootstrapped and ready to use. It should not take too long.
$ cd /path/to/metal-smart-contract-quickstart
$ yarn fund-cchain-addresses
yarn run v1.22.4
npx hardhat run scripts/fund-cchain-addresses.js
Exporting 1000 METAL to each address on the C-Chain...
2b75ae74ScLkWe5GVFTYJoP2EniMywkcZySQUoFGN2EJLiPDgp
Importing METAL to the C-Chain...
2dyXcQGiCk1ckCX4Fs8nLgL8GJgsM72f9Ga13rX5v9TAguVJYM
✨ Done in 5.03s.
Confirm each of the accounts are funded with 1000 METAL.
$ yarn balances --network local
yarn run v1.22.4
npx hardhat balances --network local
0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC has balance 50000001000000000000000000
0x9632a79656af553F58738B0FB750320158495942 has balance 1000000000000000000
0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430 has balance 1000000000000000000
0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4 has balance 1000000000000000000
0x0B891dB1901D4875056896f28B6665083935C7A8 has balance 1000000000000000000
0x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2 has balance 1000000000000000000
0x78A23300E04FB5d5D2820E23cc679738982e1fd5 has balance 1000000000000000000
0x3C7daE394BBf8e9EE1359ad14C1C47003bD06293 has balance 1000000000000000000
0x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB has balance 1000000000000000000
0x0Fa8EA536Be85F32724D57A37758761B86416123 has balance 1000000000000000000
✨ Done in 0.72s.
Send each of the accounts some METAL from the first account.
$ yarn send-metal-wallet-signer --network local
yarn run v1.22.4
npx hardhat run scripts/sendMetalWalletSigner.ts --network local
Seeding addresses with METAL
✨ Done in 1.33s.
Confirm that the balances are updated
$ yarn balances --network local
yarn run v1.22.4
npx hardhat balances --network local
0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC has balance 49999999995275000000000000
0x9632a79656af553F58738B0FB750320158495942 has balance 1000010000000000000000
0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430 has balance 1000010000000000000000
0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4 has balance 1000010000000000000000
0x0B891dB1901D4875056896f28B6665083935C7A8 has balance 1000010000000000000000
0x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2 has balance 1000010000000000000000
0x78A23300E04FB5d5D2820E23cc679738982e1fd5 has balance 1000010000000000000000
0x3C7daE394BBf8e9EE1359ad14C1C47003bD06293 has balance 1000010000000000000000
0x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB has balance 1000010000000000000000
0x0Fa8EA536Be85F32724D57A37758761B86416123 has balance 1000010000000000000000
Note: If you see Error HH108: Cannot connect to the network local. Please make
sure your node is running, and check your internet connection and networks
config
, ensure that you are using a valid Node Port. See which ports the Nodes
are using by running the command:
$ cd /path/to/metal-network-runner
$ metal-network-runner control uris \
--log-level debug \
--endpoint="0.0.0.0:8080"
Compile Smart Contracts
In
package.json
there's a compile
script.
"compile": "npx hardhat compile",
Run yarn compile
to make sure your project compiles.
Compile the smart contract.
$ yarn compile
yarn run v1.22.4
rimraf ./build/
npx hardhat compile
Compiling 1 file with 0.6.4
Compilation finished successfully
✨ Done in 2.13s.
Deploy Smart Contracts
Hardhat enables deploying to multiple environments. In package.json there is a script for deploying.
Edit the deployment script in scripts/deploy.ts
"deploy": "npx hardhat run scripts/deploy.ts",
You can choose which environment that you want to deploy to by passing in the
--network
flag with local
(for example a local network created with Metal
Network Runner), tahoe
, or mainnet
for each respective environment. If you
don't pass in --network
then it will default to the hardhat network. For
example, if you want to deploy to Mainnet:
yarn deploy --network mainnet
Deploy the contract to your local network
$ yarn deploy --network local
yarn run v1.22.4
npx hardhat run scripts/deploy.ts --network local
Coin deployed to: 0x17aB05351fC94a1a67Bf3f56DdbB941aE6
✨ Done in 1.28s.
We now have a token deployed at 0x17aB05351fC94a1a67Bf3f56DdbB941aE6
.
Interact with Smart Contract
Hardhat has a developer console to interact with contracts and the network. For more information about Hardhat's console see here. Hardhat console is a NodeJS-REPL, and you can use different tools in it. Ethers is the library that we'll use to interact with our network.
You can open console with:
$ yarn console --network local
yarn run v1.22.11
npx hardhat console --network local
Welcome to Node.js v16.2.0.
Type ".help" for more information.
>
Get the contract instance with factory and contract address to interact with our contract:
> const Coin = await ethers.getContractFactory('ExampleERC20');
undefined
> const coin = await Coin.attach('0x17aB05351fC94a1a67Bf3f56DdbB941aE6')
undefined
The first line retrieves contract factory with ABI & bytecode. The second line
retrieves an instance of that contract factory with given contract address.
Recall that our contract was already deployed to
0x17aB05351fC94a1a67Bf3f56DdbB941aE6
in the previous step.
Fetch the accounts:
> let accounts = await ethers.provider.listAccounts()
undefined
> accounts
[
'0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC',
'0x9632a79656af553F58738B0FB750320158495942',
'0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430',
'0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4',
'0x0B891dB1901D4875056896f28B6665083935C7A8',
'0x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2',
'0x78A23300E04FB5d5D2820E23cc679738982e1fd5',
'0x3C7daE394BBf8e9EE1359ad14C1C47003bD06293',
'0x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB',
'0x0Fa8EA536Be85F32724D57A37758761B86416123'
]
This is exactly the same account list as in yarn accounts
.
Now we can interact with our ERC-20
contract:
> let value = await coin.balanceOf(accounts[0])
undefined
> value.toString()
'123456789'
> value = await coin.balanceOf(accounts[1])
BigNumber { _hex: '0x00', _isBigNumber: true }
> value.toString()
'0'
account[0]
has a balance because account[0]
is the default account. The
contract is deployed with this account. The constructor of
ERC20.sol
mints TOTAL_SUPPLY
of 123456789 token to the deployer of the contract.
accounts[1]
currently has no balance. Send some tokens to accounts[1]
, which is 0x9632a79656af553F58738B0FB750320158495942
.
> let result = await coin.transfer(accounts[1], 100)
undefined
> result
{
hash: '0x35eec91011f9089ba7689479617a90baaf8590395b5c80bb209fa7000e4848a5',
type: 0,
accessList: null,
blockHash: null,
blockNumber: null,
transactionIndex: null,
confirmations: 0,
from: '0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC',
gasPrice: BigNumber { _hex: '0x34630b8a00', _isBigNumber: true },
gasLimit: BigNumber { _hex: '0x8754', _isBigNumber: true },
to: '0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25',
value: BigNumber { _hex: '0x00', _isBigNumber: true },
nonce: 3,
data: '0xa9059cbb0000000000000000000000009632a79656af553f58738b0fb7503201584959420000000000000000000000000000000000000000000000000000000000000064',
r: '0xc2b9680771c092a106eadb2887e5bff41fcda166c8e00f36ae79b196bbc53d36',
s: '0x355138cb5e2b9f20c15626638750775cfc9423881db374d732a8549d05ebf601',
v: 86260,
creates: null,
chainId: 43112,
wait: [Function (anonymous)]
}
Note: Since this is a local network, we did not need to wait until transaction
is accepted. However for other networks like tahoe
or mainnet
you need to
wait until transaction is accepted with: await result.wait()
.
Now we can ensure that tokens are transferred:
> value = await coin.balanceOf(accounts[0])
BigNumber { _hex: '0x075bccb1', _isBigNumber: true }
> value.toString()
'123456689'
> value = await coin.balanceOf(accounts[1])
BigNumber { _hex: '0x64', _isBigNumber: true }
> value.toString()
'100'
As you might noticed there was no "sender" information in await
coin.transfer(accounts[1], 100)
; this is because ethers
uses the first signer
as the default signer. In our case this is account[0]
. If we want to use
another account we need to connect with it first.
> let signer1 = await ethers.provider.getSigner(1)
> let contractAsSigner1 = coin.connect(signer1)
Now we can call the contract with signer1
, which is account[1]
.
> await contractAsSigner1.transfer(accounts[0], 5)
{
hash: '0x807947f1c40bb723ac312739d238b62764ae3c3387c6cdbbb6534501577382dd',
type: 0,
accessList: null,
blockHash: null,
blockNumber: null,
transactionIndex: null,
confirmations: 0,
from: '0x9632a79656af553F58738B0FB750320158495942',
gasPrice: BigNumber { _hex: '0x34630b8a00', _isBigNumber: true },
gasLimit: BigNumber { _hex: '0x8754', _isBigNumber: true },
to: '0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25',
value: BigNumber { _hex: '0x00', _isBigNumber: true },
nonce: 2,
data: '0xa9059cbb0000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fc0000000000000000000000000000000000000000000000000000000000000005',
r: '0xcbf126dd0b109491d037c5f3af754ef2d0d7d06149082b13d0e27e502d3adc5b',
s: '0x5978521804dd15674147cc6b532b8801c4d3a0e94f41f5d7ffaced14b9262504',
v: 86259,
creates: null,
chainId: 43112,
wait: [Function (anonymous)]
}
Let's check balances now:
> value = await coin.balanceOf(accounts[0])
BigNumber { _hex: '0x075bccb6', _isBigNumber: true }
> value.toString()
'123456694'
> value = await coin.balanceOf(accounts[1])
BigNumber { _hex: '0x5f', _isBigNumber: true }
> value.toString()
'95'
We've successfully transferred 5 tokes from accounts[1]
to accounts[0]
Summary
Now you have the tools you need to launch a local Metal Blockchain network, create a Hardhat project, as well as create, compile, deploy and interact with Solidity contracts.
If you have questions, problems, or just want to chat with us, you can reach us on our public Telegram chat. We'd love to hear from you and find out what you're building on Metal Blockchain!