Building A Decentralised Judicial Archiving System

Building A Decentralised Judicial Archiving System https://celo.academy/uploads/default/optimized/2X/d/d5eb24ccb8cc66b0b3f1eb9b7338c2a627425b6c_2_1024x576.png
none 5.0 1

Introduction​

A decentralized judicial archiving system is a type of blockchain-based system that enables the secure and transparent storage of legal documents and records. By utilizing blockchain technology, the system provides a tamper-proof and decentralized database that can be accessed by authorized parties from anywhere in the world. The decentralized nature of the system ensures that all records are stored in a distributed network of nodes, preventing data loss or manipulation. With a decentralized judicial archiving system, legal professionals, judges, and other authorized parties can access legal documents quickly and easily, increasing efficiency and transparency in the judicial process. Additionally, the system provides a secure and transparent method of storing and sharing sensitive legal information, reducing the risk of fraud and corruption. In this tutorial we shall be looking at code examples and a step by step guide to setting up a simple archiving system.

Prerequisite

  • Node.js should be installed along with a node package manager(npm)
  • A basic understanding of Celo and smart contracts
  • A Celo Wallet address:
  • To create Alfajores wallet, go to Alfajores wallet
  • To get testnest funds, go to Celo faucet
  • A text editor such as Visual Studio Code installed on your computer
  • Remix IDE
  • Metamask
  • A Pinata account

You can clone this project on github by running:

git clone https://github.com/emiridbest/Judicial_Archiving_System.git

Let’s Begin…

