Build and deploy a library member registration book on the Ethereum Blockchain using Alchemy - Hardhat & Solidity

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 variable

  • bookCount: We want to keep tract of the number of unborrowed books books that we have in the library

  • booksBorrowed: 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 in string data type.
  • from: Address of the borrower borrowing the book

  • pickupDate: Record the time of borrowing the book

  • duration: 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. Only public 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 :)