Introduction
Hey there! we will explore the technical aspects of creating a smart contract on Celo, which aims to effectively handle medical records. The primary objective is to establish a secure and efficient system for storing and managing data, focusing on preserving privacy and security. Additionally, the contract incorporates access controls to ensure that only authorized individuals have the ability to view and interact with the medical records.
Moreover, this tutorial will offer insights into the use of Celo composer when building a project. It aims to provide a comprehensive overview of the step-to-step development process.
Let’s get started!
Prerequisite
Before we dive into this tutorial, you should have:
- Understanding on how to use a terminal
- Basic knowledge of blockchain concepts
- Familiarity with the Solidity
- Basic understanding of Celo Composer
Requirements
- Node js
- A code editor
- Celo Extension Wallet or Metamask
Setting up the Project
Celo Composer is a powerful tool that enables you to create and launch decentralized applications on the Celo blockchain with ease. Whether you’re an experienced developer or just getting started with blockchain technology, Celo Composer simplifies the process of building, deploying, and updating dApps on Celo.
With Celo Composer, You have access to a variety of ready-to-use frameworks and examples, which can help you get started on your project right away.
So to begin, let’s set up our project by running the following command on our terminal
npx @Celo_Academy/celo-composer create
The terminal would display prompts so you can choose your framework.
For this tutorial, we would be using react-celo and hardhat and then add the preferred name you chose for the project.
Now open your terminal and install the dependencies using the following command
Yarn
Still on your terminal, open your project on a code editor by running the command
code .
Note
- I’d recommend Visual Studio Code or Remix as code editors you can also check for others on the net.
- Please check out the README.md file in the Celo Composer repository to learn more about setting up your environment.
Write the smart contract
In this step, we will be creating the smart contract for the medical records but before we begin, let’s navigate to the contracts folder through the hardhat directory and create a new file
MedicalRecord.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MedicalRecord {
address private owner;
mapping(bytes32 => Record) private records;
mapping(address => bool) private authorizedUsers;
mapping(bytes32 => Restrictions) private accessRestrictions;
// SPDX-License-Identifier: MIT
specifies the license under which the code is released.
The pragma solidity ^0.8.0;
statement specifies the version of the Solidity compiler to be used which in our case would be 0.8.0 and above.
contract MedicalRecord
declares the name of the smart contract as MedicalRecord
address private owner;
declares a private variable owner of type address. It is used to store the address of the owner of the medical record contract.
The mapping(bytes32 => Record) private records;
line declares a private mapping named records that maps a bytes32 key to a struct Record
.
The mapping(address => bool) private authorizedUsers;
line declares a private mapping named authorizedUsers
that maps an address key to a bool
value. It is used to keep track of authorized users who have access to the medical records.
mapping(bytes32 => Restrictions) private accessRestrictions;is a private mapping named
accessRestrictionsthat maps a bytes32 key to a struct that will be called
Restrictions`.
struct Record {
bytes data;
uint256 timestamp;
}
struct Restrictions {
mapping(address => bool) authorizedUsers;
uint8 minRole;
uint256 expirationDate;
}
The Record
struct is used to store its properties. bytes data
which stores the actual data of the medical record and uint256 timestamp
which stores the time when the medical record was created.
The Restrictions
struct is used to manage access control and define additional constraints such as authorizedUsers
, minRole
, and expirationDate
for accessing the medical record
enum Roles {
Patient,
Doctor,
Administrator
}
mapping(address => Roles) private userRoles;
constructor() {
owner = msg.sender;
}
The Roles
enum provides a way to define the Patient
,Doctor
and Administrator
roles that can be assigned to users in the contract.
The userRoles
mapping is used to store the assigned roles for each user. It associates an address with a Roles
enum value
The constructor
sets the owner variable to the deploying address, which implies that the deployer of the contract is considered the initial owner
function authorizeUser(address user) public onlyOwner {
authorizedUsers[user] = true;
}
function removeAuthorization(address user) public onlyOwner {
authorizedUsers[user] = false;
}
function setUserRole(address user, Roles role) public onlyOwner {
userRoles[user] = role;
}
The authorizeUser
function allows the contract owner to authorize a user by setting their authorization status to true.
The removeAuthorization
function allows the contract owner to remove the authorization of a user by setting their authorization status to false.
The setUserRole
function allows the contract owner to set the role of a user by assigning a value from the Roles
enum to the specified user.
function getRecord(bytes32 key) public view onlyAuthorized returns (bytes memory) {
Record storage record = records[key];
Restrictions storage restrictions = accessRestrictions[key];
require(block.timestamp < restrictions.expirationDate, "This record has expired.");
require(uint8(userRoles[msg.sender]) >= restrictions.minRole, "Your role is not high enough to access this record.");
require(restrictions.authorizedUsers[msg.sender], "You are not authorized to access this record.");
return record.data;
}
The getRecord
function allows authorized users to retrieve specific medical records based on a given key. It verifies the expiration date, role requirements, and authorization status before returning the record data.
function setRecord(bytes32 key, bytes memory data, uint256 expirationDate, uint8 minRole, address[] memory _authorizedUsers) public onlyOwner {
Record storage record = records[key];
record.data = data;
record.timestamp = block.timestamp;
Restrictions storage restrictions = accessRestrictions[key];
restrictions.expirationDate = expirationDate;
restrictions.minRole = minRole;
for (uint i = 0; i < _authorizedUsers.length; i++) {
restrictions.authorizedUsers[_authorizedUsers[i]] = true;
}
}
The setRecord
function allows the contract owner to set or update a medical record by providing the key, data, expiration date, minimum role, and a list of authorized users. It updates the record’s data and timestamp, as well as the access restrictions associated with the record, including the expiration date, minimum role, and authorized users.
function deleteRecord(bytes32 key) public onlyOwner {
delete records[key];
delete accessRestrictions[key];
}
function getOwner() public view returns (address) {
return owner;
}
modifier onlyOwner {
require(msg.sender == owner, "Only the owner can perform this action.");
_;
}
modifier onlyAuthorized {
require(authorizedUsers[msg.sender], "You are not authorized to perform this action.");
_;
}
}
The deleteRecord
function allows the contract owner to delete a medical record and its associated access restrictions.This function allows the contract owner to delete a medical record identified by the given key
.
The getOwner
function provides the address of the contract owner.
The onlyOwner
modifier ensures that only the contract owner can execute specific functions by restricting access to functions that can only be executed by the contract owner.
The onlyAuthorized
modifier ensures that only authorized users can execute specific functions. It restricts access to functions that can only be executed by authorized users.
Now your final code should be looking like this
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MedicalRecord {
address private owner;
mapping(bytes32 => Record) private records;
mapping(address => bool) private authorizedUsers;
mapping(bytes32 => Restrictions) private accessRestrictions;
struct Record {
bytes data;
uint256 timestamp;
}
struct Restrictions {
mapping(address => bool) authorizedUsers;
uint8 minRole;
uint256 expirationDate;
}
enum Roles {
Patient,
Doctor,
Administrator
}
mapping(address => Roles) private userRoles;
constructor() {
owner = msg.sender;
}
function authorizeUser(address user) public onlyOwner {
authorizedUsers[user] = true;
}
function removeAuthorization(address user) public onlyOwner {
authorizedUsers[user] = false;
}
function setUserRole(address user, Roles role) public onlyOwner {
userRoles[user] = role;
}
function getRecord(bytes32 key) public view onlyAuthorized returns (bytes memory) {
Record storage record = records[key];
Restrictions storage restrictions = accessRestrictions[key];
require(block.timestamp < restrictions.expirationDate, "This record has expired.");
require(uint8(userRoles[msg.sender]) >= restrictions.minRole, "Your role is not high enough to access this record.");
require(restrictions.authorizedUsers[msg.sender], "You are not authorized to access this record.");
return record.data;
}
function setRecord(bytes32 key, bytes memory data, uint256 expirationDate, uint8 minRole, address[] memory _authorizedUsers) public onlyOwner {
Record storage record = records[key];
record.data = data;
record.timestamp = block.timestamp;
Restrictions storage restrictions = accessRestrictions[key];
restrictions.expirationDate = expirationDate;
restrictions.minRole = minRole;
for (uint i = 0; i < _authorizedUsers.length; i++) {
restrictions.authorizedUsers[_authorizedUsers[i]] = true;
}
}
function deleteRecord(bytes32 key) public onlyOwner {
delete records[key];
delete accessRestrictions[key];
}
function getOwner() public view returns (address) {
return owner;
}
modifier onlyOwner {
require(msg.sender == owner, "Only the owner can perform this action.");
_;
}
modifier onlyAuthorized {
require(authorizedUsers[msg.sender], "You are not authorized to perform this action.");
_;
}
}
Connect your wallet
Under the hardhat folder, create a .env
file and add the following command to it
MNEMONIC=""
PRIVATE_KEY=""
CELOSCAN_API_KEY=""
Add your private key of your celo testnet account on the metamask wallet. Please ensure you are using a test account. If you do not have one, you can download a metamask wallet, add celo network and then fund your wallet by requesting tokens on celo alfajores faucet.
Learn more:
- Check out 3 Simple Steps to Connect your MetaMask Wallet To Celo for more details.
Deploy the contract
Navigate to the deploy.js file in the deploy folder and paste the following
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy("MedicalRecord", {
from: deployer,
//args: ["hello world"],
log: true,
});
module.exports.tags = ["MedicalRecord"];
Deploy your smart contracts by running this command on your terminal
yarn deploy
If successful, you should be able to see it on the terminal as it first of all, compiles the smart contract and then deploys the contract to an address.
You can verify your contract was deployed by searching for the address on Alfajores.
Conclusion
In this tutorial, you have learnt how to set up your environment, write the smart contract, connect our wallet and effectively deployed our smart contract for managing Medical Records on Celo with Solidity using Celo Composer.