Building a Verifiable Academic Credentials DApp using Rootstock Attestation Service (RAS)

Hi builders, welcome to this in-depth tutorial where we’ll explore how to build a Verifiable Academic Credentials DApp using the Rootstock Attestation Service (RAS).
Rootstock (RSK) is a Bitcoin sidechain that merges Bitcoin’s robust security with Ethereum’s EVM compatibility, making it a perfect environment for developing decentralized identity and verification systems.
In this guide, we’ll design and implement a decentralized application that allows educational institutions to issue academic credentials—like diplomas, certificates, or course completion badges—as on-chain attestations. These attestations are tamper-proof, publicly verifiable, and cryptographically secured on the Rootstock blockchain.
By the end, you’ll understand how RAS can be leveraged to issue and verify academic credentials in a trustless and transparent way—eliminating fake certificates and simplifying verification for employers, universities, and recruiters.
✨ Let’s dive in and see how Rootstock Attestation Service can bring trust and transparency to the world of academic verification.
Understanding Rootstock
Rootstock is a smart contract platform built to extend Ethereum’s functionality to the Bitcoin network. It enables developers to build decentralized applications (dApps) that enjoy the best of both worlds — Bitcoin’s unmatched security and Ethereum’s programmable flexibility.
Through its two-way peg mechanism, Rootstock allows Bitcoin holders to move their BTC onto the network as rBTC, unlocking access to DeFi protocols and smart contracts. This seamless bridge between Bitcoin and Rootstock makes it an ideal platform for trustless financial innovation.
Why Build on Rootstock
When developing a decentralized platform for issuing and verifying academic credentials, the choice of blockchain is critical. The system must be secure, transparent, and cost-efficient while maintaining compatibility with existing Web3 tools. Here’s why Rootstock (RSK) is the perfect foundation for this project:
🔒 Bitcoin-Level Security
Rootstock is merge-mined with Bitcoin, meaning it inherits Bitcoin’s unmatched security and immutability. This ensures that once a credential is issued on-chain, it can never be altered or forged — providing ultimate trust in academic records.
⚙️ EVM Compatibility
RSK is fully compatible with the Ethereum Virtual Machine (EVM), allowing developers to use familiar tools like Solidity, Hardhat, Foundry, and Remix. This makes building, testing, and deploying smart contracts for attestations seamless and developer-friendly.
💰 Cost-Effective Transactions
With lower gas fees and faster transaction times compared to Ethereum, Rootstock makes it practical to issue large numbers of academic credentials without financial barriers — ideal for universities or online platforms managing thousands of students.
🌐 Decentralized Identity & Attestation Support
Rootstock’s Attestation Service (RAS) provides a robust framework for creating verifiable, tamper-proof attestations — making it a natural fit for digital credentials, certifications, and reputation systems.
🚀 Expanding Ecosystem
Rootstock’s ecosystem is rapidly evolving, with tools, protocols, and integrations supporting DeFi, identity, and infrastructure use cases. Building on RSK connects your project to a network of interoperable services and decentralized applications.
Understanding Rootstock Attestation Service (RAS): Verifiable Trust Layer
The Rootstock Attestation Service (RAS) provides an on-chain trust framework that enables developers to issue, verify, and manage cryptographically verifiable attestations. These attestations represent claims — such as academic achievements, identity proofs, or credentials — that are stored immutably on the blockchain.
RAS is inspired by the Ethereum Attestation Service (EAS), bringing a flexible and standardized model for verifiable data exchange on the Rootstock blockchain. This makes it a perfect foundation for trust-driven use cases like academic credential verification, KYC attestations, and digital identity systems.
At its core, RAS introduces a three-part attestation model — Schema, Attestation, and Verification — forming the backbone of decentralized trust.
Schema → Attestation → Verification
Prerequisites
Before diving into this tutorial, make sure you have your development environment ready and a basic understanding of the tools we’ll use to interact with the Rootstock blockchain and the Attestation Service.
1. Node.js and npm
You’ll need Node.js (v18 or higher) and npm (v9 or higher) installed on your system to run the frontend and backend of the DApp.
Check your versions using:
node -v
npm -v
If you don’t have them installed, download from the Node.js official site.
2. Wallet Setup (MetaMask or Similar)
Install and set up MetaMask (or any EVM-compatible wallet) to interact with the Rootstock Testnet.
You’ll also need test rBTC for deploying and verifying transactions.
Network: Rootstock Testnet
Symbol: tRBTC
Add Rootstock Testnet to MetaMask
3. Environment Variables (.env.local)
Create a .env.local file in your project’s frontend directory and define the following environment variables:
NEXT_PUBLIC_RPC_URL=https://public-node.testnet.rsk.co
NEXT_PUBLIC_EAS_CONTRACT=0xYourEASContractAddress
NEXT_PUBLIC_SCHEMA_UID=0xYourSchemaUID
NEXT_PUBLIC_SCHEMA_REGISTRY=0xOptionalSchemaRegistryAddress
These variables will be used to connect your DApp to the Rootstock Attestation Service and issue or verify credentials.
4. Rootstock Attestation Service (RAS) Contract Addresses
You’ll need to reference the official RAS contract addresses deployed on Rootstock.
You can find the latest addresses here:
🔗 Rootstock Attestation Service Docs
Typical addresses include:
EAS Contract: 0xCf9f6cd3f310C2... (testnet example)
Schema Registry: 0xA018Ae66F5a8297C... (testnet example)
(Always verify the latest addresses in the official documentation.)
5. Basic Understanding of Attestations
Familiarity with attestations and how they represent verifiable claims on-chain will help you follow along. If you’re new to this concept, check out the Ethereum Attestation Service Docs — RAS follows a similar model but is tailored for the Rootstock ecosystem.
Architecture Overview
Our Verifiable Academic Credentials DApp is built as a full-stack decentralized application that connects the frontend user interface, EAS-powered smart contract logic, and Rootstock Attestation Service (RAS).
The project is designed for simplicity and scalability — allowing educational institutions to issue credentials and anyone to verify them in a few clicks.
Project Structure Overview
Let’s break down the main structure of the project:
src/
├── app/
│ ├── layout.tsx
│ ├── providers.tsx
│ ├── page.tsx
│ ├── issuer/
│ │ └── page.tsx
│ └── verify/
│ └── page.tsx
├── components/
├── lib/
│ └── eas.ts
├── .env.local.example
├── README.md
Project Setup
Let’s start by creating a new directory for our project :
cd rsks-credential-dapp
npm install
Install Required Dependencies
npm install ethers wagmi @tanstack/react-query @ethereum-attestation-service/eas-sdk
npm install tailwindcss autoprefixer postcss
npx tailwindcss init -p
Before we dive into the code, let’s take a quick look at the main libraries and tools used in this project. Each of these plays a key role in enabling blockchain interactions, managing attestations, and building a responsive user interface.
1. ethers.js
Purpose: Blockchain interaction (providers, signers, contracts)
ethers.js is a lightweight and powerful JavaScript library used to interact with the Ethereum blockchain.
It allows you to:
Connect to blockchain networks using providers
Interact with smart contracts using signers
Send transactions and fetch on-chain data easily
It’s the foundation for most blockchain-based JavaScript/TypeScript applications.
2. wagmi
Purpose: Wallet connections and React hooks
wagmi is a React library that simplifies Web3 wallet connections and blockchain interactions.
It provides a set of ready-to-use React hooks for:
Connecting wallets (like MetaMask or WalletConnect)
Reading/writing blockchain data
Managing wallet and network states
This makes it easier to integrate wallet functionality in React applications without manually writing connection logic.
3. tanstack/react-query
Purpose: Data fetching, caching, and synchronization
@tanstack/react-query (commonly known as React Query) helps manage server-side or blockchain data in React apps.
It efficiently handles:
Fetching data from APIs or smart contracts
Caching responses to avoid redundant network calls
Automatic refetching when data changes
This ensures your DApp UI always stays up-to-date and responsive.
4. ethereum-attestation-service/eas-sdk
Purpose: Create and verify attestations using EAS
This is the official SDK for the Ethereum Attestation Service (EAS).
It allows developers to:
Define schemas for attestations
Create and sign attestations on-chain
Verify existing attestations via UIDs
EAS enables decentralized identity, reputation, and verification systems built on Ethereum.
5. Tailwind CSS
Purpose: Utility-first CSS framework for styling
tailwindcss is a utility-first CSS framework that allows developers to build modern, responsive designs quickly.
In the next steps, we’ll start building each of these one by one —
Understanding IssuerPage.tsx
The IssuerPage component is the main interface for issuing verifiable credentials on the blockchain. It allows an authorized institution to connect its wallet, fill in credential details, and publish an attestation via the Ethereum Attestation Service (EAS).
**Purpose:
**Enables institutions to issue academic credentials as on-chain attestations on the Rootstock network. Each credential is immutable, verifiable, and linked to the student’s wallet address.
Key Imports:
WalletConnect — Handles wallet connection using wagmi hooks
SCHEMA_DEFINITION — Displays active EAS schema
issueCredential() — Sends credential data to the blockchain and returns UID + transaction hash
getExplorerLink() — Generates link to view the attestation on the explorer
**Form Fields:
**• Recipient Wallet
• Student Name
• Degree Name
• Institution Name
• Date Awarded
Core Logic:
Validates form inputs and converts dateAwarded to a UNIX timestamp
Calls issueCredential() to create and publish an attestation
On success → displays UID, Transaction Hash, and Explorer link
On failure → shows user-friendly error messages
UI Highlights:
Clean form layout styled with Tailwind CSS
Dynamic schema preview
Success/error states with responsive design
In short, this component connects the issuer’s wallet, collects credential data, and issues a verifiable attestation on-chain with just one transaction.
'use client';
import { FormEvent, useState } from 'react';
import { WalletConnect } from '@/components/WalletConnect';
import { SCHEMA_DEFINITION, issueCredential, getExplorerLink } from '@/lib/eas';
interface CredentialFormState {
recipient: string;
studentName: string;
degreeName: string;
institutionName: string;
dateAwarded: string;
}
const initialFormState: CredentialFormState = {
recipient: '',
studentName: '',
degreeName: '',
institutionName: '',
dateAwarded: '',
};
export default function IssuerPage() {
const [formState, setFormState] = useState<CredentialFormState>(initialFormState);
const [isSubmitting, setIsSubmitting] = useState(false);
const [successUid, setSuccessUid] = useState<string | null>(null);
const [txHash, setTxHash] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsSubmitting(true);
setErrorMessage(null);
setSuccessUid(null);
setTxHash(null);
const timestamp = Math.floor(new Date(formState.dateAwarded).getTime() / 1000);
if (!Number.isFinite(timestamp) || timestamp <= 0) {
setErrorMessage('Please provide a valid award date.');
setIsSubmitting(false);
return;
}
try {
const result = await issueCredential({
recipient: formState.recipient,
studentName: formState.studentName,
degreeName: formState.degreeName,
institutionName: formState.institutionName,
dateAwarded: timestamp,
});
setSuccessUid(result.uid);
setTxHash(result.transactionHash ?? null);
setFormState(initialFormState);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unable to issue credential.';
setErrorMessage(message);
} finally {
setIsSubmitting(false);
}
};
return (
<main className="mx-auto flex w-full max-w-5xl flex-col gap-10 px-6 py-16">
<section className="flex flex-col gap-6 rounded-3xl border border-slate-200 bg-white p-8 shadow-lg">
<header className="space-y-2">
<h1 className="text-3xl font-semibold text-slate-900">Credential Issuer</h1>
<p className="text-sm text-slate-600">
Connect your institution's wallet and submit the credential details below to
publish an attestation on the Rootstock Attestation Service. All fields are
included in the attestation payload defined by the schema listed here.
</p>
</header>
{/* Show the active schema so issuers understand the on-chain data layout. */}
<div className="flex flex-col gap-3 rounded-2xl bg-slate-50 p-4">
<span className="text-xs font-semibold uppercase tracking-widest text-slate-500">
Active Schema
</span>
<code className="whitespace-pre-wrap break-words rounded-xl bg-white p-4 text-sm text-slate-700">
{SCHEMA_DEFINITION}
</code>
</div>
{/* Wallet connection controls handled via wagmi. */}
<WalletConnect />
{/* Credential issuance form bound to local component state. */}
<form className="grid gap-6" onSubmit={handleSubmit}>
<div className="grid gap-2">
<label htmlFor="recipient" className="text-sm font-medium text-slate-700">
Recipient Wallet Address
</label>
<input
id="recipient"
name="recipient"
className="rounded-2xl border border-slate-200 px-4 py-3 text-sm shadow-sm focus:border-teal-500 focus:outline-none focus:ring-2 focus:ring-teal-200"
placeholder="0x..."
value={formState.recipient}
onChange={(event) =>
setFormState((previous) => ({ ...previous, recipient: event.target.value }))
}
required
/>
</div>
<div className="grid gap-2">
<label htmlFor="studentName" className="text-sm font-medium text-slate-700">
Student Name
</label>
<input
id="studentName"
name="studentName"
className="rounded-2xl border border-slate-200 px-4 py-3 text-sm shadow-sm focus:border-teal-500 focus:outline-none focus:ring-2 focus:ring-teal-200"
placeholder="Ada Lovelace"
value={formState.studentName}
onChange={(event) =>
setFormState((previous) => ({ ...previous, studentName: event.target.value }))
}
required
/>
</div>
<div className="grid gap-2">
<label htmlFor="degreeName" className="text-sm font-medium text-slate-700">
Degree or Program Name
</label>
<input
id="degreeName"
name="degreeName"
className="rounded-2xl border border-slate-200 px-4 py-3 text-sm shadow-sm focus:border-teal-500 focus:outline-none focus:ring-2 focus:ring-teal-200"
placeholder="Bachelor of Computer Science"
value={formState.degreeName}
onChange={(event) =>
setFormState((previous) => ({ ...previous, degreeName: event.target.value }))
}
required
/>
</div>
<div className="grid gap-2">
<label htmlFor="institutionName" className="text-sm font-medium text-slate-700">
Institution Name
</label>
<input
id="institutionName"
name="institutionName"
className="rounded-2xl border border-slate-200 px-4 py-3 text-sm shadow-sm focus:border-teal-500 focus:outline-none focus:ring-2 focus:ring-teal-200"
placeholder="RootCred University"
value={formState.institutionName}
onChange={(event) =>
setFormState((previous) => ({ ...previous, institutionName: event.target.value }))
}
required
/>
</div>
<div className="grid gap-2">
<label htmlFor="dateAwarded" className="text-sm font-medium text-slate-700">
Date Awarded
</label>
<input
id="dateAwarded"
name="dateAwarded"
type="date"
className="rounded-2xl border border-slate-200 px-4 py-3 text-sm shadow-sm focus:border-teal-500 focus:outline-none focus:ring-2 focus:ring-teal-200"
value={formState.dateAwarded}
onChange={(event) =>
setFormState((previous) => ({ ...previous, dateAwarded: event.target.value }))
}
required
/>
</div>
<button
type="submit"
className="rounded-full bg-teal-500 px-6 py-3 text-sm font-semibold text-white transition hover:bg-teal-400 disabled:cursor-not-allowed disabled:opacity-70"
disabled={isSubmitting}
>
{isSubmitting ? 'Issuing Credential…' : 'Issue Credential'}
</button>
</form>
{errorMessage ? (
<p className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{errorMessage}
</p>
) : null}
{successUid ? (
<div className="space-y-2 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
<p className="font-semibold">Credential issued successfully!</p>
<p>
UID: <span className="font-mono">{successUid}</span>
</p>
<a
href={getExplorerLink(successUid)}
className="font-semibold underline"
target="_blank"
rel="noreferrer"
>
View on Rootstock Explorer
</a>
{txHash ? (
<p>
Transaction Hash: <span className="font-mono break-all text-xs">{txHash}</span>
</p>
) : null}
</div>
) : null}
</section>
</main>
);
}
Understanding VerifyPage.tsx
The VerifyPage component is the credential verification interface of the application. It allows anyone to validate a blockchain-issued credential using its unique UID on the Rootstock Attestation Service (EAS).
**Purpose:
**Fetch and decode an attestation by UID, verify its authenticity, and display details like issuer, recipient, and credential metadata in a readable format.
Key Imports:
getAttestation() — Retrieves attestation details from EAS by UID
SchemaEncoder — Decodes raw attestation data into readable fields
SCHEMA_DEFINITION — Defines how credential data is structured on-chain
Core Logic:
User enters an attestation UID and submits the form
The app calls getAttestation() to fetch data from EAS
The SchemaEncoder decodes encoded data (recipient, name, degree, etc.)
Determines attestation status → Valid, Revoked, or Expired
Displays issuer address, timestamp, and credential fields
**Main State Variables:
**uidInput, isLoading, attestationState, statusMessage, attester, timestamp, and errorMessage
UI Highlights:
Clean, responsive layout using Tailwind CSS
Displays decoded credential details clearly
Status badges for verification state
Real-time validation with error and success feedback
In short, this page lets users verify any issued credential by simply entering its UID — confirming its validity and viewing complete credential details directly from the blockchain.
'use client';
import { FormEvent, useMemo, useState } from 'react';
import { SCHEMA_DEFINITION, getAttestation } from '@/lib/eas';
import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk';
interface DecodedCredential {
recipient?: string;
studentName?: string;
degreeName?: string;
institutionName?: string;
dateAwarded?: string;
}
function formatDateFromSeconds(value?: string) {
if (!value) return 'Unknown';
const seconds = Number(value);
if (!Number.isFinite(seconds) || seconds <= 0) return 'Unknown';
return new Date(seconds * 1000).toLocaleDateString();
}
export default function VerifyPage() {
const [uidInput, setUidInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [attestationState, setAttestationState] = useState<DecodedCredential | null>(null);
const [attester, setAttester] = useState<string | null>(null);
const [statusMessage, setStatusMessage] = useState<string | null>(null);
const [timestamp, setTimestamp] = useState<number | null>(null);
// Decoder maps the on-chain encoded bytes back to structured fields.
const schemaDecoder = useMemo(() => new SchemaEncoder(SCHEMA_DEFINITION), []);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsLoading(true);
setErrorMessage(null);
try {
// Retrieve the attestation by UID from the Rootstock Attestation Service.
const attestation = await getAttestation(uidInput.trim());
if (!attestation) {
setErrorMessage('No attestation found for that UID.');
setAttestationState(null);
return;
}
const decoded = schemaDecoder.decodeData(attestation.data);
// Convert the decoded fields into a friendlier object keyed by field name.
const mapped: DecodedCredential = decoded.reduce((accumulator, current) => {
accumulator[current.name as keyof DecodedCredential] = String(current.value.value);
return accumulator;
}, {} as DecodedCredential);
setAttestationState(mapped);
setAttester(attestation.attester);
setTimestamp(Number(attestation.time));
const isRevoked = attestation.revocationTime > 0;
const isExpired =
Number(attestation.expirationTime) !== 0 &&
Number(attestation.expirationTime) < Date.now() / 1000;
if (isRevoked) {
setStatusMessage('Revoked');
} else if (isExpired) {
setStatusMessage('Expired');
} else {
setStatusMessage('Valid');
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unable to load attestation.';
setErrorMessage(message);
setAttestationState(null);
} finally {
setIsLoading(false);
}
};
return (
<main className="mx-auto flex w-full max-w-4xl flex-col gap-10 px-6 py-16">
<section className="space-y-4 rounded-3xl border border-slate-200 bg-white p-8 shadow-lg">
<header className="space-y-2">
<h1 className="text-3xl font-semibold text-slate-900">Verify Credential</h1>
<p className="text-sm text-slate-600">
Paste the UID from an issued credential to fetch its attestation details directly
from the Rootstock Attestation Service.
</p>
</header>
{/* Lookup form that triggers the attestation fetch. */}
<form className="flex flex-col gap-4 sm:flex-row" onSubmit={handleSubmit}>
<input
className="flex-1 rounded-2xl border border-slate-200 px-4 py-3 text-sm shadow-sm focus:border-teal-500 focus:outline-none focus:ring-2 focus:ring-teal-200"
placeholder="Enter attestation UID"
value={uidInput}
onChange={(event) => setUidInput(event.target.value)}
required
/>
<button
type="submit"
className="rounded-full bg-teal-500 px-6 py-3 text-sm font-semibold text-white transition hover:bg-teal-400 disabled:cursor-not-allowed disabled:opacity-70"
disabled={isLoading}
>
{isLoading ? 'Verifying…' : 'Verify'}
</button>
</form>
{errorMessage ? (
<p className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{errorMessage}
</p>
) : null}
{attestationState ? (
<div className="space-y-5 rounded-2xl border border-slate-200 bg-slate-50 p-6 text-sm text-slate-700">
{/* High-level status indicators including revocation and issuer details. */}
<div className="flex flex-wrap items-center gap-3">
<span className="rounded-full bg-white px-4 py-2 text-xs font-semibold uppercase tracking-widest text-slate-600">
Status: {statusMessage}
</span>
{attester ? (
<span className="rounded-full bg-white px-4 py-2 font-mono text-xs">
Attester: {attester}
</span>
) : null}
{timestamp ? (
<span className="rounded-full bg-white px-4 py-2 text-xs text-slate-600">
Issued: {new Date(timestamp * 1000).toLocaleString()}
</span>
) : null}
</div>
{/* Detailed credential data decoded from the attestation payload. */}
<dl className="grid gap-4 md:grid-cols-2">
<div>
<dt className="text-xs font-semibold uppercase tracking-widest text-slate-500">
Recipient
</dt>
<dd className="font-mono text-xs md:text-sm">{attestationState.recipient}</dd>
</div>
<div>
<dt className="text-xs font-semibold uppercase tracking-widest text-slate-500">
Student Name
</dt>
<dd>{attestationState.studentName}</dd>
</div>
<div>
<dt className="text-xs font-semibold uppercase tracking-widest text-slate-500">
Degree Name
</dt>
<dd>{attestationState.degreeName}</dd>
</div>
<div>
<dt className="text-xs font-semibold uppercase tracking-widest text-slate-500">
Institution
</dt>
<dd>{attestationState.institutionName}</dd>
</div>
<div>
<dt className="text-xs font-semibold uppercase tracking-widest text-slate-500">
Date Awarded
</dt>
<dd>{formatDateFromSeconds(attestationState.dateAwarded)}</dd>
</div>
</dl>
</div>
) : null}
</section>
</main>
);
}
Understanding components/WalletConnect.tsx
The WalletConnect component is a reusable UI element that enables users to connect or disconnect their crypto wallet (like MetaMask) directly from the application.
**Purpose:
**Provide an easy interface for wallet connection and display the connected user’s address — an essential step before issuing or verifying attestations.
Key Imports:
useAccount, useConnect, useDisconnect → Hooks from wagmi for managing wallet state and actions
truncateAddress() → Helper function to display shortened wallet addresses
Core Logic:
Detects the wallet’s connection status (connected, disconnected)
Handles wallet connection using available wagmi connectors
Displays a truncated wallet address and “Disconnect” button when connected
Handles errors gracefully and updates the UI dynamically
UI Features:
Smooth, minimal design with Tailwind CSS
“Connect Wallet” and “Disconnect” buttons with loading states
Responsive and reusable across multiple pages (Issuer, Verifier)
In short, this component ensures seamless wallet integration with the dApp — letting users securely connect to the blockchain before performing any action.
'use client';
import { useCallback } from 'react';
import { useAccount, useConnect, useDisconnect } from 'wagmi';
function truncateAddress(address?: string) {
if (!address) return '';
return ${address.slice(0, 6)}…${address.slice(-4)};
}
export function WalletConnect() {
const { address, status } = useAccount();
const { disconnectAsync, isPending: isDisconnectPending } = useDisconnect();
const {
connectAsync,
connectors,
isPending: isConnectPending,
error,
} = useConnect();
const handleConnect = useCallback(async () => {
// Prefer a connector that signals readiness (e.g. MetaMask) but fall back to the first available option.
const connector = connectors.find((item) => item.ready) ?? connectors[0];
if (!connector) {
console.error('No wallet connectors are available.');
return;
}
try {
await connectAsync({ connector });
} catch (connectError) {
console.error(connectError);
}
}, [connectAsync, connectors]);
const handleDisconnect = useCallback(async () => {
try {
await disconnectAsync();
} catch (disconnectError) {
console.error(disconnectError);
}
}, [disconnectAsync]);
if (status === 'connected') {
return (
<div className="flex items-center gap-3">
<span className="rounded-full bg-slate-100 px-4 py-2 text-sm font-medium text-slate-700">
{truncateAddress(address || '')}
</span>
<button
type="button"
className="rounded-full border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 transition hover:bg-slate-100"
onClick={handleDisconnect}
disabled={isDisconnectPending}
>
{isDisconnectPending ? 'Disconnecting…' : 'Disconnect'}
</button>
</div>
);
}
return (
<div className="flex flex-col gap-2">
<button
type="button"
className="rounded-full bg-teal-500 px-4 py-2 text-sm font-semibold text-white transition hover:bg-teal-400"
onClick={handleConnect}
disabled={isConnectPending}
>
{isConnectPending ? 'Connecting…' : 'Connect Wallet'}
</button>
{error ? (
<p className="text-xs text-red-500">{error.message}</p>
) : null}
</div>
);
}
Understanding lib/eas.ts
The lib/eas.ts file contains the core blockchain logic that powers the app’s interaction with the Ethereum Attestation Service (EAS) smart contracts on the Rootstock Testnet.
**Purpose:
**Handle all read and write operations to EAS — from registering schemas and issuing attestations to fetching and decoding credential data.
Key Responsibilities:
Connect to EAS using either a read-only provider or a signer (MetaMask)
Define the schema structure for credentials (SCHEMA_DEFINITION)
Register new schemas on-chain for first-time setup
Issue credentials (attestations) based on the schema
Fetch and verify attestations using their UID
Generate explorer links for viewing credentials on-chain
Core Functions:
getSignerEas() → Connects to EAS with a wallet signer
registerSchema() → Creates a new schema on EAS
issueCredential() → Issues a blockchain-verified credential
getAttestation() → Retrieves stored attestation data
getExplorerLink() → Generates public link for attestation view
Tech Stack Used:
ethers.js → Blockchain connection & wallet handling
ethereum-attestation-service/eas-sdk → Schema creation, attestation issuance & decoding
Environment Variables (.env.local) → Store contract addresses, RPC URLs, and schema UID
In short, lib/eas.ts acts as the bridge between your frontend and the EAS smart contracts, making blockchain credential issuance and verification seamless.
import { BrowserProvider, JsonRpcProvider, ZeroAddress } from 'ethers';
import {
EAS,
SchemaEncoder,
SchemaRegistry,
type Attestation,
} from '@ethereum-attestation-service/eas-sdk';
// Pre-computed zero UID used when an attestation does not reference a previous record.
const ZERO_BYTES32 = 0x${'00'.repeat(32)};
const rpcUrl = process.env.NEXT_PUBLIC_RPC_URL || 'https://public-node.testnet.rsk.co';
const easContractAddress = process.env.NEXT_PUBLIC_EAS_CONTRACT;
const schemaUidFromEnv = process.env.NEXT_PUBLIC_SCHEMA_UID;
const schemaRegistryAddressOverride = process.env.NEXT_PUBLIC_SCHEMA_REGISTRY_CONTRACT;
if (!process.env.NEXT_PUBLIC_EAS_CONTRACT) {
console.warn(
'NEXT_PUBLIC_EAS_CONTRACT is not defined. Set it in .env.local to enable attestation writes.'
);
}
export const SCHEMA_DEFINITION =
'address recipient,string studentName,string degreeName,string institutionName,uint64 dateAwarded';
function invariant(variableName: string, value?: string | null): asserts value is string {
if (!value) {
throw new Error(`Missing environment variable or configuration value: ${variableName}`);
}
}
function getReadOnlyEas(): EAS {
invariant('NEXT_PUBLIC_EAS_CONTRACT', easContractAddress);
const eas = new EAS(easContractAddress);
const provider = new JsonRpcProvider(rpcUrl);
eas.connect(provider);
return eas;
}
async function getSignerEas(): Promise<EAS> {
if (typeof window === 'undefined') {
throw new Error('EAS signing is only available in the browser.');
}
invariant('NEXT_PUBLIC_EAS_CONTRACT', easContractAddress);
const ethereum = (window as typeof window & { ethereum?: unknown }).ethereum;
if (!ethereum) {
throw new Error('No injected wallet found. Please install MetaMask or another provider.');
}
const provider = new BrowserProvider(ethereum);
await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
const eas = new EAS(easContractAddress);
eas.connect(signer);
return eas;
}
export interface CredentialPayload {
recipient: string;
studentName: string;
degreeName: string;
institutionName: string;
dateAwarded: number;
}
export async function registerSchema() {
if (typeof window === 'undefined') {
throw new Error('Schema registration must run in the browser context.');
}
invariant('NEXT_PUBLIC_EAS_CONTRACT', easContractAddress);
const resolvedRegistryAddress = schemaRegistryAddressOverride || easContractAddress;
const ethereum = (window as typeof window & { ethereum?: unknown }).ethereum;
if (!ethereum) {
throw new Error('No injected wallet found. Please install MetaMask or another provider.');
}
const provider = new BrowserProvider(ethereum);
await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
const registry = new SchemaRegistry(resolvedRegistryAddress);
registry.connect(signer);
// Register the schema so future attestations can reference the resulting UID.
const tx = await registry.register({
schema: SCHEMA_DEFINITION,
resolverAddress: ZeroAddress,
revocable: true,
});
const schemaUid = await tx.wait();
return schemaUid;
}
export interface IssueResult {
uid: string;
transactionHash?: string;
}
export async function issueCredential(payload: CredentialPayload): Promise<IssueResult> {
invariant('NEXT_PUBLIC_SCHEMA_UID', schemaUidFromEnv);
const eas = await getSignerEas();
const schemaEncoder = new SchemaEncoder(SCHEMA_DEFINITION);
// Encode the credential fields based on the schema definition so they can be stored on-chain.
const encodedData = schemaEncoder.encodeData([
{ name: 'recipient', type: 'address', value: payload.recipient },
{ name: 'studentName', type: 'string', value: payload.studentName },
{ name: 'degreeName', type: 'string', value: payload.degreeName },
{ name: 'institutionName', type: 'string', value: payload.institutionName },
{ name: 'dateAwarded', type: 'uint64', value: BigInt(payload.dateAwarded) },
]);
const tx = await eas.attest({
schema: schemaUidFromEnv,
data: {
recipient: payload.recipient,
expirationTime: 0n,
revocable: true,
refUID: ZERO_BYTES32,
data: encodedData,
value: 0n,
},
});
const uid = await tx.wait();
return {
uid,
transactionHash: tx.receipt?.hash,
};
}
export async function getAttestation(uid: string): Promise<Attestation | null> {
const eas = getReadOnlyEas();
const attestation = await eas.getAttestation(uid);
if (!attestation.uid) {
return null;
}
return attestation;
}
export function getExplorerLink(uid: string) {
return https://explorer.testnet.rsk.co/attestation/${uid};
}
Understanding styles/globals.css
The globals.css file defines the base styling for the entire application using Tailwind CSS. It ensures consistent design, color themes, and typography across all pages.
**Purpose:
**Provide global styles like font setup, color themes (light/dark), and default layout behavior.
Key Features:
Imports Tailwind’s core layers:
@tailwind base;
@tailwind components;
@tailwind utilities;
Defines CSS variables for background and text colors using the :root selector.
Includes a dark mode setup that automatically adjusts colors when the user’s system prefers a dark theme.
Sets a global font family, text color, and ensures the body spans the full viewport height.
**In short:
**This file manages the global visual foundation of your dApp — providing a smooth, responsive, and theme-aware user interface with minimal CSS effort.
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #111827;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0f172a;
--foreground: #f8fafc;
}
}
body {
min-height: 100vh;
background-color: var(--background);
color: var(--foreground);
font-family: var(--font-geist-sans), system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
}
Setup Environment Variables
Before running the app, create a file named .env.local in your project root and add the following values and create your own NEXT_PUBLIC_SCHEMA_UID :
NEXT_PUBLIC_RPC_URL=https://public-node.testnet.rsk.co
NEXT_PUBLIC_EAS_CONTRACT=0xc300aeeadd60999933468738c9f5d7e9c0671e1c
NEXT_PUBLIC_SCHEMA_UID=0xa018ae66f5a8297cc1a13253dba7bc92455943ef19a7e161f7dc9da7204c4574
1. RPC URL
**Purpose:
**Defines the blockchain endpoint your dApp will connect to. The RPC (Remote Procedure Call) URL acts as a gateway between your app and the Rootstock blockchain.
**For Rootstock Testnet:
**You can use the public RPC endpoint provided by Rootstock:
👉 https://public-node.testnet.rsk.co
**Alternative RPCs:
**You can find other RPC URLs or run your own node from the official Rootstock documentation:
🔗 Rootstock Node Setup Guide
2. EAS Contract Address
**Purpose:
**Specifies the deployed Rootstock Attestation Service (RAS) contract address.
This contract handles all credential attestations and verifications on-chain.
Where to Find It:
The current Rootstock Testnet EAS contract address is:
0xc300aeeadd60999933468738c9f5d7e9c0671e1cYou can verify this address or find updates on:
🔗 EAS Documentation🔗 Rootstock Attestation Service Repo
3. Schema UID
**Purpose:
**Every credential you issue follows a schema, which defines the structure and data types of your attestation.
The Schema UID uniquely identifies that schema on-chain.
Where to Find It:
After running your registerSchema() function in the app, you’ll receive a UID (a 66-character hex string).
You can copy that UID from your console logs or view it on the Rootstock Explorer after registration.
**Example:
**0xa018ae66f5a8297cc1a13253dba7bc92455943ef19a7e161f7dc9da7204c4574
🔗 Rootstock Explorer (Testnet)
Run the Application
Once everything is set up, it’s time to run your project locally!
Step 1: Start the Development Server
Use the following command in your terminal:
npm run dev
If you’re using pnpm or yarn, you can run:
pnpm dev
# or
yarn dev
Step 2: Open in Browser
After the server starts, open your browser and visit:
👉 http://localhost:3000
You should see your main app interface running locally.
✅ Navigation Guide
| Route | Description |
| /issuer | Issue new credentials using the connected wallet. |
| /verify | Verify existing credentials using their unique UID. |
This confirms your EAS + Rootstock credential dApp is running correctly and connected to the blockchain. 🎉
🎉 Wrapping Up
Congratulations on successfully building and running your DApp for Issuing Verifiable Academic Credentials on the Rootstock Testnet! 🧠🎓
You’ve built a full-stack decentralized application that allows educational institutions to issue, store, and verify academic credentials as on-chain attestations using the Rootstock Attestation Service (RAS).
Join the Rootstock Community
If you’d like to keep learning, contributing, or need help troubleshooting, the Rootstock community is a great place to start:
🧠 Rootstock Docs: https://dev.rootstock.io/
💬 Rootstock Discord: discord.gg/rootstock
📱 Rootstock Telegram: https://dev.rootstock.io/
Also, don’t forget to check back to the project repo for reference or updates:
GitHub Link
This tutorial hopefully gave you a clear picture of how on-chain attestations can bring transparency, trust, and automation to the world of academic credentials.
With Rootstock’s EVM compatibility and the Ethereum Attestation Service SDK, developers can easily build verifiable identity and credential systems — not just for education, but for professional certifications, event badges, and decentralized identity frameworks too.
Keep experimenting, keep building, and keep contributing to the decentralized future! 🌍✨





