Supply Chain Dapps for Mini Project

Yasukazu Suwa
9 min readApr 24, 2024

Decentralized applications (dApps) are applications built using blockchain technology. There are several benefits to using dApps in the supply chain field:

  • Increased Transparency The blockchain is a distributed ledger that is extremely difficult to tamper with, and all data and transaction histories are made public. This increases transparency and traceability throughout the supply chain processes.
  • Ensured Trust With no central authority, the risk of tampering with supply chain data is greatly reduced. Smart contracts also enable automated execution, minimizing the risk of human error.
  • Enhanced Security As a decentralized network, blockchain has an inherent resistance to cyber attacks by design.

This time, I implemented a simple Supply Chain DApp using Hardhat and Next.js.

Technical Stack

Next.js: Next.js is a popular React framework for building server-rendered or statically exported React applications. It simplifies the setup for production and allows features like server-side rendering and static site generation out of the box. Next.js provides an opinionated structure and build process, making it easy to create high-performance web applications.

Hardhat: Hardhat is a development environment for Ethereum software. It is designed to help developers write smart contracts, run them on a development network, and deploy them to live networks. Hardhat includes a built-in Ethereum node, allowing developers to test their contracts locally without relying on a public network. It also provides features like task automation, debugging, and plugin integration.

Setup

  1. Build next.js project and install some libraries
npx create-next-app project-name
npm install --save-dev hardhat
npm install --save ethers
npm install --save web3modal

2. Initialize hardhat

npx hardhat

After initializing you can see the contracts and scripts folders in root directory

Step 1

Change code in scrips/deploy.ts

import { ethers } from "hardhat";

