Build and deploy a library member registration book on the Ethereum Blockchain using Alchemy - Hardhat & Solidity
Introduction
Record keeping is one of the most important features that we use everyday in our lives. From shopping reciepts, to report cards you'll find that keeping records is super important in the modern day.
Initializing the project
First, lets initialize an empty node project in the terminal
npm init -y
The -y
flag will accept all the default project configurations
Installing dependencies
Now that we have our project set-up, lets install the necessary dependencies that we'll use in the project later
Head over to the terminal and enter the command
npm install --save-dev hardhat
This will install hardhat in the project.
Hardhat is an enviroment that is makes it easier to build, test, deploy and debug dApps built on the ethereum block-chain. Read more
Initializing the hardhat
Now that we have hardhat installed and available to us in the project, we can now create an hardhat boiler-plate sample project using npx hardhat
command.
$ npx hardhat
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888
Welcome to Hardhat v2.0.8
? What do you want to do? …
❯ Create a sample project
Create an advanced sample project
Create an advanced sample project that uses TypeScript
Create an empty hardhat.config.js
Quit
Select ❯ Create a sample project
to get started and accept all the default configurations to get started
Install ethers
In-order interact with the smart contract, we need ether js that has inbuilt helper functions and methods that simplifies the interaction Go to the terminal and enter
npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
Together with
ethers
, we have installed additional libraries that will help us in testing and deploying the smart contract
Enviroment vars and set-up
Create a .env
file and run npm install dotenv --save
In this file, we'll be able to hide our private information from the rest of the application
In this file we need to set up and store the following vars
ALCHEMY_URL=''
ALCHEMY_API_KEY=''
ACCOUNT_ADDRESS=''
PRIVATE_KEY=''
ALCHEMY_URL: Head over to Alchemy, Create an account and start a new project with the ropsten test network. This HTTP url provided by alchemy will give us the connection to the ropsten test network on ethereum. It acts as the the major connection to the blockchain.
ALCHEMY_API_KEY: Enter your Alchemy appliction API Key
ACCOUNT_ADDRESS: Enter your account address the your using on MetaMask.
Install the meta mask chrome extention and create a wallet. Don't forget to change Meta mask's network to match the same network as we set up in alchemy which is ropsten
PRIVATE_KEY: Enter your Meta Musk's account private key
IMPORTANT: HIDE & DO NOT share your PRIVATE KEY with anyone because this will give him full access to all your wallet's assets.
Now that we're done with the enviroment var set-up, close the .env
file and lets go to the next step
Creating the smart contract
Setting up variables
Open the contracts
folder and lets create a new file called Library.sol
Select the solidity versions and SPDX licence
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
Create the contract and call it Library
contract Library {
}
Now lets set up some important contract variables
address public owner;
uint256 public memberCount;
uint256 public bookCount;
uint256 public booksBorrowed;
owner
: This will be the address of the miner or admin that will deploy the contract to the block chain
memberCount
: We may want to keep tract of the number of people that are borrowing the books using this variablebookCount
: We want to keep tract of the number of unborrowed books books that we have in the librarybooksBorrowed
: We also want to keep track of the number of borrowed book in the library
Lets describe a struct data type object to preserve the borrow book info
struct Borrow {
address from;
string bookName;
uint256 pickupDate;
uint256 duration;
}
This keeps track of the name of
bookName
: Name of the book being borrowed instring
data type.
from
: Address of the borrower borrowing the bookpickupDate
: Record the time of borrowing the bookduration
: How long the borrower will stay with the book
In-order to keep track of our smart contract activity, we'll set up events
that will notify us of what is happening in our smart contract.
Using the NewBorrowEvent
event that will fire every time a new borrow record has been registered in the smart contract.
event NewBorrowEvent(
address from,
string bookName,
uint256 pickupDate,
uint256 returnDate
);
Lets add some additional variables that will make it easier for us to navigate the borrow information and make indexing much more easier
// borrow list
Borrow[] borrowers;
// borrow mapping
mapping(address => Borrow) public borrower;
The mapping
will come in handy when we want to index specific borrower information from the smart contract and the borrowers
list will help in looping through the information.
Working with the constructor
The constructor is function that will get executed only once, making it a perfect place for initializing variables.
Lets set up a very simplified constructor
function
constructor(
uint256 _bookCount,
uint256 _memberCount,
uint256 _booksBorrowed
) {
owner = msg.sender;
bookCount = _bookCount;
memberCount = _memberCount;
booksBorrowed = _booksBorrowed;
}
The constructor will set up and initialize major variables defined in the smart contract before.
Functions
Lets define our first contract function borrowBook
function borrowBook(
address _address,
string memory _bookName,
uint256 _duration
) public {
require(
_duration > 7,
"You can not take the book for more than a week"
);
memberCount++;
borrowers.push(Borrow(_address, _bookName, block.timestamp, _duration));
booksBorrowed++;
bookCount--;
emit NewBorrowEvent(_address, _bookName, block.timestamp, _duration);
}
This function will be called when recording a new book borrowed, accepting the borrower's book address, the name of the book being borrowed and the duration of the borrow.
First, we have to ensure the that the borrower is not setting a duration period not longer than the required one that is 7 days. This is done using the require()
function.
We add a the entered information into the Borrow
struct, store and then push it to the borrowers
list using borrowers.push()
.
We then add a memberCount
, increase booksBorrowed
and deduct on the bookCount
number respectively.
We may want to see all the books in the library at some point. In order to enable this, we'll use the getAllBorrowers()
function to return borrowers
list.
function getAllBorrowers() public view returns (Borrow[] memory) {
return borrowers;
}
Here is our smart contract code so far.
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract Library {
address public owner;
uint256 public memberCount;
uint256 public bookCount;
uint256 public booksBorrowed;
event NewBorrowEvent(
address from,
string bookName,
uint256 pickupDate,
uint256 returnDate
);
// borrow list
Borrow[] borrowers;
// borrow mapping
mapping(address => Borrow) public borrower;
struct Borrow {
address from;
string bookName;
uint256 pickupDate;
uint256 duration;
}
constructor(
uint256 _bookCount,
uint256 _memberCount,
uint256 _booksBorrowed
) {
owner = msg.sender;
bookCount = _bookCount;
memberCount = _memberCount;
booksBorrowed = _booksBorrowed;
}
function borrowBook(
address _address,
string memory _bookName,
uint256 _duration
) public {
require(
_duration > 7,
"You can not take the book for more than a week"
);
memberCount++;
borrowers.push(Borrow(_address, _bookName, block.timestamp, _duration));
booksBorrowed++;
bookCount--;
emit NewBorrowEvent(_address, _bookName, block.timestamp, _duration);
}
function getAllBorrowers() public view returns (Borrow[] memory) {
return borrowers;
}
}
Feel free to extend it if you want and add more functions. I tried to keep it minimal and basic so that we can deploy it easier
Compling
Enter npx hardhat compile
in order to compile our smart contract
After compiling successfully you should get a similar message to this one
PS E:\Devstuff\Hashnode> npx hardhat compile
Compiled 2 Solidity files successfully
PS E:\Devstuff\Hashnode>
Hardhat configuration
In the hardhat.config.js
file, we need to pull the variables in the .env
file to help us set the hardhat configurations. By the help of dotenv package.
Let initialize the dotenv
file and import ALCHEMY_URL, PRIVATE_KEY
const dotenv = require('dotenv');
dotenv.config()
const { ALCHEMY_URL, PRIVATE_KEY } = process.env;
In module.exports
configure as below,
module.exports = {
solidity: "0.8.4",
defaultNetwork: 'ropsten',
networks: {
ropsten: {
url: ALCHEMY_URL,
accounts: [PRIVATE_KEY]
}
}
};
Scripts
Deploy script
Under the scripts
folder, create a new file called deploy.js
.
We'll use this script file to write the code that will deploy the smart contract to the block-chain
Create an async function called main
and export as shown below.
async function main() {
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
We do some error handling and checks when we call the main
function.
We'll need ethers
to deploy our contract, so we import it at the top of the file.
const { ethers } = require('hardhat');
Inside main
;
async function main() {
const Contract = await ethers.getContractFactory("Library");
const contract = await Contract.deploy(10, 0, 0);
await contract.deployed();
console.log("Contract deployed at: ", contract.address);
}
We create an instance of the contract using the ethers.getContractFactory()
passing in the name of the smart contract. In this case we called it Library
We use the contract.deploy()
passing in the number of books that the library has, the number of members that have borrowed books from the library, in this case which is 0
and the number of books borrowed which is also 0
because noone has borrowed the books yet.
All this information will be passed into the constructor and assigned to all their variables respectively
So far here is our deploy script
// Deploy script
const { ethers } = require('hardhat');
async function main() {
const Contract = await ethers.getContractFactory("Library");
const contract = await Contract.deploy(10, 0, 0);
await contract.deployed();
console.log("Contract deployed at: ", contract.address);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Deploying the smart contract
Now that our deploy script is ready, its time to deploy the contract to the network.
And to do this, we have to make sure that we have some eth in our wallet because deploying of the smart contract requires some amount of eth.
Fortunately, for development purposes, we do not have to use real money. We can set-up our wallet with fake ether.
Hed over to here and enter your wallet address to get some fake eth sent to your wallet for development purposes.
To deploy the contract, go to your terminal and type
npx hardhat run scripts/deploy.js --network ropsten
This will deploy the contract to the network on an address, as shown in the terminal output below.
PS E:\Devstuff\Hashnode> npx hardhat run scripts/deploy.js --network ropsten
Contract deployed at: 0x0c03451dFb338b570a1AF5F981C6dC2c7C393e28
Copy the contract's address and store it in the .env
file because we are going to use it.
Interacting with the contract
Here comes the fun part :) Now that our smart contract has been deployed to the network, its time to start interating with it. Interacting with the contract can be done in several different ways, for example, like using a front-end library like react, or vue. To keep this simple, we wont use any front-end library or framework, we'll just use a script to interact with the contract.
Under the \scripts
folder create a new file called interact.js
This is some important data that we need to use in this script to interact with the contract easily.
First we'll import ethers from hardhat, that provides us with alot of helper functions that we'll use to interact with the contract.
const { ethers } = require("hardhat");
Lets import our vars from .env
const { CONTRACT_ADDRESS, PRIVATE_KEY, ALCHEMY_API_KEY, ALCHEMY_URL } = process.env;
We need to import the contract's compiled ABI from artifacts
folder
// ABI
const contractABI = require('../artifacts/contracts/Library.sol/Library.json');
We also need a provider object from ethers.providers
// provider
const provider = new ethers.providers.JsonRpcProvider(ALCHEMY_URL);
Using the JsonRpcProvider()
we pass in the ALCHEMY_URL
from our alchemy app dashboard. This will create a provider object for us
Instead of alchemy, you can substitute the url with Infura ropesten url and pass it in the
JsonRpcProvider
and it will still work fine. I had issues with the alchemy network url and substituted it with infura but it still worked fine
Now that we've successfully created our provider
, we'll use the ether.Wallet
object to create our signer
while passing in the provider
and our wallet's PRIVATE_KEY
from .env
.
Here's how you do it;
// signer
const signer = new ethers.Wallet(PRIVATE_KEY, provider)
Finally, we need to create an instance of the contract, and by the help of the provider
, signer
and contractABI
, We'll be able to create it.
Here's how you do it;
// contract
const contract = new ethers.Contract(CONTRACT_ADDRESS, contractABI.abi, signer);
Using the ether.Contract
pasing in the CONTRACT_ADDRESS
, the contract's ABI that's extracted from the artifacts folder and the created signer
earlier, we'll get the contract's instance.
Here's our code so far;
const { ethers } = require("hardhat");
const { CONTRACT_ADDRESS, PRIVATE_KEY, ALCHEMY_API_KEY, ALCHEMY_URL } = process.env;
// ABI
const contractABI = require('../artifacts/contracts/Library.sol/Library.json');
// provider
const provider = new ethers.providers.JsonRpcProvider(ALCHEMY_URL);
// signer
const signer = new ethers.Wallet(PRIVATE_KEY, provider)
// contract
const contract = new ethers.Contract(CONTRACT_ADDRESS, contractABI.abi, signer);
Interaction
With the contract instance that we have, we now that it's functions to our disposal. We can now freely call them
Inside the main()
function, we'll start with returning the member count of the smart contract.
To do that just type;
const memberCount = await contract.memberCount();
console.log("count: ", memberCount);
Remember that in our smart contract, memberCount
was just a variable not a function. Well, in solidity, public
variables``` will be accessed by calling them like functions
We are able to access these functions, because we used the
public
key when declaring them in the smart contract. Onlypublic
variables will be accessed outside the smart contract.
Now that we have that set-up lets run the script and see the memberCount
of our contract.
In the terminal, type
npx hardhat run scripts/interact.js --network ropsten
Output:
PS E:\Devstuff\Hashnode> npx hardhat run scripts/interact.js --network ropsten
count: BigNumber { value: "0" }
We can see that the memberCount
is 0 as we set it up in the contract.
Now that we've confirmed that our contract is smooth, let try to borrow a book
To do this we need to call the borrowBook
const tx = await contract.borrowBook("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "The Lion and The Jewel", 5)
await tx.wait();
console.log("Mining.....");
const allBorrowers = await contract.getAllBorrowers();
console.log("All borrowers: ", allBorrowers);
To confirm that our borrow books record has been successfully recorded onto the blockchain, we'll use the getAllBorrowers
function from our contract that returns all the borrowers in the smart contract.
Let borrow a book called "The Lion and the Jewel" using an address for 5 days
To run the script, go to the terminal and type
npx hardhat run scripts/interact.js --network ropsten
Output;
PS E:\Devstuff\Hashnode> npx hardhat run scripts/interact.js --network ropsten
count: BigNumber { value: "0" }
Mining.....
All borrowers: [
[
'0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
'The Lion and The Jewel',
BigNumber { value: "1652980123" },
BigNumber { value: "10" },
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
bookName: 'The Lion and The Jewel',
pickupDate: BigNumber { value: "1652980123" },
duration: BigNumber { value: "10" }
],
[
'0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
'Every Witcher's Way',
BigNumber { value: "1652980123" },
BigNumber { value: "5" },
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
bookName: 'Every Witcher's Way',
pickupDate: BigNumber { value: "1652980123" },
duration: BigNumber { value: "5" }
],
[
'0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
'From Paris with Love',
BigNumber { value: "1653028078" },
BigNumber { value: "3" },
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
bookName: 'From Paris with Love',
pickupDate: BigNumber { value: "1653028078" },
duration: BigNumber { value: "3" }
]
]
count 2: BigNumber { value: "3" }
As you can tell, I've been testing the function with all three books recorded onto the blockchain
Feel free to extend the contract, maybe add payments :)