This repository contains the source code of a dApp designed for this HKUST blockchain course project (Spring 2023).
Contributors (in alphabetical order):
Note that this course is actually MSBD5017, an MSc Big Data Technology masters level course, in disguise.
The goal of this project is to create a decentralized Accreditation system for different organizations (Issuers) to issue Certificates to Applicants.
The specific objectives are as follows:
- To allow Accreditation Issuers to mint certificate NFTs for applicants
- To allow Issuers to view (and alter) status of Certificates and Accreditations
- To allow outsiders (e.g. potential employers) to check Certificates obtained by each Applicant
- To provide a UI for such interactions
Note that all Certificates and Accreditations issued are public (obviously because it's on a blockchain, duh).
This accreditation system is rather complex for a beginner, and many terms seemingly convey similar concepts. As such, the terminology used in this project shall be explained in this section:
- Issuer: An organization that defines and announces Accreditations, and issues Certificates to Applicants
- Applicant: An individual/entity that applies for an Accreditation and can receive Certificates from Issuers
- Accreditation: a type of qualification that Applicants can apply for, e.g. public examinations such as HKDSE/IB/GCE and product quality standards such as ISO-XXXXX, an NFT is minted with the Issuer as owner
- Certificate: issued when an Applicant successfully qualifies for an Accreditation, equivalent to certificates issued on paper, e.g. public examination transcript and certificate of a product achieving a quality standard, an NFT is minted with the Applicant as owner
The scope of this project is developing a simple yet production-ready dApp that consist of smart contracts deployed to a provided test blockchain network and a UI working on a web browser with MetaMask extension activated. All blockchain interactions shall be conducted via MetaMask.
The entire project is split into 3 parts: Smart Contract Programming, Front-end Interaction, and Final Group Presentation. Our team recommends that readers examine project code by following the order described in this documentation.
Steps of project setup are detailed in the Instructions section.
In this part, the open-source Hardhat framework is adopted due to its production-readiness and flexibility.
The directory for this section is contract/
.
This hardhat project was initialized with the command npx hardhat
.
The Solidity language is used to draft smart contracts, due to its widespread popularity and availability of powerful open-source libraries such as base class contracts and tokens developed by OpenZeppelin.
The directory for this subsection is contracts/
.
The smart contracts in this project adopt a modular design, meaning that each contract shall only serve one specific purpose, making it more scalable and easier to maintain and debug. Each "microservice" apart from the endpoint contracts have implemented access control such that only explicitly authorized contract addresses can call functions from and interact with each contract.
Types are defined in and imported from the contracts/types/
directory. 4 types are defined and used both internally, between contracts, and externally:
An additional type CompleteCert
has been implemented to facilitate end users to obtain complete Certificate information, along with the corresponding Accreditation info. Note that this type is only used to return data to end users, and is not used within Contracts.
Storage of data is a vital part of the application. The data management system formed by the following 4 storage Contracts acts as a database. These Contracts should only return data if the requesting address is an authorized Contract address.
Each internally used type has its own Storage system:
The defined data structure is designed to mirror relational database paradigm to reduce storage costs and maintain data integrity.
Since every Certificate and Accreditation is issued or established in form of an NFT, the NFT Contracts will be implemented based on the ERC721 token standard. Specifically, 2 smart Contracts will inherit the ERC721 token implemented by OpenZeppelin:
Note that despite both Certificates and Accreditations have NFT issued to their corresponding owners to signify ownership, Certificate NFTs are much more significant than Accreditation NFTs because the degree of accountability required for Certificate owners and handlers is much higher.
To clearly segregate the responsibilities of each Contract, each Contract should only have one functionality. Hence, an endpoint Contract is created for each major use case. Functions in Contracts documented in above sections will not be callable by any addresses other than the explicitly authorized Contracts addresses. Instead, functions in these endpoint Contracts will serve as API endpoints or entry points to our system. The endpoint Contracts are as follows:
After writing the smart Contracts, the Contracts can be compiled to EVM bytecode using the command npm start
. The unit test command npm test
, coverage command npm run coverage
and deployment command npm run deploy:<insert environment>
will also compile the Contracts before performing the corresponding tasks.
After compilation, 3 directories contract/artifacts/
, contract/cache/
and frontend/src/types/typechain-types/
will be automatically generated. Do not manually modify these files.
TypeScript is used for unit testing due to compatibility with hardhat and the fact that similar code can be preliminarily tested and later reused for front-end development.
The directory for this subsection is test/
.
Unit tests are written for each Storage, NFT, and Endpoint Contract, testing the correctness of functions defined in the smart Contracts.
After writing the unit tests, they can be executed using the command npm test
. A complete log of the testing process will be available in the terminal.
Alternatively, instead of directly running unit tests, coverage can be obtained with the command npm run coverage
, and the generated files can be removed with the command npm run clean:coverage
. Note that the course TA expects >80% coverage for our project.
Following the test-driven development pattern, test cases are written alongside Contract implementation to make sure of Contract feature correctness.
A deployment script to a blockchain network is available at /scripts/deploy.ts
. For the purposes of our project, there are 3 possible networks to deploy to, each with description and corresponding commands below:
etherdata
: The test net provided by the course TA. Expected to be the "Production" environment for presentation demo of the group project- Deployment command:
npm run deploy:prod
- Deployment command:
polygon
: Since it was difficult to request for tokens on the providedetherdata
network, our group searched for another test net to perform development on, and we found that Polygon Mumbai provides such a service- Deployment command:
npm run deploy:testnet
- Deployment command:
local
: A local test net created using Ganache, a GUI software that can create one-click blockchain networks- Deployment command:
npm run deploy:local
- Deployment command:
Network details are configured in /hardhat.config.ts
. See Instructions section for more details.
In case of errors or overwritten contracts, clean up of compiled files can be performed by the command npm run clean:build
.
During deployment, logs are generated, printed to console and saved into a log file at /scripts/logs/
. The command npm run clean:logs
can be used to delete all log files.
A very simple "checking" script at /scripts/check.ts
is created for easy and rudimentary testing on whether or not the smart contracts can be called and interacted with correctly from clients. The script simulates a simple live demo by calling Endpoint contract functions, and can be used as a seeding script with further fine-tuning. The command npm run check:<insert environment>
can be used to execute this script.
The command npm run clean
can be used to clean up all generated files.
This Next.js project with TypeScript is initialized with the command npx create-next-app@latest
, and is styled with the Material UI package.
The page contains several pages for registering, launching and viewing accreditations, and issuing and viewing certificates. The typical workflow is as follows:
- Issuer:
- Connect to MetaMask to login
- Register as an issuer
- Launch an accreditation
- Issue a certificate to an applicant
- Applicant:
- Connect to MetaMask to login
- Register as an applicant
- View certificates that he/she has received
- Outsider (Guest):
- Search an accreditation, issuer, certificate, or applicant
An API endpoint is also designed to dynamically generate certificate image given a certificate ID.
The deliverables include a set of presentation slides, a live group presentation where each member has 5 minutes, and a final project report. The materials have been uploaded to presentation/
.
README.md
: documentation for this course projectcontract/
: directory that stores a hardhat project with smart contracts written in Soliditycontracts/
: directory that stores a Solidity smart contract source filestypes/
: directory that stores Solidity types used in the projectnft/
: directory that stores Solidity types for NFT entitiesAccreditation.sol
: Accreditation type definitionCertificate.sol
: Certificate type definition
users/
: directory that stores Solidity types for usersIssuer.sol
: Issuer type definitionApplicant.sol
: Applicant type definition
CompleteCert.sol
: a helper type that contains entire details for a certificate
storage/
: directory that stores storage contractsnft/
: directory that stores NFT entities dataAccreditationStorage.sol
: stores Accreditation dataCertificateStorage.sol
: stores Certificate data
users/
: directory that stores users dataIssuerStorage.sol
: stores Issuer dataApplicantStorage.sol
: stores Applicant data
nft/
: directory that stores NFT contractsAccreditationNFT.sol
: Accreditation NFT contract compliant with the ERC721 standardCertificateNFT.sol
: Certificate NFT contract compliant with the ERC721 standard
endpoints/
: directory that stores endpoint contractsIssuerEndpoint.sol
: endpoint contract containing Issuer functionsApplicantEndpoint.sol
: endpoint contract containing Applicant functionsAccreditationEndpoint.sol
: endpoint contract containing Accreditation functionsCertificateEndpoint.sol
: endpoint contract containing Certificate functions
scripts/
: directory that stores deployment and other miscellaneous scriptsdeploy.ts
: script for deployment of contracts incontracts/
to blockchaincheck.ts
: script to simulate a live demo, can be used for seeding purposes
test/
: directory that stores unit tests (each test file is one-to-one for each contract incontracts/
, with the exception of type definition files incontracts/types/
).env
: stores sensitive environment variables (not committed to GitHub)- Copy from
.env.example
and add variable values
- Copy from
hardhat.config.ts
: stores environment settings and loads environment variables from.env
forhardhat
to use
frontend/
: directory that stores a Next.js project containing a simple UI to interact with deployed contracts fromcontract/
directorysrc/
: directory that stores source code for the projectcomponents/
: directory that stores reusable component designspages/
: directory that stores pages available in this Next.js appblockchain/
: directory that stores config data related to blockchain network and smart contractabi/
: directory that stores ABI files copied fromcontract/
IssuerEndpoint.json
: ABI for IssuerEndpointApplicantEndpoint.json
: ABI for ApplicantEndpointAccreditationEndpoint.json
: ABI for AccreditationEndpointCertificateEndpoint.json
: ABI for CertificateEndpoint
contracts.config.ts
: stores blockchain name, RPC, deployed contract addresses
contexts/
: directory that stores React contextsapp.tsx
: context that saves user status
hooks/
: directory that stores React hooksuseMetaMask/
: directory that stores the useMetaMask hookindex.ts
: implementation of the useMetaMask hook. Essentially all interactions with the MetaMask browser extension and the blockchain are implemented here, only exposing high level descriptive function names to the frontend developers
theme/
: directory that stores MUI theme definitionmuiTheme.ts
: defines the Material UI theme for this project
types/
: directory that stores types used in front-endglobal.d.ts
: defines ethereum in Window interfaceindex.ts
: defines types and enums used in the project
utils/
: directory that stores utility functionsgenerateCertificate.ts
: extracts the dynamic certificate generation process
public/
: directory that stores statically served filesimg/
: directory that stores images used in the projectcertificate_template.png
: certificate template imagecertificate_template_smaller.png
: same certificate template image, but of smaller dimensioncertificate_template_even_smaller.png
: same certificate template image, but of even smaller dimension. This version is currently used to in dynamic certificate generation due to size limitation of Vercel deployment
presentation/
: directory that stores presentation slides & supplementary materials (if any) for the presentation, as well as the final reportISOM3000H_Presentation.pptx
: PPT slides used in the final presentation
-
Install dependencies and other setup scripts:
cd contract && npm install && npm start cd ../frontend && npm install
-
In
contract/
:-
Set up the provided etherdata network in MetaMask browser extension:
- Add the network on MetaMask according to the instructions in this tutorial
- Obtain the account private key and place it in
contract/.env
file in the formatPROD_PK=0x<Obtained Private Key>
Note: Remember to add
0x
in front of the private key
-
[Optional] Set up a test network in MetaMask
- Add the network on MetaMask by scrolling to the bottom, finding the "Add Mumbai Network" button at the bottom right corner and clicking it
- Obtain the account private key and place it in
contract/.env
file in the formatTEST_PK=0x<Obtained Private Key>
- To obtain test tokens, visit Polygon Faucet.
-
[Optional] Set up a local test network on Ganache
- Create a quick local network on Ganache
- Obtain an account private key and place it in
contract/.env
file in the formatLOCAL_PK=0x<Obtained Private Key>
- If the port number on Ganache is inconsistent with the default in the
contract/.env
file, replace the field with the number shown on Ganache
-
In
/hardhat.config.ts
, set up the target deployment network:const config: HardhatUserConfig = { solidity: "0.8.9", networks: { etherdata: { url: "http://rpc.debugchain.net", accounts: process.env.PROD_PK !== undefined ? [process.env.PROD_PK] : [], }, }, };
etherdata
is the alias of the network to be used in the deployment command (see next step)- Can be renamed (preferrably to your network name)
- Other networks are set up similarly
url
field stores the RPC endpointaccounts
field scrapes the account private key from the.env
file and passes it as part of the deployment information- More networks are configured for project use
-
In
/package.json
, configure the deployment command:"scripts": { "deploy:prod": "hardhat run scripts/deploy.ts --network etherdata", "deploy:testnet": "hardhat run scripts/deploy.ts --network polygon", "deploy:local": "hardhat run scripts/deploy.ts --network local" }
- As mentioned in the previous step,
etherdata
is the network alias configured in/hardhat.config.ts
- For our project, the test networks
polygon
andlocal
are also configured - To deploy to
etherdata
, usenpm run deploy:prod
- To deploy to
polygon
, usenpm run deploy:testnet
- To deploy to
local
, usenpm run deploy:local
- As mentioned in the previous step,
-
In
/scripts/deploy.ts
, configure the smart contracts to be deployed:const Contract = await ethers.getContractFactory("ContractName"); const contract = await Contract.deploy(); await contract.deployed(); console.log("Contract deployed to:", contract.address);
- The
ethers.getContractFactory()
method takes a string argument of the contract name (i.e. the title of a Solidity contract)- e.g. if a
SampleContract.sol
containingcontract SampleContract {}
is to be deployed, pass"SampleContract"
into this method
- e.g. if a
- Add the names of all contracts to be deployed to this file
- If a contract A calls functions from another contract B, first deploy contract B, and pass address of contract B as an argument when deploying contract A with the .deploy() function
- On Solidity, add contract B address as a parameter in constructor of contract A
- If contracts are codependent, create another setup function in contract A to store deployed address of contract B
- The
-
Deploy the smart contracts to a configured network using one of the npm commands specified in Step 5
-
After deployment, the smart contract addresses will be printed in the terminal output, and in a log file at
/scripts/logs/
. Contract addresses can be obtained for further use- Note that the logs will not be committed to GitHub
-
Details of deployed contracts can be viewed on:
etherdata
: this web UIpolygon
: this web UIlocal
: Ganache UI
-
-
In
frontend/
:- Fill in the blockchain network and deployed contract information in
contracts.config.ts
- Copy the abi JSON files for Endpoint contracts from
contract/artitacts/contracts/*
tofrontend/src/blockchain/abi/
- Start the frontend by the command
npm run dev
ornpm start
and go to the dev URL
- Fill in the blockchain network and deployed contract information in