async function main() {
const Tracking = await ethers.getContractFactory("Tracking");
const tracking = await Tracking.deploy();

await tracking.deployed();

console.log(
`Tracking deployed to ${tracking.address}`
);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

This code is a script to deploy a smart contract called “Tracking” on the Ethereum blockchain using Hardhat.

The main flow is as follows:

  1. Import the ethers library from Hardhat
  2. Create a contract instance with ethers.getContractFactory
  3. Deploy the contract with Tracking.deploy()
  4. Wait for the deployment to complete with tracking.deployed()
  5. Print the deployed contract address to the console
  6. If there is an error during deployment, catch it and print the error message to the console

In other words, when you run this script, a new “Tracking” contract will be deployed on the specified Ethereum network, and its address will be displayed.

Step 2

Create Tracking.sol in contracts folder

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Tracking {
enum ShipmentStatus { PENDING, IN_TRANSIT, DELIVERED }

struct Shipment {
address sender;
address receiver;
uint256 pickupTime;
uint256 deliveryTime;
uint256 distance;
uint256 price;
ShipmentStatus status;
bool isPaid;
}

mapping(address => Shipment[]) public shipments;
uint256 public shipmentCount;

struct TypeShipment {
address sender;
address receiver;
uint256 pickupTime;
uint256 deliveryTime;
uint256 distance;
uint256 price;
ShipmentStatus status;
bool isPaid;
}

TypeShipment[] typeShipments;

event ShipmentCreated(address indexed sender, address indexed receiver, uint256 pickupTime, uint256 distance, uint256 price);
event ShipmentInTransit(address indexed sender, address indexed receiver, uint256 pickupTime);
event ShipmentDelivered(address indexed sender, address indexed receiver, uint256 deliveryTime);
event ShipmentPaid(address indexed sender, address indexed receiver, uint256 amount);

constructor() {
shipmentCount = 0;
}

function createShipment(address _receiver, uint256 _pickupTime, uint256 _distance, uint256 _price) public payable {
require(msg.value == _price, "Payment amount must match the price.");

Shipment memory shipment = Shipment(msg.sender, _receiver, _pickupTime, 0, _distance, _price, ShipmentStatus.PENDING, false);

shipments[msg.sender].push(shipment);
shipmentCount++;

typeShipments.push(
TypeShipment(
msg.sender,
_receiver,
_pickupTime,
0,
_distance,
_price,
ShipmentStatus.PENDING,
false
)
);

emit ShipmentCreated(msg.sender, _receiver, _pickupTime, _distance, _price);
}

function startShipment(address _sender, address _receiver, uint256 _index) public {
Shipment storage shipment = shipments[_sender][_index];
TypeShipment storage typeShipment = typeShipments[_index];

require(shipment.receiver == _receiver, "Invalid receiver.");
require(shipment.status == ShipmentStatus.PENDING, "Shipment already in transit.");

shipment.status = ShipmentStatus.IN_TRANSIT;
typeShipment.status = ShipmentStatus.IN_TRANSIT;

emit ShipmentInTransit(_sender, _receiver, shipment.pickupTime);
}

function completeShipment(address _sender, address _receiver, uint256 _index) public {
Shipment storage shipment = shipments[_sender][_index];
TypeShipment storage typeShipment = typeShipments[_index];

require(shipment.receiver == _receiver, "Invalid receiver.");
require(shipment.status == ShipmentStatus.IN_TRANSIT, "Shipment not in transit.");
require(!shipment.isPaid, "Shipment already paid.");

shipment.status = ShipmentStatus.DELIVERED;
typeShipment.status = ShipmentStatus.DELIVERED;
typeShipment.deliveryTime = block.timestamp;
shipment.deliveryTime = block.timestamp;

uint256 amount = shipment.price;

payable(shipment.sender).transfer(amount);

shipment.isPaid = true;
typeShipment.isPaid = true;

emit ShipmentDelivered(_sender, _receiver, shipment.deliveryTime);
emit ShipmentPaid(_sender, _receiver, amount);
}

function getShipment(address _sender, uint256 _index) public view returns (address, address, uint256, uint256, uint256, uint256, ShipmentStatus, bool) {
Shipment memory shipment = shipments[_sender][_index];
return (shipment.sender, shipment.receiver, shipment.pickupTime, shipment.deliveryTime, shipment.distance, shipment.price, shipment.status, shipment.isPaid);
}

function getShipmentCount(address _sender) public view returns (uint256) {
return shipments[_sender].length;
}

function getAllTransactions() public view returns (TypeShipment[] memory) {
return typeShipments;
}
}

This code is a smart contract that implements a delivery tracking system on the Ethereum blockchain. The main functions are:

  1. Creating Shipment Requests
  • The createShipment function allows creating a new shipment request by specifying the sender, receiver, pickup time, distance, and price.
  • It rejects the request if the required amount of Ether is not sent.

2. Updating Shipment Status

  • The startShipment function can start the shipment.
  • The completeShipment function records shipment completion and sends Ether to the sender.

3. Getting Shipment Data

  • getShipment retrieves details of a specific shipment.
  • getShipmentCount gets the number of shipment requests for a sender.
  • getAllTransactions retrieves the history of all shipments.

4. Emitting Events

  • It emits events for shipment creation, start, completion, and payment.

The code also uses structs and mappings to store shipment data, enums to manage shipment status, and various other Solidity features.

Step3

Run the command to deploy the contract

npx hardhat run --network localhost scripts/deploy.ts

When you run this command, the following happens:

  1. A local Ethereum node is started (if not already running).
  2. The code in the scripts/deploy.ts file is executed, and according to the deployment logic written there, the smart contract is deployed to the local node.

Typically, the deploy.ts file contains code for compiling the contract, deploying it, setting constructor arguments, and so on. If the deployment is successful, the deployed contract address is printed to the console.

Step4

Create TrackingContext.tsx

'use client';
import React, { useState, useEffect, ReactNode } from "react";
import Web3Modal from "web3modal";
import { ethers } from "ethers";

// INTERNAL IMPORT
import tracking from "@/context/Tracking.json";
const ContractAddress = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS;
const ContractABI = tracking.abi;

interface ShipmentItems {
receiver: string;
pickupTime: string;
distance: string;
price: string;
}

interface Shipment {
sender: string;
receiver: string;
price: ethers.BigNumber;
pickupTime: ethers.BigNumber;
deliveryTime: ethers.BigNumber;
distance: ethers.BigNumber;
isPaid: boolean;
status: number;
}

interface CompleteShipmentParams {
receiver: string;
index: number;
}

interface GetProductParams {
receiver: string;
index: number;
}

// ---FETCHING SMART CONTRACT
const fetchContract = (signerOrProvider: ethers.Signer | ethers.providers.Provider) =>
new ethers.Contract(ContractAddress, ContractABI, signerOrProvider);

export const TrackingContext = React.createContext({});

export const TrackingProvider = ({ children }: { children: ReactNode }) => {
//STATE VARIABLE
const DappName = "Product Tracking Dapp";
const [currentUser, setCurrentUser] = useState("");

const createShipment = async (items: ShipmentItems) => {
const { receiver, pickupTime, distance, price } = items;

try {
const web3modal = new Web3Modal();
const connection = await web3modal.connect();
const provider = new ethers.providers.Web3Provider(connection);
const signer = provider.getSigner();
const contract = fetchContract(signer);

const createItem = await contract.createShipment(
receiver,
new Date(pickupTime).getTime(),
distance,
ethers.utils.parseUnits(price, 18),
{
value: ethers.utils.parseUnits(price, 18),
}
);
await createItem.wait();
} catch (error) {
console.error("Some want wrong", error);
}
}

const getAllShipments = async () => {
try {
const provider = new ethers.providers.JsonRpcProvider();
const contract = fetchContract(provider);

const shipments = await contract.getAllTransactions();
const allShipments = shipments.map((shipment: Shipment) => ({
sender: shipment.sender,
receiver: shipment.receiver,
price: ethers.utils.formatEther(shipment.price.toString()),
pickupTime: shipment.pickupTime.toNumber(),
deliveryTime: shipment.deliveryTime.toNumber(),
distance: shipment.distance.toNumber(),
isPaid: shipment.isPaid,
status: shipment.status,
}));

return allShipments;
} catch (error) {
console.error("error want, getting shipment", error);
}
}

const getShipmentCount = async () => {
try {
if (!window.ethereum) return "Install MetaMask";

const accounts = await window.ethereum.request({
method: "eth_accounts",
});
const provider = new ethers.providers.JsonRpcProvider();
const contract = fetchContract(provider);
const shipmentsCount = await contract.getShipmentCount(accounts[0]);

return shipmentsCount.toNumber();
} catch (error) {
console.error("error want, getting shipment", error);
}
}

const completeShipment = async (completeShip: CompleteShipmentParams) => {
const { receiver, index } = completeShip;
try {
if (!window.ethereum) return "Install MetaMask";

const accounts = await window.ethereum.request({
method: "eth_accounts",
});
const web3modal = new Web3Modal();
const connection = await web3modal.connect();
const provider = new ethers.providers.Web3Provider(connection);
const signer = provider.getSigner();
const contract = fetchContract(signer);

const transaction = await contract.completeShipment(
accounts[0],
receiver,
index,
{
gasLimit: 300000,
}
);

transaction.wait();
} catch (error) {
console.error("wrong completeShipment", error);
}
}

const getShipment = async (index: number) => {
try {
if (!window.ethereum) return "Install MetaMask";

const accounts = await window.ethereum.request({
method: "eth_accounts",
});

const provider = new ethers.providers.JsonRpcProvider();
const contract = fetchContract(provider);
const shipment = await contract.getShipment(accounts[0], index * 1);

const SingleShipment = {
sender: shipment[0],
receiver: shipment[1],
pickupTime: shipment[2].toNumber(),
deliveryTime: shipment[3].toNumber(),
distance: shipment[4].toNumber(),
price: ethers.utils.formatEther(shipment[5].toString()),
status: shipment[6],
isPaid: shipment[7],
};
return SingleShipment;
} catch (error) {
console.error("Sorry no shipment", error);
}
}

const startShipment = async (getProduct: GetProductParams) => {
const { receiver, index } = getProduct;

try {
if (!window.ethereum) return "Install MetaMask";

const accounts = await window.ethereum.request({
method: "eth_accounts",
});

const web3modal = new Web3Modal();
const connection = await web3modal.connect();
const provider = new ethers.providers.Web3Provider(connection);
const signer = provider.getSigner();
const contract = fetchContract(signer);
const shipment = await contract.startShipment(
accounts[0],
receiver,
index * 1
);

shipment.wait();
} catch (error) {
console.error("Sorry no shipment", error);
};
};
//---CHECK WALLET CONNECTED
const checkIfWalletConnected = async () => {
try {
if (!window.ethereum) return "Install MetaMask";

const accounts = await window.ethereum.request({
method: "eth_accounts",
});

if (accounts.length) {
setCurrentUser(accounts[0]);
} else {
return "No account";
}
} catch (error) {
return "Not connected";
};
}

//---CONNECT WALLET FUNCTION
const connectWallet = async () => {
try {
if (!window.ethereum) return "Install MetaMask";

const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});

setCurrentUser(accounts[0]);
} catch (error) {
return `Failed to connect wallet: ${error}`;
};
}