This is what our Dapp will look like:

  • Step 1: Write your Smart Contract and Deploy on Remix IDE
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Library {    struct document {        uint256 id;        string title;        string pinataCID; // Pinata CID for the PDF file        uint256 timestamp;    }    enum Role {        Owner,        Admin,        licencedUser    }    modifier onlyAdmin() {        require(            roles[msg.sender] == uint256(Role.Admin),            "Only Admin can call this."        );        _;    }    modifier onlyOwner() {        require(            roles[msg.sender] == uint256(Role.Owner),            "Only Owner can call this."        );        _;    }    mapping(address => uint256) public roles;    mapping(address => uint256) public userCount;    document[] private documentList;    mapping(uint => address) public owner;    uint256 public documentCount;    event DocumentAdded(uint256 id, string title, string pinataCID, uint256 timestamp);    event RoleAssigned(address user, string role);    function assignRole(address user, string memory role) public onlyOwner {        if (keccak256(abi.encodePacked(role)) == keccak256(abi.encodePacked("Admin"))) {            roles[user] = uint256(Role.Admin);        } else if (keccak256(abi.encodePacked(role)) == keccak256(abi.encodePacked("licencedUser"))) {            roles[user] = uint256(Role.licencedUser);        } else {            revert("Invalid role");        }        emit RoleAssigned(user, role);    }    function addDocument(string memory title, string memory pinataCID, uint256 timestamp) public onlyAdmin {        uint256 id = documentList.length;        documentList.push(document(id, title, pinataCID, timestamp));        owner[id] = msg.sender;        emit DocumentAdded(id, title, pinataCID, timestamp);    }    function getAllDocuments() public view returns (document[] memory) {        return documentList;    }    function getDocument(uint256 id) public view returns (document memory) {        return documentList[id];    }}

Now, we compile this contract then deploy on Injected web3 provider. This pops up our already install metamask wallet, make sure to choose Alfajores containing wallet. On deploying, a link to Alfajores Explorer pops up at the top of your browser. You can now copy out your contract address and save it somewhere as web3.js needs this address to interact with this particular contract. Also, go back to remix and copy out you contract ABI save it somewhere.

Now Let’s code the frontend:

  • Step 2: Set up your new react project.

Run this on your terminal

npx create-react-app legal

This should take a few minutes to complete depending on your internet connection. Navigate to the directory created Open the directory using your code editor. In this case, it would be vscode and you would run this on your terminal;

Code legal

This should open a vscode window with the legal directory.

  • Step 3: Update package.json file.

Now, update the content of the package.json by copying and pasting this;

{  "name": "legal",  "version": "0.1.0",  "private": true,  "dependencies": {    "@celo-tools/use-contractkit": "^3.1.0",    "@celo/contractkit": "^1.5.1",    "@testing-library/jest-dom": "^5.16.1",    "@testing-library/react": "^12.1.2",    "@testing-library/user-event": "^13.5.0",    "@web3uikit/core": "^0.2.45",    "@web3uikit/web3": "^0.2.2",    "react": "^18.2.0",    "react-bootstrap": "^2.5.0",    "react-dom": "^18.2.0",    "react-error-overlay": "6.0.9",    "react-scripts": "4.0.3",    "web-vitals": "^2.1.4",    "web3": "^1.7.0",    "web3uikit": "0.1.159"  },  "scripts": {    "start": "react-scripts start",    "build": "react-scripts build",    "test": "react-scripts test",    "eject": "react-scripts eject"  },  "eslintConfig": {    "extends": ["react-app", "react-app/jest"]  },  "browserslist": {    "production": [">0.2%", "not dead", "not op_mini all"],    "development": [      "last 1 chrome version",      "last 1 firefox version",      "last 1 safari version"    ]  },  "resolutions": {    "react-error-overlay": "6.0.9"  }}

Step 4: Install all dependencies

npm install

Step 5: Start development server

npm start

Update the App.js file

This is the file that will contain codes that interact with our smart contract and also house all the components of the frontend. The App.js file should look like this:

import React, { useState, useEffect, useCallback } from "react";import Web3 from "web3";import { newKitFromWeb3 } from "@celo/contractkit";import "./App.css";import { contractABI, contractAddress } from "./utils/const";import {  AddDocument,  Archive,  AssignRole,  Navbar,  Welcome,} from "./components/Index";import "./App.css";function App() {  const [currentAccount, setCurrentAccount] = useState("");  const [contract, setContractInstance] = useState(null);  const [kit, setKit] = useState(null);  const [formData, setFormData] = useState({ address: "", role: "" });  const [role, setRole] = useState("");  const [documents, setDocuments] = useState([]);  const initContract = useCallback(async () => {    try {      if (!window.ethereum) {        console.error("Celo Wallet extension not detected");        return;      }      const web3 = new Web3(window.ethereum);      const kit = newKitFromWeb3(web3);      await window.ethereum.request({ method: "eth_requestAccounts" });      const contract = new kit.web3.eth.Contract(contractABI, contractAddress);      setCurrentAccount((await kit.web3.eth.getAccounts())[0]);      setKit(kit);      setContractInstance(contract);    } catch (error) {      console.log(error);    }  }, [contractAddress]);  useEffect(() => {    if (currentAccount) {      initContract();    }  }, [currentAccount, initContract]);  //connect wallet  async function connectToWallet() {    try {      if (!window.ethereum) throw new Error("Wallet extension not detected");      await window.ethereum.request({ method: "eth_requestAccounts" });      const accounts = await window.ethereum.request({        method: "eth_requestAccounts",      });      setCurrentAccount(accounts[0]);    } catch (error) {      console.log(error);      throw new Error("Please install Celo Wallet extension");    }  }  const handleChange = (e, name) => {    setFormData((prevState) => ({ ...prevState, [name]: e.target.value }));  };  //assign role  async function assignRole(address, role) {    try {      await contract.methods        .assignRole(address, role)        .send({ from: currentAccount, gasLimit: 3000000 });      setRole(role);      alert(`Role assigned to ${address} successfully!`);    } catch (error) {      console.error(error);    }  }  //add document  async function addDocument(title, pinataCID) {    try {      const timestamp = Math.floor(Date.now() / 1000);      const tx = await contract.methods        .addDocument(title, pinataCID, timestamp)        .send({ from: currentAccount });      console.log(tx);    } catch (error) {      console.error("Error details:", error);    }  }  //get document count  async function getDocumentCount() {    try {      const count = await contract.methods.getDocumentCount().call();      console.log(count);    } catch (error) {      console.log(error);    }  }  //get document  async function getDocument(id) {    try {      const document = await contract.methods.getDocument(id).call();      console.log(document);      const url = "https://ipfs.io/ipfs/" + document.pinataCID;      window.open(url, "_blank");    } catch (error) {      console.log(error);    }  }  //get documents  async function getAllDocuments() {    try {      const documents = await contract.methods.getAllDocuments().call();      console.log(documents);      setDocuments(documents); // Update the state here    } catch (error) {      console.log(error);    }  }  return (    <div>      <div>        <Navbar />      </div>      <div className="">        <Welcome          connectToWallet={connectToWallet}          currentAccount={currentAccount}        />      </div>      <div className="components">        <AssignRole          formData={formData}          assignRole={assignRole}          handleChange={handleChange}        />        <AddDocument          formData={formData}          addDocument={addDocument}          handleChange={handleChange}        />        <Archive          documents={documents}          getDocumentCount={getDocumentCount}          getAllDocuments={getAllDocuments}          getDocument={getDocument}        />      </div>    </div>  );}export default App;

Now let’s break this down;

  • Step 6: Import all dependencies
import React, { useState, useEffect, useCallback } from "react";import Web3 from "web3";import { newKitFromWeb3 } from "@celo/contractkit";import "./App.css";import { contractABI, contractAddress } from "./utils/const";import {  AddDocument,  Archive,  AssignRole,  Navbar,  Welcome,} from "./components/Index";import "./App.css";...

Here, we imported all the dependencies and then also imported our contractABI and contractAddress which we save earlier when we deployed our contract. This serves as the link between our frontend and the already deployed contract.

We also imported the react components which we will be seeing more of in this tutorial.

  • Step 7: Let’s use useState to keep track of some variables throughout the dapp.
...  const [currentAccount, setCurrentAccount] = useState("");  const [contract, setContractInstance] = useState(null);  const [kit, setKit] = useState(null);  const [formData, setFormData] = useState({ address: "", role: "" });  const [role, setRole] = useState("");  const [documents, setDocuments] = useState([]);  ...
  • Step 8: Create functions for connecting to the network
...const initContract = useCallback(async () => {    try {      if (!window.ethereum) {        console.error("Celo Wallet extension not detected");        return;      }      const web3 = new Web3(window.ethereum);      const kit = newKitFromWeb3(web3);      await window.ethereum.request({ method: "eth_requestAccounts" });      const contract = new kit.web3.eth.Contract(contractABI, contractAddress);      setCurrentAccount((await kit.web3.eth.getAccounts())[0]);      setKit(kit);      setContractInstance(contract);    } catch (error) {      console.log(error);    }  }, [contractAddress]);  useEffect(() => {    if (currentAccount) {      initContract();    }  }, [currentAccount, initContract]);  //connect wallet  async function connectToWallet() {    try {      if (!window.ethereum) throw new Error("Wallet extension not detected");      await window.ethereum.request({ method: "eth_requestAccounts" });      const accounts = await window.ethereum.request({        method: "eth_requestAccounts",      });      setCurrentAccount(accounts[0]);    } catch (error) {      console.log(error);      throw new Error("Please install Celo Wallet extension");    }  }...

The initContract is a function that initializes a contract instance and sets the currentAccount and kit state variables. It uses the useCallback hook to memoize the function and the async/await syntax to handle asynchronous calls to the Celo blockchain. initContract takes no arguments, but it depends on the contractAddress state variable.

connectToWallet is a function that connects to the Celo Wallet extension using the window.ethereum object. It sets the currentAccount state variable if the connection is successful. If the extension is not detected or if an error occurs, an error message is logged and an error is thrown.

  • Step 9: Create functions to call our smart contract functions

The assignRole function

 async function assignRole(address, role) {    try {      await contract.methods        .assignRole(address, role)        .send({ from: currentAccount, gasLimit: 3000000 });      setRole(role);      alert(`Role assigned to ${address} successfully!`);    } catch (error) {      console.error(error);    }  }  ...

It takes two inputs: address and role.

The function asks the blockchain to update the user’s role, using the assignRole command.

The addDocument function

... async function addDocument(title, pinataCID) {    try {      const timestamp = Math.floor(Date.now() / 1000);      const tx = await contract.methods        .addDocument(title, pinataCID, timestamp)        .send({ from: currentAccount });      console.log(tx);    } catch (error) {      console.error("Error details:", error);    }  }...

This function takes two inputs: title and pinataCIDfor storing files on IPFS).

It generates a timestamp representing the current time.

Then, it sends a transaction to the blockchain using a contract method named addDocument. This transaction requests the blockchain to add the provided document details.

  • Other Functions
...  //get document count  async function getDocumentCount() {    try {      const count = await contract.methods.getDocumentCount().call();      console.log(count);    } catch (error) {      console.log(error);    }  }  //get a document  async function getDocument(id) {    try {      const document = await contract.methods.getDocument(id).call();      console.log(document);      const url = "https://ipfs.io/ipfs/" + document.pinataCID;      window.open(url, "_blank");    } catch (error) {      console.log(error);    }  }  //get a list of all the documents  async function getAllDocuments() {    try {      const documents = await contract.methods.getAllDocuments().call();      console.log(documents);      setDocuments(documents); // Update the state here    } catch (error) {      console.log(error);    }  }  ...
  • Step 10: Renders a user interface with various sub-components
... return (    <div>      <div>        <Navbar />      </div>      <div className="">        <Welcome          connectToWallet={connectToWallet}          currentAccount={currentAccount}        />      </div>      <div className="components">        <AssignRole          formData={formData}          assignRole={assignRole}          handleChange={handleChange}        />        <AddDocument          formData={formData}          addDocument={addDocument}          handleChange={handleChange}        />        <Archive          documents={documents}          getDocumentCount={getDocumentCount}          getAllDocuments={getAllDocuments}          getDocument={getDocument}        />      </div>    </div>  );}export default App;
  • Step 11: Create the Components and the import them into App.js. To do this we have to create a new folder called components in the src folder.

AssignRole.js

import React from "react";const Input = ({ placeholder, name, type, handleChange }) => (  <input    placeholder={placeholder}    type={type}    name={name} // Add the 'name' attribute here    onChange={(e) => handleChange(e, name)}    className=""  />);const AssignRole = ({ formData, assignRole, handleChange }) => {  const handleSubmit = (e) => {    const { address, role } = formData;    e.preventDefault();    if (!address || !role) return;    console.log("Submitting:", formData); // Add a console log here to see the formData    assignRole(address, role);  };  return (    <div className="add">      <label className="">Roles</label>      <div className="">        <Input          placeholder="Address"          name="address"          type="text"          handleChange={handleChange}        />        <select          name="role"          onChange={(e) => handleChange(e, "role")}          className="role"        >          <option value="Admin">Admin</option>          <option value="licencedUser">licencedUser</option>        </select>      </div>      <div />      <button type="button" onClick={handleSubmit}>        Assign Role      </button>    </div>  );};export default AssignRole;

AddDocument.js

import React, { useState } from "react";const pinataApiKey = process.env.REACT_APP_PINATA_API_KEY;const pinataSecretApiKey = process.env.REACT_APP_PINATA_API_SECRET;const Input = ({ placeholder, name, type, value, handleChange, id }) => (  <input    id={id}    name={name}    placeholder={placeholder}    type={type}    value={value}    onChange={(e) => handleChange(e)}    className=""  />);const AddDocument = ({ addDocument }) => {  const [title, setTitle] = useState("");  const [pinataCid, setPinataCid] = useState("");  const handleChange = (e) => {    const { name, value } = e.target;    switch (name) {      case "title":        setTitle(value);        break;      default:        console.error(`Invalid input field name: ${name}`);    }  };  const handleFileChange = async (event) => {    const file = event.target.files[0];    const formData = new FormData();    formData.append("file", file);    formData.append("pinataMetadata", JSON.stringify({ name: file.name }));    formData.append("pinataOptions", JSON.stringify({ cidVersion: 0 }));    const response = await fetch(      "https://api.pinata.cloud/pinning/pinFileToIPFS",      {        method: "POST",        headers: {          pinata_api_key: pinataApiKey,          pinata_secret_api_key: pinataSecretApiKey,        },        body: formData,      }    );    if (response.ok) {      const result = await response.json();      console.log("File added to IPFS with CID:", result.IpfsHash);      setPinataCid(result.IpfsHash);    } else {      console.error("Error uploading file to IPFS:", response.status);    }  };  const handleSubmit = (e) => {    e.preventDefault();    if (!title || !pinataCid) return;    addDocument(title, pinataCid);  };  return (    <div className="add">      <label className="">Add Document</label>      <div className="">        <Input          placeholder="Documment Title"          name="title"          type="text"          value={title}          handleChange={handleChange}        />        <input          placeholder="pdf here"          type="file"          id="pdfFileInput"          accept="application/pdf"          onChange={handleFileChange}          className="white-glassmorphism"        />      </div>      <div />      <button type="button" onClick={handleSubmit}>        Add Document      </button>    </div>  );};export default AddDocument;

Archive.js

import React from "react";const Archive = ({ documents, getAllDocuments, getDocument }) => {  const handleGetDocuments = async () => {    try {      await getAllDocuments();    } catch (error) {      console.error("Error retrieving document list:", error);    }  };  return (    <div className="docs">      <h2>Document List</h2>      <button onClick={handleGetDocuments}>Refresh List</button>      <table>        <thead>          <tr>            <th>Document ID</th>            <th>Title</th>            <th>Document Identifier</th>            <th>Time Modified</th>          </tr>        </thead>        <tbody>          {documents &&            Array.isArray(documents) &&            documents.map((document, index) => (              <tr key={index}>                <td>{document.id}</td>                <td>{document.title}</td>                <td>{document.pinataCID}</td>                <td>{new Date(document.timestamp * 1000).toLocaleString()}</td>                <td>                  <button onClick={() => getDocument(document.id)}>                    Retrieve document                  </button>                </td>              </tr>            ))}        </tbody>      </table>    </div>  );};export default Archive;

Welcome.js

import React from "react";const Welcome = ({ connectToWallet, currentAccount }) => {  return (    <div>      <div className="welcome">        <h1 className="">          Retrieve all the Documents <br />          You Ever Wanted        </h1>        <p className="">Explore The World Of Decentralized Judicial Archives</p>      </div>      <div className="connect">        {!currentAccount && (          <button type="button" onClick={connectToWallet}>            <p className="button"> Connect Wallet</p>          </button>        )}      </div>    </div>  );};export default Welcome;

Navbar.js

import React from "react";import logo from "./logo512.png";const NavbarItem = ({ title, classprops }) => <li>{title}</li>;const Navbar = () => {  return (    <div className="nav">      <nav className="navbar">        <ul>          <img src={logo} alt="logo" className="logo" />          {["About Us", "Mission", "FAQ", "Contact"].map((item, index) => (            <NavbarItem key={item + index} title={item} />          ))}        </ul>      </nav>    </div>  );};export default Navbar;

Now, let’s bundle all the components into one file in the component folder so we can batch export them as one;

Index.js

export { default as Welcome } from "./Welcome";export { default as Navbar } from "./Navbar";export { default as AssignRole } from "./AssignRole";export { default as AddDocument } from "./AddDocument";export { default as Archive } from "./Archive";

There you go, our Dapp is ready.

Here is Source Code

Conclusion​

In conclusion, this tutorial has shown how to use blockchain to implement a judicial archiving system. Mastering these skills will help you become adept at building decentralized applications, making the most of blockchain’s security, transparency and reliability.

Next Steps​

  • One possible next step for the platform could be to introduce a subscription model or payable functions that would allow users to pay for retrieving documents.

About the Author​

Emiri Udogwu, a licensed medical doctor with a burning passion for technology and gifted with skills for spreading knowledge and propagating ideas. A web3 and frontend developer.

References​

3 Likes

This is insightful, welldone brother

2 Likes

We keep trying our best brother, massive respect sir.

3 Likes

Nice content my boss

5 Likes

Your content is valuable for developers looking to explore Celo, solidity, dApp development, and blockchain-based solutions in the legal domain. Kudos to the writer for a job well done!

2 Likes

Thanks @jimohyusufolatunji4
We keep trying to get our ideas out as clear as we can

3 Likes

Thanks brother. I enjoy practical approaches in the use of blockchain.

3 Likes

Great project. Love to see people using blockchain technology to seolve real world problems

1 Like

Hi @EmiriDbest,

This is a very good post. To buttress your points, I rewrote the contracts to show some acceptable standards and conventions when writing smart contracts using solidity. I used inline comments to denote the changes. Arrangements of state variables and functions matter a lot in minimizing gas cost for users. See the contracts below.

/ SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Library {
    event DocumentAdded(uint256 id, string title, string pinataCID, uint256 timestamp);

    // If you want "user's address" to be searchable, always index it  
    event RoleAssigned(address indexed user, string role);    
    
    // Members of na enum shound always be in uppercase
    enum Role { OWNER, ADMIN, LICENSEDUSER } 

    uint256 public documentCount;  

    /* Arrangement of variable by types matter in solidity
     * Also, It is cheaper using bytes in storage than string. Strings are more expensive in storage 
        since they're measured in character e,g a string "MyName" will take larger space. 
      The name of a struct should be start with uppercase.
      Arrange varibles in structs by types
    */ 
    struct Document {
        uint256 id;
        uint256 timestamp; 
        string title;
        string pinataCID; 
        // Pinata CID for the PDF file        
    }    
    
    mapping(address => uint256) public roles;   
     
    mapping(address => uint256) public userCount;    
    
    mapping(uint => address) public owners;
    
    Document[] private documentList;    

    modifier onlyAdmin() { 
        require(
            roles[msg.sender] == uint256(Role.ADMIN), "Only Admin can call this." 
        );        _;    
    }    
    
    modifier onlyOwner() {        
        require(roles[msg.sender] == uint256(Role.OWNER), "Only Owner can call this." );        
        _;    
    }

    function encode(string memory args) internal pure returns(bytes32) {
        return keccak256(abi.encodePacked(args));
    }
    
    function assignRole(address user, string memory role) public onlyOwner {
        if (encode(role) == encode("Admin")) {
            roles[user] = uint256(Role.ADMIN); 
        } else if (encode(role) == encode("licencedUser")) {
            roles[user] = uint256(Role.LICENSEDUSER);        
            
        } else { 
            revert("Invalid role");        
        }       
        emit RoleAssigned(user, role);    
    }    
        
    function addDocument(string memory title, string memory pinataCID, uint256 timestamp) public onlyAdmin {       
        uint256 id = documentList.length;        
        documentList.push(Document(id, timestamp, title, pinataCID));        
        owners[id] = msg.sender;       
        emit DocumentAdded(id, title, pinataCID, timestamp);    
    }    
    
    ///Note; Its not idea return an array especially when its a dynamic array whose size can grow indefinitey
    function getAllDocuments() public view returns (Document[] memory) {       
        return documentList;    
    }    
    
    /*This function can throw out-of-bound error when the end of the array is reachs,  
        There should a check to avoid it.
    */
    function getDocument(uint256 id) public view returns (Document  memory) {
        return documentList[id];    
    }
}
4 Likes

This is great, you did a great job of doing all of these.
Thanks for pointing these out. They are all valid points and i agree with them. Apologies for not thoroughly proofreading my codes to implement these conventions.

I believe in keeping up with conventions even if they aint rules and would not also affect the function of your codes.

i will make the suggested changes.

Correct me if i am right tho, i am not too sure of the enums rule of using uppercases. i will confirm that and then get back to you soonest.

Also for the bytes you talked about. I believe you are talking about taking advantage of setting byte size limits to control computational cost. I think this would apply in some cases in this contract but not all as i wouldnt wanna restrict the ability of users to name their books according to plan due to character size limits.

4 Likes

Not really. Storing strings can be very costly. It increases deployment costs as well as the cost of interaction for users. The max spot for any variable is 32 bytes equivalent to the size of an address that is 256 bits. Take for example I want to store “https://www.somenftsites/nftid/somegibbberishwords”. This won’t be stored as a single-byte representation. It is far greater than 32 bytes. What the compile and EVM does is create additional slots for it whereas if you encode it using bytes(“https://www.somenftsites/nftid/somegibbberishwords”) or abi.encode("https://www.somenftsites/nftid/somegibbberishwords") will conveniently be fixed in a 32 bytes storage and you only need to decode it whenever you need to retrieve it using abi.decode()

4 Likes

Great explanations sir. This would be an important consideration when building a production ready DApp.

3 Likes

Please can you share links to any reference you have on the use of UPPERCASE in enums as i only know of using CamelCase, just like i did here.

4 Likes