Table of contents
These are my notes from the 5th lesson of Freecodecamp's web3/Blockchain development course. You can read the notes for the previous lesson here =>muratcanyuksel.hashnode.dev/creating-a-stor..
In the video, Patrick starts with a Javascript refresher. This post does not contain notes to that section. Here you'll only find things directly related to blockchain development.
Starting the project
First, in our project root folder that contains the SimpleStorage.sol
file, we create a deploy.js
file and populate it with the following code =>
async function main() {}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Since we want to compile our SimpleStorage.sol
contract, we need to install the solc-js
compiler. Here's the link to the solc-js
repo => github.com/ethereum/solc-js
We install it with the following command => yarn add solc@0.8.7-fixed
Now, the reason we installed this very specific version is because our SimpleStorage.sol
smart contract's compiler version was defined as such. That means that if you go the the SimpleStorage
you'd see this line there => pragma solidity 0.8.7;
After the installation, if we run the following command => yarn solcjs --bin --abi --include-path node_modules/ --base-path . -o . SimpleStorage.sol
it will create two files :SimpleStorage_sol_SimpleStorage.abi
i.e. the ABI of our contract, and SimpleStorage_sol_SimpleStorage.bin
i.e. the binary of our contract, i.e. really low level of this code.
If we were using Remix IDE, we could've checked these info on the Compilation Details
after compiling our contract.
But since writing all those lines are tedious, we're going to write our own scripts in package.json
file like so:
{
"dependencies": {
"solc": "0.8.7-fixed"
},
"scripts": {
"compile": "yarn solcjs --bin --abi --include-path node_modules/ --base-path . -o . SimpleStorage.sol"
}
}
Now, whenever we enter yarn compile
, it'll run that tedious script for us.
Ganache & Networks
In Remix IDE, we can either deploy our contract into Javascript VM
or Injected Web3
. We will learn how to do both, starting with Javascript VM
, i.e. a fake blockchain. In the future, we'll be using hardhat
, but for this course we'll use ganache
.
We download ganache, run it, and click quickstart
. Now, I did download ganache a long time ago, so I don't remember how I did it exactly for my manjaro linux OS. You'll figure it our yourself for your OS.
When we click quickstart
, it'll run a fake blockchain on our computer, with fake eth in fake accounts just like the Remix IDE does.
In our code, one of the first things we want to do is to connect to a blockchain. If we'd open Remix, choose Injected, and there, click add network
on our metamask wallet, and from there choose networks
section on the left sidebar, we'd see info about those networks. Let's learn more about the RPC URL
. RPC stands for Remote Procedure Call, and url stands for Uniform Resource Locator.This RPC URLstands for connection to a blockchain node that somebody is running. The URL inside the input field connects us to make API calls to interact with a blockchain node.
Now, in ganache, if we'd look at the top we'll see the RPC SERVER
. Its content is our endpoint for the ganache node right now. We need this endpoint. We copy and save it somewhere for later use.
If we go to the JSON RPC Specification on this link => https://playground.open-rpc.org/?schemaUrl=https://raw.githubusercontent.com/ethereum/execution-apis/assembled-spec/openrpc.json&uiSchema%5BappBar%5D%5Bui:splitView%5D=false&uiSchema%5BappBar%5D%5Bui:input%5D=false&uiSchema%5BappBar%5D%5Bui:examplesDropdown%5D=false
we can see the different calls we can make to our node to get different info. But since making these API calls ourselves is tedious, we're going to use a wrapper to do such things. And here's where ethers.js
comes into play.
Intro to Ethers.js
Just here we need a detour. We have our deploy.js file, we want to connect to the local blockchain provided by ganache, and do to that we need to enter some sensitive information like our wallet private key, and sharing our wallet's private key is a HUUUUGE NO-NO. So, in order to never let anyone get a hold of our private key, we're going to use environment variables for now. At the end of the course, Patrick also shows different, even more secure ways to hide sensitive data from being stolen. But now, I'll show the part about env variables.
Install dotenv
package by entering yarn add dotenv
and adding it into our deploy.js
file like so => require("dotenv").config();
Now we'll create a .env
file in our root. We need to populate it with the RPC SERVER
endpoint we've saved somewhere previously and our wallet's pricate key. Now, since we're using ganache, we're provided with a list of fake wallet that we can use for testing. Now we'll copy the private key of the fake wallet we're going to use and save it like the RPC SERVER
endpoint.
Now we can enter these data into our .env
file like so =>
PRIVATE_KEY=0xbxxx1a0d7xxx2221efexxxb18ee8e3c2d08xxx70dxx
RPC_URL= http://xx.0.0.1:xx5x
NB! It is a good practice to add 0x
to your pricate key. EthersJs and Hardhat are smart enough to ignore that, bu still we added it.
Also, you'll see that even though I'm sharing the private key of a fake wallet provided to me by ganache, I still hide it somehow here. Yes, I guess we need to be THAT paranoid about sharing our private keys.
Now, in JS, we can access environment variables with process.env.VARIABLE_NAME
.
Now we can get back to ethersJS
.
Let's install ethers with yarn add ethers
, and import it in our deploy.js
like so => const ethers = require("ethers");
.Let's install ethers with yarn add ethers
, and import it in our deploy.js
like so => const ethers = require("ethers");
.
We add the following snippet into our main
function in deploy.js
=>
const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
The first line says that "we are going to connect to this URl right there". So, our script will connect to our local blockchain.
"These two lines alone gives us everything we need to interact with the smart contract. They give us a connection to the blockchain, and they give us a wallet with a private key so that we can sign different transactions"
In order to deploy our contract, we need the ABI and the binary compiled code of the contract. To do this, we're going to the node.js world and we're gonna use the fs
library. So, In our deploy.js
we add the following line => const fs= require("fs-extra");
(you can also do const fs= require("fs");
). To be on the sure side, just add the library with yarn add fs-extra
.
In order to deploy our contract, we need the ABI and the binary compiled code of the contract. To do this, we're going to the node.js world and we're gonna use the fs
library. So, In our deploy.js
we add the following line => const fs= require("fs-extra");
(you can also do const fs= require("fs");
). To be on the sure side, just add the library with yarn add fs-extra
.
Now, inside our main
function, we add the following lines =>
const abi = fs.readFileSync("SimpleStorage_sol_SimpleStorage.abi", "utf-8");
const binary = fs.readFileSync("SimpleStorage_sol_SimpleStorage.bin", "utf-8");
Now since we have the contract ABI and binary, we can create a contract factory
. In ethersJS
a contract factory
is an object that you can use to deploy contracts.
We also add the logic to deploy our contract.
So, still inside our main
function, we add these lines =>
const contractFactory = new ethers.ContractFactory(abi, binary, wallet);
console.log("Deploying, please wait...");
const contract = await contractFactory.deploy();
console.log(contract);
Now we deploy our contract to our local blockchain by running the following line on the terminal => node deploy.js
. In Ganache, we can see the address we used has a bit less ether, and if we go to the transactions section, we can see our transaction as if we were checking etherscan.
This is the latest version of our main
function:
async function main() {
//my ganache RPC server => HTTP://127.0.0.1:7545
const provider = new ethers.providers.JsonRpcProvider(
"http://127.0.0.1:7545"
);
const wallet = new ethers.Wallet(
"1804a5d0a50eac9f3ff2b8133074ec0e8184decc12f7416d84132896b024ae79",
provider
);
const abi = fs.readFileSync("SimpleStorage_sol_SimpleStorage.abi", "utf-8");
const binary = fs.readFileSync(
"SimpleStorage_sol_SimpleStorage.bin",
"utf-8"
);
const contractFactory = new ethers.ContractFactory(abi, binary, wallet);
console.log("Deploying, please wait...");
const contract = await contractFactory.deploy(); //await keyword says STOP HERE, wait for the contract to be deployed
console.log(contract);
}
about await keyword
If we didn't put the await
there, when we console.logged our contract object, nothing would return. Because the await keyword makes the code stop at that point and makes it wait until the transaction is complete. It'll go to the next line only after the promise is resolved.
Transactions
This part is not as complete as others. Also I omit how to send a raw transaction in ethersJs.
Adding transaction overrides
VsCode tip! IF you click to the `deploy` keyword while pressing `ctrl`, it'll take you to the file where that function's been defined.
We can add arguments to our deploy function, such as gas price, gas limits etc.
Like, we can do this const contract = await contractFactory.deploy({gasPrice:10000000000});
But we're not going to do such a thing right now.
Transaction receipts
We can wait for blocks, for whatever that means, Patrick says "maybe we want to wait one block to make sure it's attached to the chain"
I guess, we're specifying the number of confirmations we want to actually wait. Here, we're going to wait for 1 block of confirmation.
We can do this like so:
const transactionReceipt = await contract.deployTransaction.wait(1);
console.log(transactionReceipt);
interacting with contracts in ethersJS
Since we've deployed our contract to a local blockchain, we can intereact with it. Remember that in Remix IDE, we have buttons after deploying that let us call our functions, variables and so on, we're going to do that with ethers now.
We can start by calling the simplest function inSimpleStorage.sol
by writing this inside our main
function => const currentFavoriteNumber = await contract.retrieve();
Here, the contract
object is what's returned from our contractFactory
as long as we await
it. The code we've written for it was this => const contract = await contractFactory.deploy();
The contract
object is going to come with all the functionality described in the abi.
Here's a trick: We can read what's returned from which function in our SimpleStorage_sol_SimpleStorage.abi
file. But since it is not formatted, it is quite difficult to do so. What we can do about is, we can change the extension to json
, like so => SimpleStorage_sol_SimpleStorage.json
, format it, and change the extension back to abi
and the formatting will stay prettified(if you use prettify or any other linting tool).
Anyways, now, if we add these lines into our code :
const currentFavoriteNumber = await contract.retrieve();
console.log(currentFavoriteNumber);
and enter node deploy.js
on the terminal, we'll get a response like this => BigNumber { _hex: '0x00', _isBigNumber: true }
This BigNumber is a library that comes with ethers application that helps us work with numbers. The reason we use this library is that both solidity and javascript have problems working with big numbers, decimals etc. It would be better to turn them into strings and then work with them. So, instead of the above console.log statement, we write is as such => console.log(currentFavoriteNumber.toString());
we'll get 0
as response since our favoriteNumber gets initialized as 0
if not specified otherwise in our SimpleStorage.sol
contract.
Now, let's update this number by calling the store
function in our smart contact like so => const transactionResponse = await contract.store("7");
Note that we can pass the number as string like we've just done, or as a number without the quotation marks. But, it is advised to use the quotation marks and pass them as strings so JS won't get confused had we were to use a bigger number.
We also wait 1 block for the transaction receipt. With the above line, it'll be like this =>
//update favorite number
const transactionResponse = await contract.store("7");
//wait 1 block
const transactionReceipt = await transactionResponse.wait(1);
Now, when we call a function on a contract, we get a transactionResponse
, and when we wait for the transactionResponse
to finish, we get the transactionReceipt
Now if we create a new variable called updatedFavoriteNumber
and console log it, we'll get the value 7
as response.. Let's check what we've added about this favorite number so far:
//interacting with the contract
//get favorite number
const currentFavoriteNumber = await contract.retrieve();
console.log(`Current favorite number is ${currentFavoriteNumber.toString()}`);
//update favorite number
const transactionResponse = await contract.store("7");
//wait 1 block
const transactionReceipt = await transactionResponse.wait(1);
//get updated number
const updatedFavoriteNumber = await contract.retrieve();
console.log(`Updated favorite number is ${updatedFavoriteNumber.toString()}`);
Encrypting keys with Encrypt.js file
Now, about our sensitive data, keys etc., if you're REALLY REALLY PARANOID, or just really professional, you can encrypt your keys. Let's start by creating a encryptKey.js
file.
The thing is, once we set it up, we can run this file once and then we can remove our keys from our workspace for good.
We start our encryptKey.js
file quite smilar to our deploy.js
file =>
const ethers = require("ethers");
const fs = require("fs-extra");
require("dotenv").config();
async function main() {}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Now, inside our main
function, we're going to create a wallet with the following line => const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
then, we'll create an encryptedJsonKey
variable like so => const encryptedJsonKey = await wallet.encrypt();
Now, this encrypt
function will create an encrypted json key that we can store locally and only decrypt with a password. It takes 2 parameters: a private key password, and a private key. So, in our .env
file we're going to create the following variable JUST FOR NOW PRIVATE_KEY_PASSWORD=password
(yes, we put password
as our password lol)
Now we're going to pass the PRIVATE_KEY_PASSWORD
as the first parameter to our encrypt
function in encryptedJsonKey
variable, and our PRIVATE_KEY
variable as our second parameter. Like so =>
async function main() {
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
const encryptedJsonKey = await wallet.encrypt(
process.env.PRIVATE_KEY_PASSWORD,
process.env.PRIVATE_KEY
);
console.log(encryptedJsonKey);
}
If we run this by node encryptKey.js
we get a json oject, which is the encrypted version of our keys. If someone were to get into our system and see that object, they'd need the password to decrypt it. To reiterate, this is our private key, encrypted. In order to access the key, you need to know the private key password that you've entered into your .env
file.
Now since we've created our key, let's save it by adding this line into our encryptKey.js
file => fs.writeFileSync("./.encryptedKey.json", encryptedJsonKey);
NOTICE THAT THERE IS A DOT IN FRONT OF THE FILE NAME. and run node encryptKey.js
again. This will create a .encryptedKey.json
file. Now we'd want to add this new .encryptedKey.json
file to our .gitignore
file like we do with .env
files if we hadn't done so.
We'd also go to our .env
file and remove the PRIVATE_KEY
as well as PRIVATE_KEY_PASSWORD
variables. We don't need them anymore.
Now that we have our encrypted key, we can go to deploy.js
and change the way how we get to our wallet. We start by commenting out or deleting the following like const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
and add the lines below =>
//const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const encryptedJson = fs.readFileSync("./.encryptedKey.json", "utf-8");
let wallet = new ethers.Wallet.fromEncryptedJsonSync(
encryptedJson,
process.env.PRIVATE_KEY_PASSWORD
);
wallet = await wallet.connect(provider);
Now, in the first line, readFileSync
reads from the .encryptedKey.json
file. We then pass that variable into fromEncryptedJsonSync
function, which takes 2 parameters: the encrypted json key, and the password. We then connect our wallet to our provider. This fromEncryptedJsonSync
and many more can be found on the ethers
documentation.
After we entered these lines, we need to run it. But, since we've deleted the PRIVATE_KEY_PASSWORD
variable from our .env
file, we need to write it manually in the terminal like so => PRIVATE_KEY_PASSWORD=password node deploy.js
(as we gave password
as our password lol). If everything goes alright, we get the following response =>
Deploying, please wait...
Current favorite number is 0
Updated favorite number is 7
NB! If someone hacked into your computer, they can enter the history
command on the terminal and see your password there. So, you might want to run history -c
after doing all these stuff.
This is the latest version of encryptKey.js
=>
const ethers = require("ethers");
const fs = require("fs-extra");
require("dotenv").config();
async function main() {
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
const encryptedJsonKey = await wallet.encrypt(
process.env.PRIVATE_KEY_PASSWORD,
process.env.PRIVATE_KEY
);
console.log(encryptedJsonKey);
fs.writeFileSync("./.encryptedKey.json", encryptedJsonKey);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
optional prettier formatting
In this part, we're installing this package https://github.com/prettier-solidity/prettier-plugin-solidity
to make sure that whomever wants to use our code after we push it to somewhere can get the same formatting as we've useed to avoid confusion.
We install it like so => prettier prettier-plugin-solidity
Then we create a .prettierrc
file and populate it with =>
{
"tabWidth": 4,
"useTabs": false,
"semi": false,
"singleQuote": false
}
Deploying to a testnet or mainnet
We go to Alchemy
, create a new project, and get the HTTP
key there. We will use this instead of the RPC we got from ganache. This will be our RPC URL that connects to the testnet.
So we copy the HTTP KEY
and replace our RPC URL
variable in our .env
file.
We also change the private key we've entered with our real Metamask private key.
Before deployment, we add the following in our deploy.js
file after we wait for a block after deploying the contract to get the address of our contract.
...
const contractFactory = new ethers.ContractFactory(abi, binary, wallet);
console.log("Deploying, please wait...");
const contract = await contractFactory.deploy(); //await keyword says STOP HERE, wait for the contract to be deployed
//wait one block for the transaction receipt
//we comment this out for now as we're not gonna use transactionReceipt now
// const transactionReceipt = await contract.deployTransaction.wait(1);
//we wait 1 block for that transaction to finish
await contract.deployTransaction.wait(1);
console.log(`Contract Address: ${contract.address}`);
...
verifying and publishing
We can go to etherscan and publish our code. It is pretty straightforward, we just need to copy-paste our solidity code there. That's so that anybody can read our contract.
deploy.js in full
This is the last version of our deploy.js
const ethers = require("ethers");
const fs = require("fs-extra");
require("dotenv").config();
async function main() {
const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL);
const encryptedJson = fs.readFileSync("./.encryptedKey.json", "utf-8");
let wallet = new ethers.Wallet.fromEncryptedJsonSync(
encryptedJson,
process.env.PRIVATE_KEY_PASSWORD
);
wallet = await wallet.connect(provider);
const abi = fs.readFileSync("SimpleStorage_sol_SimpleStorage.abi", "utf-8");
const binary = fs.readFileSync(
"SimpleStorage_sol_SimpleStorage.bin",
"utf-8"
);
const contractFactory = new ethers.ContractFactory(abi, binary, wallet);
console.log("Deploying, please wait...");
const contract = await contractFactory.deploy(); //await keyword says STOP HERE, wait for the contract to be deployed
//wait one block for the transaction receipt
//we comment this out for now as we're not gonna use transactionReceipt now
// const transactionReceipt = await contract.deployTransaction.wait(1);
//we wait 1 block for that transaction to finish
await contract.deployTransaction.wait(1);
console.log(`Contract Address: ${contract.address}`);
//interacting with the contract
//get favorite number
const currentFavoriteNumber = await contract.retrieve();
console.log(`Current favorite number is ${currentFavoriteNumber.toString()}`);
//update favorite number
const transactionResponse = await contract.store("7");
//wait 1 block
const transactionReceipt = await transactionResponse.wait(1);
//get updated number
const updatedFavoriteNumber = await contract.retrieve();
console.log(`Updated favorite number is ${updatedFavoriteNumber.toString()}`);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});