useEffect(() => {
checkIfWalletConnected();
}, []);

return (
<TrackingContext.Provider
value={{
connectWallet,
createShipment,
getAllShipments,
completeShipment,
getShipment,
startShipment,
getShipmentCount,
DappName,
currentUser,
}}
>
{children}
</TrackingContext.Provider>
);
};

process.env.NEXT_PUBLIC_CONTRACT_ADDRESS should be the contract address that you get in the console when deployed.

This code acts as a context provider for a decentralized application (dApp) built with Next.js. It provides various functions to interact with a smart contract for a shipment tracking system.

It has the following features:

  1. Wallet Connectivity: It connects to a MetaMask wallet and retrieves the current user’s address.
  2. Shipment Creation: It allows creating a new shipment request by specifying the receiver, pickup time, distance, and price.
  3. Shipment Tracking: It can fetch a list of all shipments or retrieve details of a specific shipment.
  4. Shipment Status: It can start a shipment or complete a shipment and make the payment.
  5. Contract Interaction: It connects to an Ethereum node using Web3Modal and handles data exchange with the smart contract.

This component acts as a context provider, allowing other components in the application to access these functionalities.

Step5

Now completed the backend part, let’s move frontend and connect MetaMask

Using the private keys of the accounts created with npx hardhat node, you can get 10000 ETH for two accounts, one as the Sender and the other as the Receiver.

You will need to have two accounts, one to act as the Sender and the other as the Receiver, in order to test your dApp’s shipment tracking functionality.

Demo

Step1: Call createShipment to add tracking. should be inputted receiver address, date, distance, and ETH
After creating the shipment, the contract is shown in the table.
Step2: Get the tracking detail by ID 2, which is shown below the button.
Step3: The ID 2 contract is pending status so the sender can it to IN_TRANSIT by contract.
After did start shipping, the shipment status changed pending to IN_TRANSIT
Step4: After the sender delivers it to the receiver, the sender will change the status by completing the shipment contract.
After completing all contracts the final status is Delivered and Paid becomes completed. And also fills Delivery Time.

--

--