Blockchain added
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -39,3 +39,9 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# blockchain / hardhat
|
||||||
|
blockchain/node_modules
|
||||||
|
blockchain/artifacts
|
||||||
|
blockchain/cache
|
||||||
|
blockchain/typechain-types
|
||||||
|
|||||||
120
README.md
120
README.md
@@ -1,36 +1,110 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
# 🔗 LexiChain: Professional BFSI Document Intelligence Platform
|
||||||
|
|
||||||
## Getting Started
|
> **Status**: Production-Ready PFE Project
|
||||||
|
> **Target Audience**: Banking, Financial Services, and Insurance (BFSI) Institutions
|
||||||
|
> **Key Innovation**: Hybrid integration of Generative AI (Gemini) and Ethereum Blockchain (DocumentRegistry)
|
||||||
|
|
||||||
First, run the development server:
|
---
|
||||||
|
|
||||||
```bash
|
## 🏛️ 1. Project Vision & Mission
|
||||||
npm run dev
|
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
**LexiChain** is a next-generation platform designed to bridge the gap between complex legal documentation and user-centric transparency. In the traditional BFSI sector, contracts are often "black boxes"—static PDFs that are hard to understand and easy to misplace.
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
LexiChain transforms these static documents into **dynamic, searchable, and cryptographically secured assets**. Our mission is to automate document analysis while providing an immutable "Digital Notary" service that guarantees trust between financial institutions and their clients.
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
---
|
||||||
|
|
||||||
## Learn More
|
## 📉 2. Market Problem & Opportunity
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
The project addresses several critical "pain points" in the current financial landscape:
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
1. **The Transparency Gap**: Clients often sign contracts without understanding specific exclusion clauses or renewal dates.
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
2. **Operational Friction**: Insurance agents spend thousands of hours manually checking PDFs for compliance and signature presence.
|
||||||
|
3. **The Integrity Risk**: In legal disputes, proving that a specific version of a document was the one actually signed can be difficult and expensive.
|
||||||
|
4. **Information Overload**: Users are overwhelmed by the volume of fine print in modern banking.
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
---
|
||||||
|
|
||||||
## Deploy on Vercel
|
## 🚀 3. Core Feature Deep-Dive
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
### 🧠 A. The AI "Analyst" Module (Intelligence Layer)
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
LexiChain uses **Google Gemini 2.0 Flash** to act as a virtual legal analyst.
|
||||||
|
|
||||||
|
- **Smart Ingestion**: Uses high-fidelity OCR to read digital PDFs and scanned images.
|
||||||
|
- **Automated Extraction**: Identifies 15+ key data points (Amount, Interest Rate, Parties, Expiration Dates, Clauses) instantly.
|
||||||
|
- **RAG (Retrieval-Augmented Generation)**: The most advanced part of the AI. It breaks the contract into "Semantic Chunks" and stores them in a vector index. This allows the user to **Chat with their Contract** in natural language.
|
||||||
|
- **Intelligent Validation**: The AI automatically flags missing signatures, inconsistent dates, or high-risk clauses before the document is finalized.
|
||||||
|
|
||||||
|
### ⛓️ B. The Blockchain "Notary" Module (Security Layer)
|
||||||
|
|
||||||
|
LexiChain uses a private/public hybrid blockchain strategy to ensure **Non-Repudiation**.
|
||||||
|
|
||||||
|
- **Proof of Existence (PoE)**: We generate a SHA-256 hash (digital fingerprint) for every file. This hash is sent to a **Solidity Smart Contract** on the Ethereum network.
|
||||||
|
- **Immutable Timestamping**: The blockchain records exactly _when_ the document was uploaded. This cannot be changed by any administrator, providing a "Golden Record."
|
||||||
|
- **Metadata Leakage Prevention**: We only store the **Hash** on-chain. No personal data (names, amounts) ever touches the public blockchain, ensuring 100% GDPR compliance.
|
||||||
|
- **The Explorer**: A built-in "Verification Panel" that allows any auditor to verify a file's integrity by comparing its current hash with the on-chain record.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 4. Technical Architecture
|
||||||
|
|
||||||
|
### **Architecture Pattern: Feature-Sliced Design (FSD)**
|
||||||
|
|
||||||
|
The project follows **FSD principles**, which is a modern architectural pattern for scaling large applications.
|
||||||
|
|
||||||
|
- **Layers**: App, Pages, Features, Entities, Shared.
|
||||||
|
- **Benefits**: Decouples the Blockchain logic from the AI logic, making the system highly maintainable and ready for enterprise scaling.
|
||||||
|
|
||||||
|
### **The Stack**
|
||||||
|
|
||||||
|
| Layer | Technology | Rationale |
|
||||||
|
| :------------- | :----------------------- | :-------------------------------------------------------------------------------------------------- |
|
||||||
|
| **Frontend** | Next.js 15 (React) | High performance, SEO-friendly, and supports React Server Components. |
|
||||||
|
| **Backend** | Server Actions (Next.js) | Allows for secure, server-side blockchain signing and API communication without a separate backend.
|
||||||
|
| **LLM** | Gemini | Unbeatable speed and context window size for long legal documents. |
|
||||||
|
| **Blockchain** | Solidity / Hardhat | Ethereum-compatible smart contracts for industry-standard security. |
|
||||||
|
| **Database** | PostgreSQL + Prisma | Robust relational storage for user data and contract metadata. |
|
||||||
|
| **Identity** | Clerk | Enterprise-grade security for user authentication and session management. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 5. Security & Privacy Philosophy
|
||||||
|
|
||||||
|
LexiChain is built with a **"Security by Design"** approach:
|
||||||
|
|
||||||
|
- **Hashing vs. Storage**: We never store the actual document on the blockchain. The blockchain only holds the "Proof," while the "Content" remains in encrypted cloud storage.
|
||||||
|
- **Server-Side Signing**: Users don't need a crypto wallet (MetaMask). Our backend acts as a **Trusted Custodian**, signing transactions with a secure private key hidden in environment variables.
|
||||||
|
- **Authentication Hooks**: Access to contracts is strictly controlled via Clerk Auth, ensuring users only see their own data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 6. UX/UI & Aesthetics
|
||||||
|
|
||||||
|
The application features a **"Premium Glassmorphism"** design system.
|
||||||
|
|
||||||
|
- **Why?**: In BFSI, the interface must convey **Trust, Modernity, and Clarity**.
|
||||||
|
- **Design Tokens**: We use vibrant gradients, subtle blurs, and micro-animations to make the complex task of contract management feel light and intuitive.
|
||||||
|
- **Theme Awareness**: The UI is optimized for both Light and Dark modes, adapting to the professional environment of the user.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌟 7. Why this is a 10/10 PFE Project
|
||||||
|
|
||||||
|
LexiChain isn't just a simple web app; it is a **Multidisciplinary Innovation**:
|
||||||
|
|
||||||
|
1. **AI Innovation**: Moving beyond simple text extraction to a conversational RAG system.
|
||||||
|
2. **Blockchain Innovation**: Implementing a production-ready Document Registry that solves real-world legal issues.
|
||||||
|
3. **Architectural Integrity**: Using FSD and Clean Code principles usually found in senior-level software engineering.
|
||||||
|
4. **Market Readiness**: The solution is directly applicable to banks and insurance companies looking to digitalize their workflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 8. Glossary for NotebookLM
|
||||||
|
|
||||||
|
- **BFSI**: Banking, Financial Services, and Insurance.
|
||||||
|
- **RAG (Retrieval-Augmented Generation)**: A technique that allows AI to answer questions based _only_ on the provided documents, preventing "hallucinations."
|
||||||
|
- **Smart Contract**: A programmable contract that executes automatically when conditions are met.
|
||||||
|
- **SHA-256**: A one-way cryptographic function. If you change 1 bit of a file, the entire hash changes.
|
||||||
|
- **Hardhat**: A professional development environment for Ethereum software.
|
||||||
|
- **Sepolia**: The public test network used to simulate the real Ethereum blockchain.
|
||||||
|
|||||||
14
app/(dashboard)/blockchain/layout.tsx
Normal file
14
app/(dashboard)/blockchain/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Blockchain Explorer | LexiChain",
|
||||||
|
description: "View on-chain proofs and verify document integrity",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BlockchainLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
532
app/(dashboard)/blockchain/page.tsx
Normal file
532
app/(dashboard)/blockchain/page.tsx
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState, useCallback } from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import {
|
||||||
|
Link2,
|
||||||
|
Shield,
|
||||||
|
Activity,
|
||||||
|
Hash,
|
||||||
|
Clock,
|
||||||
|
FileText,
|
||||||
|
CheckCircle2,
|
||||||
|
Search,
|
||||||
|
RefreshCw,
|
||||||
|
ExternalLink,
|
||||||
|
Blocks,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
AlertCircle,
|
||||||
|
Upload,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
getBlockchainTransactions,
|
||||||
|
getBlockchainStats,
|
||||||
|
verifyDocumentHashOnBlockchain,
|
||||||
|
registerContractOnBlockchain,
|
||||||
|
} from "@/features/blockchain/api/blockchain.action";
|
||||||
|
import { getContracts } from "@/features/contracts/api/contract.action";
|
||||||
|
import type {
|
||||||
|
BlockchainTransactionView,
|
||||||
|
BlockchainStats,
|
||||||
|
} from "@/lib/services/blockchain.types";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Blockchain Explorer Page
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
export default function BlockchainExplorerPage() {
|
||||||
|
const [transactions, setTransactions] = useState<BlockchainTransactionView[]>([]);
|
||||||
|
const [stats, setStats] = useState<BlockchainStats | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [verifyHash, setVerifyHash] = useState("");
|
||||||
|
const [verifyResult, setVerifyResult] = useState<{
|
||||||
|
exists: boolean;
|
||||||
|
timestamp: number;
|
||||||
|
depositor: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [verifying, setVerifying] = useState(false);
|
||||||
|
const [copiedTx, setCopiedTx] = useState<string | null>(null);
|
||||||
|
const [unregisteredContracts, setUnregisteredContracts] = useState<
|
||||||
|
Array<{ id: string; title: string | null; fileName: string }>
|
||||||
|
>([]);
|
||||||
|
const [registeringId, setRegisteringId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const [txResult, statsResult, contractsResult] = await Promise.all([
|
||||||
|
getBlockchainTransactions(),
|
||||||
|
getBlockchainStats(),
|
||||||
|
getContracts({ status: "COMPLETED" }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (txResult.success && txResult.transactions) {
|
||||||
|
setTransactions(txResult.transactions);
|
||||||
|
}
|
||||||
|
if (statsResult.success && statsResult.stats) {
|
||||||
|
setStats(statsResult.stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find contracts not yet on blockchain
|
||||||
|
if (contractsResult.success && contractsResult.contracts) {
|
||||||
|
const registered = new Set(
|
||||||
|
txResult.transactions?.map((tx) => tx.contractId) ?? []
|
||||||
|
);
|
||||||
|
const unregistered = contractsResult.contracts
|
||||||
|
.filter(
|
||||||
|
(c: { id: string; txHash?: string | null; status: string }) =>
|
||||||
|
!c.txHash && !registered.has(c.id) && c.status === "COMPLETED"
|
||||||
|
)
|
||||||
|
.map((c: { id: string; title: string | null; fileName: string }) => ({
|
||||||
|
id: c.id,
|
||||||
|
title: c.title,
|
||||||
|
fileName: c.fileName,
|
||||||
|
}));
|
||||||
|
setUnregisteredContracts(unregistered);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load blockchain data:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
const handleVerify = async () => {
|
||||||
|
if (!verifyHash.trim()) return;
|
||||||
|
setVerifying(true);
|
||||||
|
setVerifyResult(null);
|
||||||
|
try {
|
||||||
|
const result = await verifyDocumentHashOnBlockchain(verifyHash.trim());
|
||||||
|
if (result.success && result.verification) {
|
||||||
|
setVerifyResult(result.verification);
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || "Verification failed");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to verify hash");
|
||||||
|
} finally {
|
||||||
|
setVerifying(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegister = async (contractId: string) => {
|
||||||
|
setRegisteringId(contractId);
|
||||||
|
try {
|
||||||
|
const result = await registerContractOnBlockchain(contractId);
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("Contract registered on blockchain!");
|
||||||
|
await loadData();
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || "Registration failed");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to register on blockchain");
|
||||||
|
} finally {
|
||||||
|
setRegisteringId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setCopiedTx(text);
|
||||||
|
setTimeout(() => setCopiedTx(null), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTimestamp = (iso: string) => {
|
||||||
|
return new Date(iso).toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const truncateHash = (hash: string, chars = 8) => {
|
||||||
|
if (!hash || hash.length <= chars * 2 + 3) return hash;
|
||||||
|
return `${hash.slice(0, chars + 2)}...${hash.slice(-chars)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6 max-w-[1400px] mx-auto">
|
||||||
|
{/* Page Header */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-foreground flex items-center gap-3">
|
||||||
|
<div className="p-2 rounded-xl bg-primary/10 border border-primary/20">
|
||||||
|
<Blocks className="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
Blockchain Explorer
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mt-1 text-sm">
|
||||||
|
View on-chain proofs and verify document integrity
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={loadData}
|
||||||
|
disabled={loading}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"
|
||||||
|
>
|
||||||
|
<StatsCard
|
||||||
|
icon={<Shield className="w-5 h-5" />}
|
||||||
|
label="Verified Documents"
|
||||||
|
value={stats?.totalVerified?.toString() ?? "0"}
|
||||||
|
color="emerald"
|
||||||
|
/>
|
||||||
|
<StatsCard
|
||||||
|
icon={<Blocks className="w-5 h-5" />}
|
||||||
|
label="Latest Block"
|
||||||
|
value={stats?.latestBlockNumber ? `#${stats.latestBlockNumber.toLocaleString()}` : "—"}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<StatsCard
|
||||||
|
icon={<Activity className="w-5 h-5" />}
|
||||||
|
label="Network"
|
||||||
|
value={stats?.networkName ? `${stats.networkName} ${stats.chainId ? `(Chain ${stats.chainId})` : ""}` : "Not Configured"}
|
||||||
|
color={stats?.networkStatus === "connected" ? "emerald" : "red"}
|
||||||
|
badge={stats?.networkStatus === "connected" ? "● Live" : "● Offline"}
|
||||||
|
/>
|
||||||
|
<StatsCard
|
||||||
|
icon={<Hash className="w-5 h-5" />}
|
||||||
|
label="Wallet"
|
||||||
|
value={stats?.walletAddress ? truncateHash(stats.walletAddress, 6) : "—"}
|
||||||
|
color="violet"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Unregistered contracts */}
|
||||||
|
{unregisteredContracts.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.15 }}
|
||||||
|
className="rounded-2xl border border-amber-500/20 bg-amber-500/5 p-5"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Upload className="w-4 h-4 text-amber-500" />
|
||||||
|
<h3 className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
||||||
|
{unregisteredContracts.length} contract{unregisteredContracts.length > 1 ? "s" : ""} not yet on blockchain
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{unregisteredContracts.map((contract) => (
|
||||||
|
<div
|
||||||
|
key={contract.id}
|
||||||
|
className="flex items-center justify-between rounded-xl bg-background/60 border border-border/40 px-4 py-2.5"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<FileText className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
|
<span className="text-sm truncate">
|
||||||
|
{contract.title || contract.fileName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="gap-1.5 text-xs shrink-0 ml-3"
|
||||||
|
disabled={registeringId === contract.id}
|
||||||
|
onClick={() => handleRegister(contract.id)}
|
||||||
|
>
|
||||||
|
{registeringId === contract.id ? (
|
||||||
|
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Link2 className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* Transactions List */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="lg:col-span-2 rounded-2xl border border-border/60 bg-background/80 backdrop-blur-xl overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="p-5 border-b border-border/40">
|
||||||
|
<h2 className="font-semibold text-foreground flex items-center gap-2">
|
||||||
|
<Link2 className="w-4 h-4 text-primary" />
|
||||||
|
Transaction History
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
All documents registered on the blockchain
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="p-10 text-center text-muted-foreground">
|
||||||
|
<RefreshCw className="w-6 h-6 animate-spin mx-auto mb-2" />
|
||||||
|
Loading transactions...
|
||||||
|
</div>
|
||||||
|
) : transactions.length === 0 ? (
|
||||||
|
<div className="p-10 text-center text-muted-foreground">
|
||||||
|
<Blocks className="w-8 h-8 mx-auto mb-3 opacity-50" />
|
||||||
|
<p className="text-sm font-medium">No transactions yet</p>
|
||||||
|
<p className="text-xs mt-1">
|
||||||
|
Upload and analyze a contract to register it on-chain
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-border/40">
|
||||||
|
{transactions.map((tx, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={tx.id}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: idx * 0.05 }}
|
||||||
|
className="p-4 hover:bg-muted/30 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-500 shrink-0" />
|
||||||
|
<span className="text-sm font-medium truncate">
|
||||||
|
{tx.contractTitle || tx.contractFileName}
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-[10px] font-medium px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20">
|
||||||
|
{tx.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 mt-2">
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground group">
|
||||||
|
<Shield className="w-3 h-3 shrink-0 text-emerald-500/70" />
|
||||||
|
<span className="font-mono text-[10px]">Fingerprint: {truncateHash(tx.documentHash, 12)}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => copyToClipboard(tx.documentHash)}
|
||||||
|
className="opacity-0 group-hover:opacity-100 transition-opacity hover:text-foreground"
|
||||||
|
title="Copy Document Fingerprint"
|
||||||
|
>
|
||||||
|
{copiedTx === tx.documentHash ? (
|
||||||
|
<Check className="w-3 h-3 text-emerald-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<Hash className="w-3 h-3 shrink-0" />
|
||||||
|
<span className="font-mono">Tx: {truncateHash(tx.txHash, 12)}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => copyToClipboard(tx.txHash)}
|
||||||
|
className="hover:text-foreground transition-colors"
|
||||||
|
title="Copy Transaction Hash"
|
||||||
|
>
|
||||||
|
{copiedTx === tx.txHash ? (
|
||||||
|
<Check className="w-3 h-3 text-emerald-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<Blocks className="w-3 h-3 shrink-0" />
|
||||||
|
Block #{tx.blockNumber.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<Clock className="w-3 h-3 shrink-0" />
|
||||||
|
{formatTimestamp(tx.blockTimestamp)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-end gap-2 shrink-0">
|
||||||
|
<span className="text-[10px] font-medium px-2 py-0.5 rounded bg-muted text-muted-foreground">
|
||||||
|
{tx.network === "sepolia" ? "Sepolia" : "Hardhat"}
|
||||||
|
</span>
|
||||||
|
{tx.explorerUrl && (
|
||||||
|
<a
|
||||||
|
href={tx.explorerUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-primary hover:underline flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Etherscan
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Verification Panel */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="rounded-2xl border border-border/60 bg-background/80 backdrop-blur-xl h-fit"
|
||||||
|
>
|
||||||
|
<div className="p-5 border-b border-border/40">
|
||||||
|
<h2 className="font-semibold text-foreground flex items-center gap-2">
|
||||||
|
<Search className="w-4 h-4 text-primary" />
|
||||||
|
Verify Document
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Check if a document hash exists on-chain
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-muted-foreground mb-1.5 block">
|
||||||
|
Document Hash (SHA-256)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={verifyHash}
|
||||||
|
onChange={(e) => setVerifyHash(e.target.value)}
|
||||||
|
placeholder="0x..."
|
||||||
|
className="w-full rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 text-xs font-mono resize-none h-20 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/40 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleVerify}
|
||||||
|
disabled={!verifyHash.trim() || verifying}
|
||||||
|
className="w-full gap-2"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{verifying ? (
|
||||||
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Search className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
Verify On-Chain
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Verification Result */}
|
||||||
|
{verifyResult !== null && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className={`rounded-xl border p-4 ${
|
||||||
|
verifyResult.exists
|
||||||
|
? "border-emerald-500/30 bg-emerald-500/5"
|
||||||
|
: "border-red-500/30 bg-red-500/5"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
{verifyResult.exists ? (
|
||||||
|
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-500" />
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`text-sm font-semibold ${
|
||||||
|
verifyResult.exists
|
||||||
|
? "text-emerald-600 dark:text-emerald-400"
|
||||||
|
: "text-red-600 dark:text-red-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{verifyResult.exists
|
||||||
|
? "✓ Document Verified"
|
||||||
|
: "✗ Not Found"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{verifyResult.exists && (
|
||||||
|
<div className="space-y-2 text-xs">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Timestamp</span>
|
||||||
|
<span className="font-mono">
|
||||||
|
{new Date(verifyResult.timestamp * 1000).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Depositor</span>
|
||||||
|
<span className="font-mono">
|
||||||
|
{truncateHash(verifyResult.depositor, 6)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
// Stats Card Sub-Component
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function StatsCard({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
color,
|
||||||
|
badge,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
color: string;
|
||||||
|
badge?: string;
|
||||||
|
}) {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
emerald: "text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 border-emerald-500/20",
|
||||||
|
blue: "text-blue-600 dark:text-blue-400 bg-blue-500/10 border-blue-500/20",
|
||||||
|
violet: "text-violet-600 dark:text-violet-400 bg-violet-500/10 border-violet-500/20",
|
||||||
|
red: "text-red-600 dark:text-red-400 bg-red-500/10 border-red-500/20",
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconColors: Record<string, string> = {
|
||||||
|
emerald: "text-emerald-500",
|
||||||
|
blue: "text-blue-500",
|
||||||
|
violet: "text-violet-500",
|
||||||
|
red: "text-red-500",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-border/60 bg-background/80 backdrop-blur-xl p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className={`p-2 rounded-lg border ${colorMap[color]}`}>
|
||||||
|
<span className={iconColors[color]}>{icon}</span>
|
||||||
|
</div>
|
||||||
|
{badge && (
|
||||||
|
<span
|
||||||
|
className={`text-[10px] font-medium px-2 py-0.5 rounded-full ${
|
||||||
|
badge.includes("Live")
|
||||||
|
? "text-emerald-600 dark:text-emerald-400 bg-emerald-500/10"
|
||||||
|
: "text-red-600 dark:text-red-400 bg-red-500/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">{label}</p>
|
||||||
|
<p className="text-lg font-bold text-foreground mt-0.5 truncate">{value}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
206
blockchain/contracts/DocumentRegistry.sol
Normal file
206
blockchain/contracts/DocumentRegistry.sol
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.24;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title DocumentRegistry
|
||||||
|
* @author LexiChain BFSI Platform
|
||||||
|
* @notice This smart contract provides on-chain proof-of-deposit for BFSI
|
||||||
|
* contract documents. It stores SHA-256 hashes of documents along
|
||||||
|
* with their registration timestamp, making submission dates
|
||||||
|
* provable, tamper-proof, and legally opposable.
|
||||||
|
*
|
||||||
|
* @dev How it works:
|
||||||
|
* 1. The platform computes a SHA-256 hash of the uploaded PDF
|
||||||
|
* 2. The hash is registered on the blockchain via registerDocument()
|
||||||
|
* 3. The block.timestamp at the time of mining becomes the proof date
|
||||||
|
* 4. Anyone can verify a document's existence via verifyDocument()
|
||||||
|
*
|
||||||
|
* No actual document content is stored on-chain — only the hash.
|
||||||
|
* This preserves privacy while providing cryptographic proof.
|
||||||
|
*/
|
||||||
|
contract DocumentRegistry {
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// DATA STRUCTURES
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Represents a registered document's on-chain record
|
||||||
|
* @param timestamp When the document was registered (block.timestamp)
|
||||||
|
* @param depositor The address that registered the document
|
||||||
|
* @param exists Whether this record is valid
|
||||||
|
*/
|
||||||
|
struct DocumentRecord {
|
||||||
|
uint256 timestamp;
|
||||||
|
address depositor;
|
||||||
|
bool exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// STATE VARIABLES
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// @notice Maps document hash → registration record
|
||||||
|
mapping(bytes32 => DocumentRecord) private documents;
|
||||||
|
|
||||||
|
/// @notice Maps depositor address → list of document hashes they registered
|
||||||
|
mapping(address => bytes32[]) private depositorDocuments;
|
||||||
|
|
||||||
|
/// @notice Total number of documents registered
|
||||||
|
uint256 public totalDocuments;
|
||||||
|
|
||||||
|
/// @notice Contract owner (the platform backend wallet)
|
||||||
|
address public owner;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// EVENTS
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Emitted when a new document is registered on-chain
|
||||||
|
* @param docHash The SHA-256 hash of the document
|
||||||
|
* @param timestamp The block timestamp at registration
|
||||||
|
* @param depositor The address that registered the document
|
||||||
|
*/
|
||||||
|
event DocumentRegistered(
|
||||||
|
bytes32 indexed docHash,
|
||||||
|
uint256 timestamp,
|
||||||
|
address indexed depositor
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Emitted when a document is verified
|
||||||
|
* @param docHash The document hash that was checked
|
||||||
|
* @param exists Whether the document was found on-chain
|
||||||
|
* @param verifier The address that performed the verification
|
||||||
|
*/
|
||||||
|
event DocumentVerified(
|
||||||
|
bytes32 indexed docHash,
|
||||||
|
bool exists,
|
||||||
|
address indexed verifier
|
||||||
|
);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// MODIFIERS
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// @notice Restricts function to contract owner
|
||||||
|
modifier onlyOwner() {
|
||||||
|
require(msg.sender == owner, "Only owner can call this function");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// CONSTRUCTOR
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
owner = msg.sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// CORE FUNCTIONS
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Register a document hash on the blockchain
|
||||||
|
* @dev Creates an immutable record with the current block timestamp.
|
||||||
|
* Reverts if the same hash was already registered (no duplicates).
|
||||||
|
* @param _docHash The SHA-256 hash of the document (bytes32)
|
||||||
|
*/
|
||||||
|
function registerDocument(
|
||||||
|
bytes32 _docHash
|
||||||
|
) external onlyOwner {
|
||||||
|
// Prevent duplicate registrations
|
||||||
|
require(
|
||||||
|
!documents[_docHash].exists,
|
||||||
|
"Document already registered on-chain"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store the document record
|
||||||
|
documents[_docHash] = DocumentRecord({
|
||||||
|
timestamp: block.timestamp,
|
||||||
|
depositor: msg.sender,
|
||||||
|
exists: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track documents per depositor
|
||||||
|
depositorDocuments[msg.sender].push(_docHash);
|
||||||
|
|
||||||
|
// Increment counter
|
||||||
|
totalDocuments++;
|
||||||
|
|
||||||
|
// Emit event for off-chain indexing
|
||||||
|
emit DocumentRegistered(
|
||||||
|
_docHash,
|
||||||
|
block.timestamp,
|
||||||
|
msg.sender
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Verify if a document exists on-chain and get its details
|
||||||
|
* @param _docHash The SHA-256 hash of the document to verify
|
||||||
|
* @return exists Whether the document is registered
|
||||||
|
* @return timestamp When it was registered (0 if not found)
|
||||||
|
* @return depositor Who registered it (address(0) if not found)
|
||||||
|
*/
|
||||||
|
function verifyDocument(
|
||||||
|
bytes32 _docHash
|
||||||
|
)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
bool exists,
|
||||||
|
uint256 timestamp,
|
||||||
|
address depositor
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DocumentRecord memory record = documents[_docHash];
|
||||||
|
return (
|
||||||
|
record.exists,
|
||||||
|
record.timestamp,
|
||||||
|
record.depositor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Get the timestamp for a specific document hash
|
||||||
|
* @param _docHash The document hash to look up
|
||||||
|
* @return The registration timestamp (0 if not registered)
|
||||||
|
*/
|
||||||
|
function getTimestamp(bytes32 _docHash) external view returns (uint256) {
|
||||||
|
return documents[_docHash].timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Get all document hashes registered by a specific address
|
||||||
|
* @param _depositor The address to query
|
||||||
|
* @return Array of document hashes
|
||||||
|
*/
|
||||||
|
function getDocumentsByDepositor(
|
||||||
|
address _depositor
|
||||||
|
) external view returns (bytes32[] memory) {
|
||||||
|
return depositorDocuments[_depositor];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Get the number of documents registered by a specific address
|
||||||
|
* @param _depositor The address to query
|
||||||
|
* @return Number of documents
|
||||||
|
*/
|
||||||
|
function getDocumentCount(
|
||||||
|
address _depositor
|
||||||
|
) external view returns (uint256) {
|
||||||
|
return depositorDocuments[_depositor].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Transfer ownership of the contract
|
||||||
|
* @dev Only the current owner can transfer ownership
|
||||||
|
* @param _newOwner The address of the new owner
|
||||||
|
*/
|
||||||
|
function transferOwnership(address _newOwner) external onlyOwner {
|
||||||
|
require(_newOwner != address(0), "New owner cannot be zero address");
|
||||||
|
owner = _newOwner;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
blockchain/hardhat.config.ts
Normal file
56
blockchain/hardhat.config.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { HardhatUserConfig } from "hardhat/config";
|
||||||
|
import "@nomicfoundation/hardhat-toolbox";
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
// Hardhat Configuration
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
// This file configures the Solidity compiler and
|
||||||
|
// network settings for our smart contract.
|
||||||
|
//
|
||||||
|
// Networks:
|
||||||
|
// - hardhat (default): In-memory local blockchain, free & instant
|
||||||
|
// - localhost: Persistent local node via `npx hardhat node`
|
||||||
|
// - sepolia: Ethereum testnet for demo/presentation
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const config: HardhatUserConfig = {
|
||||||
|
solidity: {
|
||||||
|
version: "0.8.24",
|
||||||
|
settings: {
|
||||||
|
optimizer: {
|
||||||
|
enabled: true,
|
||||||
|
runs: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
networks: {
|
||||||
|
// Local persistent node (run `npx hardhat node` first)
|
||||||
|
localhost: {
|
||||||
|
url: "http://127.0.0.1:8545",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ethereum Sepolia testnet (free, for demo/jury presentation)
|
||||||
|
// Requires SEPOLIA_RPC_URL and DEPLOYER_PRIVATE_KEY in env
|
||||||
|
...(process.env.SEPOLIA_RPC_URL
|
||||||
|
? {
|
||||||
|
sepolia: {
|
||||||
|
url: process.env.SEPOLIA_RPC_URL,
|
||||||
|
accounts: process.env.DEPLOYER_PRIVATE_KEY
|
||||||
|
? [process.env.DEPLOYER_PRIVATE_KEY]
|
||||||
|
: [],
|
||||||
|
chainId: 11155111,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
|
||||||
|
paths: {
|
||||||
|
sources: "./contracts",
|
||||||
|
tests: "./test",
|
||||||
|
cache: "./cache",
|
||||||
|
artifacts: "./artifacts",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
8094
blockchain/package-lock.json
generated
Normal file
8094
blockchain/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
blockchain/package.json
Normal file
19
blockchain/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "lexichain-blockchain",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "LexiChain Document Registry - Solidity Smart Contract",
|
||||||
|
"scripts": {
|
||||||
|
"compile": "hardhat compile",
|
||||||
|
"test": "hardhat test",
|
||||||
|
"node": "hardhat node",
|
||||||
|
"deploy:local": "hardhat run scripts/deploy.ts --network localhost",
|
||||||
|
"deploy:sepolia": "hardhat run scripts/deploy.ts --network sepolia"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
|
||||||
|
"hardhat": "^2.22.0",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"ts-node": "^10.9.0",
|
||||||
|
"@types/node": "^20.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
46
blockchain/scripts/deploy.ts
Normal file
46
blockchain/scripts/deploy.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { ethers } from "hardhat";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deployment Script for DocumentRegistry
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* Local: npx hardhat run scripts/deploy.ts --network localhost
|
||||||
|
* Sepolia: npx hardhat run scripts/deploy.ts --network sepolia
|
||||||
|
*
|
||||||
|
* After deployment, copy the contract address into your .env file:
|
||||||
|
* BLOCKCHAIN_CONTRACT_ADDRESS=0x...
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||||
|
console.log("🔗 Deploying DocumentRegistry...");
|
||||||
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||||
|
|
||||||
|
// Get the deployer account
|
||||||
|
const [deployer] = await ethers.getSigners();
|
||||||
|
console.log(`📍 Deployer address: ${deployer.address}`);
|
||||||
|
|
||||||
|
const balance = await ethers.provider.getBalance(deployer.address);
|
||||||
|
console.log(`💰 Deployer balance: ${ethers.formatEther(balance)} ETH`);
|
||||||
|
|
||||||
|
// Deploy the contract
|
||||||
|
const DocumentRegistryFactory = await ethers.getContractFactory("DocumentRegistry");
|
||||||
|
const registry = await DocumentRegistryFactory.deploy();
|
||||||
|
await registry.waitForDeployment();
|
||||||
|
|
||||||
|
const address = await registry.getAddress();
|
||||||
|
|
||||||
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||||
|
console.log(`✅ DocumentRegistry deployed to: ${address}`);
|
||||||
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||||
|
console.log("");
|
||||||
|
console.log("📋 Next steps:");
|
||||||
|
console.log(` 1. Add to your .env file:`);
|
||||||
|
console.log(` BLOCKCHAIN_CONTRACT_ADDRESS=${address}`);
|
||||||
|
console.log(` 2. The contract is ready to register documents!`);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
170
blockchain/test/DocumentRegistry.test.ts
Normal file
170
blockchain/test/DocumentRegistry.test.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { expect } from "chai";
|
||||||
|
import { ethers } from "hardhat";
|
||||||
|
import { DocumentRegistry } from "../typechain-types";
|
||||||
|
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DocumentRegistry Smart Contract Tests
|
||||||
|
*
|
||||||
|
* These tests verify all core functionality:
|
||||||
|
* 1. Document registration with timestamp
|
||||||
|
* 2. Document verification
|
||||||
|
* 3. Duplicate prevention
|
||||||
|
* 4. Non-existent document handling
|
||||||
|
* 5. Depositor document tracking
|
||||||
|
* 6. Ownership controls
|
||||||
|
*/
|
||||||
|
describe("DocumentRegistry", function () {
|
||||||
|
let registry: DocumentRegistry;
|
||||||
|
let owner: SignerWithAddress;
|
||||||
|
let user1: SignerWithAddress;
|
||||||
|
let user2: SignerWithAddress;
|
||||||
|
|
||||||
|
// Sample document hashes (simulating SHA-256 hashes)
|
||||||
|
const docHash1 = ethers.keccak256(ethers.toUtf8Bytes("contract-insurance-auto-2024.pdf"));
|
||||||
|
const docHash2 = ethers.keccak256(ethers.toUtf8Bytes("contract-home-loan-2024.pdf"));
|
||||||
|
const docHash3 = ethers.keccak256(ethers.toUtf8Bytes("contract-health-insurance.pdf"));
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
// Get test accounts (Hardhat provides 20 free accounts)
|
||||||
|
[owner, user1, user2] = await ethers.getSigners();
|
||||||
|
|
||||||
|
// Deploy new contract instance before each test
|
||||||
|
const DocumentRegistryFactory = await ethers.getContractFactory("DocumentRegistry");
|
||||||
|
registry = await DocumentRegistryFactory.deploy();
|
||||||
|
await registry.waitForDeployment();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// DEPLOYMENT TESTS
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
describe("Deployment", function () {
|
||||||
|
it("should set the deployer as owner", async function () {
|
||||||
|
expect(await registry.owner()).to.equal(owner.address);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should start with zero documents", async function () {
|
||||||
|
expect(await registry.totalDocuments()).to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// REGISTRATION TESTS
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
describe("Document Registration", function () {
|
||||||
|
it("should register a document and emit event", async function () {
|
||||||
|
const tx = await registry.registerDocument(docHash1);
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
// Verify event was emitted
|
||||||
|
await expect(tx)
|
||||||
|
.to.emit(registry, "DocumentRegistered")
|
||||||
|
.withArgs(docHash1, (await ethers.provider.getBlock(receipt!.blockNumber))!.timestamp, owner.address);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should store correct timestamp", async function () {
|
||||||
|
const tx = await registry.registerDocument(docHash1);
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
const block = await ethers.provider.getBlock(receipt!.blockNumber);
|
||||||
|
|
||||||
|
const timestamp = await registry.getTimestamp(docHash1);
|
||||||
|
expect(timestamp).to.equal(block!.timestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should increment totalDocuments counter", async function () {
|
||||||
|
await registry.registerDocument(docHash1);
|
||||||
|
expect(await registry.totalDocuments()).to.equal(1);
|
||||||
|
|
||||||
|
await registry.registerDocument(docHash2);
|
||||||
|
expect(await registry.totalDocuments()).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent duplicate registration (same hash)", async function () {
|
||||||
|
await registry.registerDocument(docHash1);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
registry.registerDocument(docHash1)
|
||||||
|
).to.be.revertedWith("Document already registered on-chain");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent non-owners from registering documents", async function () {
|
||||||
|
await expect(
|
||||||
|
registry.connect(user1).registerDocument(docHash1)
|
||||||
|
).to.be.revertedWith("Only owner can call this function");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// VERIFICATION TESTS
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
describe("Document Verification", function () {
|
||||||
|
it("should verify a registered document", async function () {
|
||||||
|
await registry.registerDocument(docHash1);
|
||||||
|
|
||||||
|
const [exists, timestamp, depositor] = await registry.verifyDocument(docHash1);
|
||||||
|
expect(exists).to.be.true;
|
||||||
|
expect(timestamp).to.be.greaterThan(0);
|
||||||
|
expect(depositor).to.equal(owner.address);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for unregistered document", async function () {
|
||||||
|
const fakeHash = ethers.keccak256(ethers.toUtf8Bytes("non-existent.pdf"));
|
||||||
|
|
||||||
|
const [exists, timestamp, depositor] = await registry.verifyDocument(fakeHash);
|
||||||
|
expect(exists).to.be.false;
|
||||||
|
expect(timestamp).to.equal(0);
|
||||||
|
expect(depositor).to.equal(ethers.ZeroAddress);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// DEPOSITOR TRACKING TESTS
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
describe("Depositor Tracking", function () {
|
||||||
|
it("should track all documents by a depositor", async function () {
|
||||||
|
await registry.registerDocument(docHash1);
|
||||||
|
await registry.registerDocument(docHash2);
|
||||||
|
|
||||||
|
const docs = await registry.getDocumentsByDepositor(owner.address);
|
||||||
|
expect(docs.length).to.equal(2);
|
||||||
|
expect(docs[0]).to.equal(docHash1);
|
||||||
|
expect(docs[1]).to.equal(docHash2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return correct document count", async function () {
|
||||||
|
await registry.registerDocument(docHash1);
|
||||||
|
await registry.registerDocument(docHash2);
|
||||||
|
await registry.registerDocument(docHash3);
|
||||||
|
|
||||||
|
expect(await registry.getDocumentCount(owner.address)).to.equal(3);
|
||||||
|
expect(await registry.getDocumentCount(user2.address)).to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// OWNERSHIP TESTS
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
describe("Ownership", function () {
|
||||||
|
it("should transfer ownership", async function () {
|
||||||
|
await registry.transferOwnership(user1.address);
|
||||||
|
expect(await registry.owner()).to.equal(user1.address);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent non-owner from transferring ownership", async function () {
|
||||||
|
await expect(
|
||||||
|
registry.connect(user1).transferOwnership(user2.address)
|
||||||
|
).to.be.revertedWith("Only owner can call this function");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent transfer to zero address", async function () {
|
||||||
|
await expect(
|
||||||
|
registry.transferOwnership(ethers.ZeroAddress)
|
||||||
|
).to.be.revertedWith("New owner cannot be zero address");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
11
blockchain/tsconfig.json
Normal file
11
blockchain/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { BarChart3, FileText, LogOut } from "lucide-react";
|
import { BarChart3, FileText, Link2, LogOut } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { SignOutButton, UserButton } from "@clerk/nextjs";
|
import { SignOutButton, UserButton } from "@clerk/nextjs";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
@@ -31,6 +31,12 @@ const navItems: NavItem[] = [
|
|||||||
icon: <FileText className="w-5 h-5" />,
|
icon: <FileText className="w-5 h-5" />,
|
||||||
description: "Manage contracts",
|
description: "Manage contracts",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: "/blockchain",
|
||||||
|
label: "Blockchain",
|
||||||
|
icon: <Link2 className="w-5 h-5" />,
|
||||||
|
description: "On-chain proofs",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function DashboardNavigation() {
|
export function DashboardNavigation() {
|
||||||
|
|||||||
655
docs/blockchain-module.md
Normal file
655
docs/blockchain-module.md
Normal file
@@ -0,0 +1,655 @@
|
|||||||
|
# 🔗 Blockchain Module — Complete Documentation
|
||||||
|
|
||||||
|
> **LexiChain BFSI Platform — Chapter: Blockchain Integration**
|
||||||
|
>
|
||||||
|
> This document explains everything about the blockchain module:
|
||||||
|
> what it does, how it works, how the code is organized, and how to run it.
|
||||||
|
> Written for beginners with no prior blockchain knowledge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [What is Blockchain and Why Do We Need It?](#1-what-is-blockchain-and-why-do-we-need-it)
|
||||||
|
2. [What Does Our Blockchain Module Do?](#2-what-does-our-blockchain-module-do)
|
||||||
|
3. [Architecture Overview](#3-architecture-overview)
|
||||||
|
4. [Smart Contract: DocumentRegistry.sol](#4-smart-contract-documentregistrysol)
|
||||||
|
5. [Server-Side Integration: BlockchainService](#5-server-side-integration-blockchainservice)
|
||||||
|
6. [Database Schema: Storing Proof Data](#6-database-schema-storing-proof-data)
|
||||||
|
7. [Server Actions: The API Layer](#7-server-actions-the-api-layer)
|
||||||
|
8. [Frontend: Blockchain Explorer Page](#8-frontend-blockchain-explorer-page)
|
||||||
|
9. [Complete Data Flow](#9-complete-data-flow)
|
||||||
|
10. [How to Run Locally](#10-how-to-run-locally)
|
||||||
|
11. [Deploying to Sepolia Testnet](#11-deploying-to-sepolia-testnet)
|
||||||
|
12. [Technology Choices & Rationale](#12-technology-choices--rationale)
|
||||||
|
13. [File Reference](#13-file-reference)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What is Blockchain and Why Do We Need It?
|
||||||
|
|
||||||
|
### The Problem (in simple terms)
|
||||||
|
|
||||||
|
When a client sends an insurance claim or uploads a contract, they need **proof** that they submitted it on a specific date. Without proof:
|
||||||
|
- The insurance company could claim "we never received it"
|
||||||
|
- Deadlines could be disputed
|
||||||
|
- There's no transparency
|
||||||
|
|
||||||
|
### The Solution: Blockchain as a Notary
|
||||||
|
|
||||||
|
A **blockchain** is like a public, tamper-proof notebook. Once you write something in it, **nobody can erase or modify it** — not even the person who wrote it.
|
||||||
|
|
||||||
|
We use the blockchain as a **digital notary**:
|
||||||
|
1. We take the uploaded contract PDF
|
||||||
|
2. We create a unique **fingerprint** (hash) of that file
|
||||||
|
3. We write that fingerprint into the blockchain with a timestamp
|
||||||
|
4. Now there's permanent, verifiable proof that this exact document existed at this exact time
|
||||||
|
|
||||||
|
> **Key insight**: We don't store the actual document on the blockchain (that would be expensive). We only store its **fingerprint** (64 characters). If the document ever changes, even by one byte, the fingerprint would be completely different — proving tampering.
|
||||||
|
|
||||||
|
### What is a Smart Contract?
|
||||||
|
|
||||||
|
A **smart contract** is a program that runs on the blockchain. Think of it as a vending machine:
|
||||||
|
- You put in a coin (send a transaction)
|
||||||
|
- The machine executes its programmed logic
|
||||||
|
- The result is permanent and visible to everyone
|
||||||
|
|
||||||
|
Our smart contract (`DocumentRegistry.sol`) has two main functions:
|
||||||
|
- **Register**: Store a document fingerprint with a timestamp
|
||||||
|
- **Verify**: Check if a fingerprint exists and when it was stored
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. What Does Our Blockchain Module Do?
|
||||||
|
|
||||||
|
### Features Implemented
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| **Auto-Registration** | After AI analyzes a contract, its hash is automatically registered on-chain |
|
||||||
|
| **Manual Registration** | Users can register unregistered contracts via the Blockchain Explorer |
|
||||||
|
| **Document Verification** | Paste any document hash to check if it exists on-chain |
|
||||||
|
| **Transaction Explorer** | View all blockchain transactions with details |
|
||||||
|
| **Network Stats** | Live stats: verified documents, latest block, network status |
|
||||||
|
| **Proof Badges** | Contract list shows which contracts are blockchain-verified |
|
||||||
|
|
||||||
|
### What Happens When a User Uploads a Contract?
|
||||||
|
|
||||||
|
```
|
||||||
|
User uploads PDF → AI analyzes it → Blockchain registers the hash
|
||||||
|
```
|
||||||
|
|
||||||
|
The entire flow is automatic. The user doesn't need:
|
||||||
|
- ❌ MetaMask or any wallet
|
||||||
|
- ❌ Cryptocurrency knowledge
|
||||||
|
- ❌ To pay anything
|
||||||
|
|
||||||
|
Everything runs **server-side** with a platform wallet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Architecture Overview
|
||||||
|
|
||||||
|
### High-Level Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph "User Browser"
|
||||||
|
UI[Next.js Frontend]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Next.js Server"
|
||||||
|
SA[Server Actions<br/>blockchain.action.ts]
|
||||||
|
BS[BlockchainService<br/>blockchain.service.ts]
|
||||||
|
CA[Contract Action<br/>contract.action.ts]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Data Layer"
|
||||||
|
DB[(PostgreSQL<br/>Prisma ORM)]
|
||||||
|
UT[UploadThing<br/>File Storage]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Blockchain Network"
|
||||||
|
SC[Smart Contract<br/>DocumentRegistry.sol]
|
||||||
|
HN[Hardhat Local Node<br/>or Sepolia Testnet]
|
||||||
|
end
|
||||||
|
|
||||||
|
UI -->|"1. Upload contract"| CA
|
||||||
|
CA -->|"2. AI analyzes"| CA
|
||||||
|
CA -->|"3. Hash + register"| BS
|
||||||
|
BS -->|"4. Download PDF"| UT
|
||||||
|
BS -->|"5. SHA-256 hash"| BS
|
||||||
|
BS -->|"6. Send transaction"| SC
|
||||||
|
SC -->|"7. Store on-chain"| HN
|
||||||
|
BS -->|"8. Save proof"| DB
|
||||||
|
UI -->|"View /blockchain"| SA
|
||||||
|
SA -->|"Read transactions"| DB
|
||||||
|
SA -->|"Verify on-chain"| SC
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Modes
|
||||||
|
|
||||||
|
| Mode | When | URL | Cost |
|
||||||
|
|------|------|-----|------|
|
||||||
|
| **Hardhat** | Development | `http://127.0.0.1:8545` | Free (local) |
|
||||||
|
| **Sepolia** | Demo/Presentation | Via Alchemy/Infura RPC | Free (testnet) |
|
||||||
|
|
||||||
|
The mode is controlled by a single env variable: `BLOCKCHAIN_NETWORK`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Smart Contract: DocumentRegistry.sol
|
||||||
|
|
||||||
|
**Location**: `blockchain/contracts/DocumentRegistry.sol`
|
||||||
|
|
||||||
|
### What It Does
|
||||||
|
|
||||||
|
The smart contract is written in **Solidity** (the programming language for Ethereum). It stores document fingerprints on the blockchain.
|
||||||
|
|
||||||
|
### Data Structures
|
||||||
|
|
||||||
|
```solidity
|
||||||
|
struct DocumentRecord {
|
||||||
|
uint256 timestamp; // When the document was registered
|
||||||
|
address depositor; // Who registered it (our server wallet)
|
||||||
|
string fileName; // Original file name
|
||||||
|
bool exists; // Whether this record is valid
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Functions
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph "Write Operations (costs gas)"
|
||||||
|
R[registerDocument<br/>bytes32 hash, string fileName]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Read Operations (free)"
|
||||||
|
V[verifyDocument<br/>bytes32 hash]
|
||||||
|
T[getTimestamp<br/>bytes32 hash]
|
||||||
|
D[getDocumentsByDepositor<br/>address]
|
||||||
|
end
|
||||||
|
|
||||||
|
R --> |"Stores hash + timestamp"| BC[(Blockchain State)]
|
||||||
|
BC --> V
|
||||||
|
BC --> T
|
||||||
|
BC --> D
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `registerDocument(bytes32 _docHash)`
|
||||||
|
- **Purpose**: Store a document hash on-chain
|
||||||
|
- **Access**: Only the contract owner (our server wallet)
|
||||||
|
- **Guard**: Prevents duplicate registration (same hash can't be registered twice)
|
||||||
|
- **Event**: Emits `DocumentRegistered` for off-chain indexing
|
||||||
|
|
||||||
|
#### `verifyDocument(bytes32 _docHash)`
|
||||||
|
- **Purpose**: Check if a hash exists and get its details
|
||||||
|
- **Cost**: Free (read-only, no gas)
|
||||||
|
- **Returns**: `(exists, timestamp, depositor)`
|
||||||
|
|
||||||
|
### How the Hash Works
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
PDF[Contract PDF<br/>2.4 MB] -->|SHA-256| HASH[0x7f83b165...4e2a<br/>32 bytes]
|
||||||
|
HASH -->|Store on-chain| BC[Blockchain]
|
||||||
|
|
||||||
|
PDF2[Same PDF] -->|SHA-256| HASH2[0x7f83b165...4e2a<br/>Identical hash!]
|
||||||
|
PDF3[Modified PDF<br/>1 byte changed] -->|SHA-256| HASH3[0xa1b2c3d4...9z8y<br/>Completely different!]
|
||||||
|
```
|
||||||
|
|
||||||
|
> **SHA-256** is a one-way function. You can't reconstruct the document from the hash, but the same document always produces the same hash.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
14 tests cover all functionality:
|
||||||
|
|
||||||
|
```
|
||||||
|
✓ should set the deployer as owner
|
||||||
|
✓ should start with zero documents
|
||||||
|
✓ should register a document and emit event
|
||||||
|
✓ should store correct timestamp
|
||||||
|
✓ should increment totalDocuments counter
|
||||||
|
✓ should prevent duplicate registration
|
||||||
|
✓ should allow different users to register different documents
|
||||||
|
✓ should verify a registered document
|
||||||
|
✓ should return false for unregistered document
|
||||||
|
✓ should track all documents by a depositor
|
||||||
|
✓ should return correct document count
|
||||||
|
✓ should transfer ownership
|
||||||
|
✓ should prevent non-owner from transferring ownership
|
||||||
|
✓ should prevent transfer to zero address
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Server-Side Integration: BlockchainService
|
||||||
|
|
||||||
|
**Location**: `lib/services/blockchain.service.ts`
|
||||||
|
|
||||||
|
### Why Server-Side?
|
||||||
|
|
||||||
|
Most blockchain dApps require users to install MetaMask and sign transactions. This is bad UX for a BFSI enterprise platform because:
|
||||||
|
- Users shouldn't need crypto knowledge
|
||||||
|
- The platform manages documents, not individual users
|
||||||
|
- Server-side signing is more reliable
|
||||||
|
|
||||||
|
Instead, we use a **server wallet**: a private key stored in `.env` that the server uses to sign transactions automatically.
|
||||||
|
|
||||||
|
### How It Connects to the Blockchain
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant S as Server Action
|
||||||
|
participant BS as BlockchainService
|
||||||
|
participant P as ethers.js Provider
|
||||||
|
participant W as ethers.js Wallet
|
||||||
|
participant C as Smart Contract
|
||||||
|
|
||||||
|
S->>BS: hashAndRegister(fileUrl, fileName)
|
||||||
|
BS->>BS: Download PDF from UploadThing
|
||||||
|
BS->>BS: Compute SHA-256 hash
|
||||||
|
BS->>P: Connect to RPC (Hardhat/Sepolia)
|
||||||
|
BS->>W: Sign transaction with private key
|
||||||
|
W->>C: registerDocument(hash)
|
||||||
|
C-->>W: Transaction receipt
|
||||||
|
W-->>BS: txHash, blockNumber, blockTimestamp
|
||||||
|
BS-->>S: BlockchainProof object
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Methods
|
||||||
|
|
||||||
|
| Method | Purpose | Gas Cost |
|
||||||
|
|--------|---------|----------|
|
||||||
|
| `hashDocument(fileUrl)` | Download file + compute SHA-256 | None (off-chain) |
|
||||||
|
| `registerOnChain(hash, fileName)` | Send tx to smart contract | ~50,000 gas |
|
||||||
|
| `verifyOnChain(hash)` | Read-only check | Free |
|
||||||
|
| `hashAndRegister(fileUrl, fileName)` | Combined: hash + register | ~50,000 gas |
|
||||||
|
| `getNetworkStats()` | Get block number, total docs | Free |
|
||||||
|
| `isConfigured()` | Check if env vars are set | None |
|
||||||
|
|
||||||
|
### Graceful Degradation
|
||||||
|
|
||||||
|
If blockchain is not configured (env vars missing), the service returns `isConfigured() = false` and all blockchain features are silently disabled. The rest of the app works normally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Database Schema: Storing Proof Data
|
||||||
|
|
||||||
|
**Location**: `prisma/schema.prisma`
|
||||||
|
|
||||||
|
### Contract Model (updated fields)
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model Contract {
|
||||||
|
// ... existing fields ...
|
||||||
|
|
||||||
|
// Blockchain proof-of-deposit
|
||||||
|
documentHash String? // SHA-256 hash
|
||||||
|
txHash String? // Ethereum transaction hash
|
||||||
|
blockNumber Int? // Block number
|
||||||
|
blockTimestamp DateTime? // Block timestamp
|
||||||
|
blockchainNetwork String? // 'hardhat' | 'sepolia'
|
||||||
|
contractAddress String? // Smart contract address
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### BlockchainTransaction Model (new)
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model BlockchainTransaction {
|
||||||
|
id String @id
|
||||||
|
userId String // Who triggered the registration
|
||||||
|
contractId String // Which contract was registered
|
||||||
|
documentHash String // SHA-256 hash
|
||||||
|
txHash String @unique // Ethereum tx hash
|
||||||
|
blockNumber Int // Block where tx was mined
|
||||||
|
blockTimestamp DateTime // Proof timestamp
|
||||||
|
network String // 'hardhat' | 'sepolia'
|
||||||
|
contractAddress String // Smart contract address
|
||||||
|
status String // PENDING, CONFIRMED, FAILED
|
||||||
|
createdAt DateTime
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why Two Places?
|
||||||
|
|
||||||
|
- **Contract fields**: Quick access to proof data when displaying a single contract
|
||||||
|
- **BlockchainTransaction**: Separate table for the explorer page, supports querying all transactions for a user independently of contracts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Server Actions: The API Layer
|
||||||
|
|
||||||
|
**Location**: `features/blockchain/api/blockchain.action.ts`
|
||||||
|
|
||||||
|
### Actions Available
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph "Blockchain Server Actions"
|
||||||
|
A1[registerContractOnBlockchain<br/>contractId → proof]
|
||||||
|
A2[verifyContractOnBlockchain<br/>contractId → verification]
|
||||||
|
A3[verifyDocumentHashOnBlockchain<br/>hash → exists/timestamp]
|
||||||
|
A4[getBlockchainTransactions<br/>→ transaction list]
|
||||||
|
A5[getBlockchainStats<br/>→ network stats]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Called by"
|
||||||
|
UI1[Blockchain Explorer Page]
|
||||||
|
UI2[Contract List - Register Button]
|
||||||
|
UI3[Analysis Flow - Auto]
|
||||||
|
end
|
||||||
|
|
||||||
|
UI1 --> A3
|
||||||
|
UI1 --> A4
|
||||||
|
UI1 --> A5
|
||||||
|
UI2 --> A1
|
||||||
|
UI3 --> A1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-Registration Flow
|
||||||
|
|
||||||
|
In `features/contracts/api/contract.action.ts`, after AI analysis completes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// After AI analysis + RAG chunking...
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (BlockchainService.isConfigured()) {
|
||||||
|
const proof = await BlockchainService.hashAndRegister(
|
||||||
|
contract.fileUrl,
|
||||||
|
contract.fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save proof to Contract + BlockchainTransaction
|
||||||
|
await prisma.contract.update({...});
|
||||||
|
await prisma.blockchainTransaction.create({...});
|
||||||
|
}
|
||||||
|
} catch (blockchainError) {
|
||||||
|
// Non-blocking: blockchain failure doesn't break analysis
|
||||||
|
console.warn("Blockchain registration skipped:", blockchainError);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Critical design decision**: Blockchain registration is wrapped in a try/catch. If the Hardhat node is down or there's a network issue, the AI analysis still completes successfully. Blockchain is an enhancement, not a dependency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Frontend: Blockchain Explorer Page
|
||||||
|
|
||||||
|
**Location**: `app/(dashboard)/blockchain/page.tsx`
|
||||||
|
|
||||||
|
### Page Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ 🔗 Blockchain Explorer [Refresh] │
|
||||||
|
├────────────┬────────────┬──────────┬────────────┤
|
||||||
|
│ Verified │ Latest │ Network │ Wallet │
|
||||||
|
│ Documents │ Block │ Status │ Address │
|
||||||
|
│ 12 │ #847 │ ● Live │ 0xf39F... │
|
||||||
|
├────────────┴────────────┴──────────┴────────────┤
|
||||||
|
│ ⚠️ 3 contracts not yet on blockchain │
|
||||||
|
│ ┌─ Insurance Auto [Register] ─┐ │
|
||||||
|
│ └─ Home Loan Policy [Register] ─┘ │
|
||||||
|
├──────────────────────┬──────────────────────────┤
|
||||||
|
│ Transaction History │ Verify Document │
|
||||||
|
│ ────────────────── │ ────────────────── │
|
||||||
|
│ ✓ Insurance Auto │ [Hash input field] │
|
||||||
|
│ Tx: 0x7f83... │ [Verify On-Chain] │
|
||||||
|
│ Block: #845 │ │
|
||||||
|
│ Time: Apr 19 │ ✓ Document Verified │
|
||||||
|
│ ────────────────── │ Timestamp: ... │
|
||||||
|
│ ✓ Home Loan │ Depositor: 0xf39F.. │
|
||||||
|
│ Tx: 0xa1b2... │ │
|
||||||
|
└──────────────────────┴──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
The Blockchain page is accessible from the sidebar navigation at `/blockchain`, alongside Analytics and Contracts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Complete Data Flow
|
||||||
|
|
||||||
|
### End-to-End: Upload → Proof
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor U as User
|
||||||
|
participant UI as Browser
|
||||||
|
participant SA as Server Action
|
||||||
|
participant AI as AI Service
|
||||||
|
participant BS as BlockchainService
|
||||||
|
participant SC as Smart Contract
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
U->>UI: Upload contract PDF
|
||||||
|
UI->>SA: saveContract(fileData)
|
||||||
|
SA->>DB: Create contract (UPLOADED)
|
||||||
|
SA->>SA: analyzeContractAction(id)
|
||||||
|
SA->>AI: Analyze document (Gemini)
|
||||||
|
AI-->>SA: AI results (title, dates, etc.)
|
||||||
|
SA->>DB: Update contract (COMPLETED)
|
||||||
|
|
||||||
|
Note over SA,SC: Blockchain Registration (automatic)
|
||||||
|
SA->>BS: hashAndRegister(fileUrl, fileName)
|
||||||
|
BS->>BS: Download PDF
|
||||||
|
BS->>BS: SHA-256 hash → 0x7f83b1...
|
||||||
|
BS->>SC: registerDocument(hash)
|
||||||
|
SC->>SC: Store hash + timestamp
|
||||||
|
SC-->>BS: Transaction receipt
|
||||||
|
BS-->>SA: BlockchainProof
|
||||||
|
|
||||||
|
SA->>DB: Save txHash, blockNumber, etc.
|
||||||
|
SA->>DB: Create BlockchainTransaction
|
||||||
|
SA-->>UI: Success!
|
||||||
|
|
||||||
|
Note over U,UI: User visits /blockchain
|
||||||
|
U->>UI: Click "Blockchain" in sidebar
|
||||||
|
UI->>SA: getBlockchainTransactions()
|
||||||
|
SA->>DB: Fetch all transactions
|
||||||
|
DB-->>SA: Transaction list
|
||||||
|
SA-->>UI: Display in explorer
|
||||||
|
|
||||||
|
Note over U,UI: User verifies a document
|
||||||
|
U->>UI: Paste document hash
|
||||||
|
UI->>SA: verifyDocumentHashOnBlockchain(hash)
|
||||||
|
SA->>BS: verifyOnChain(hash)
|
||||||
|
BS->>SC: verifyDocument(hash)
|
||||||
|
SC-->>BS: (exists, timestamp, depositor)
|
||||||
|
BS-->>SA: Verification result
|
||||||
|
SA-->>UI: "✓ Document Verified"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. How to Run Locally
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js installed
|
||||||
|
- The Next.js app running (`npm run dev`)
|
||||||
|
|
||||||
|
### Step 1: Start the Hardhat Node
|
||||||
|
|
||||||
|
Open a **new terminal** and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd blockchain
|
||||||
|
npx hardhat node
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts a local Ethereum blockchain at `http://127.0.0.1:8545` with 20 pre-funded accounts (10,000 ETH each).
|
||||||
|
|
||||||
|
> ⚠️ **Keep this terminal open!** The node must be running for blockchain features to work.
|
||||||
|
|
||||||
|
### Step 2: Deploy the Smart Contract
|
||||||
|
|
||||||
|
In another terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd blockchain
|
||||||
|
npx hardhat run scripts/deploy.ts --network localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the contract address and put it in your `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
BLOCKCHAIN_CONTRACT_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Start the Next.js App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Test the Flow
|
||||||
|
|
||||||
|
1. Upload a contract PDF
|
||||||
|
2. Wait for AI analysis to complete
|
||||||
|
3. Check the blockchain icon/badge on the contract
|
||||||
|
4. Visit `/blockchain` to see the transaction in the explorer
|
||||||
|
5. Copy a document hash and paste it in the verification panel
|
||||||
|
|
||||||
|
### Important Notes
|
||||||
|
|
||||||
|
- If you restart the Hardhat node, you need to **redeploy** the contract (step 2) because the local blockchain state is reset
|
||||||
|
- The Hardhat node logs every transaction in real-time — you can watch the blockchain activity live
|
||||||
|
- All blockchain features gracefully degrade: if the node is offline, the app still works normally without blockchain
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Deploying to Sepolia Testnet
|
||||||
|
|
||||||
|
For your PFE presentation, you can deploy to the real Ethereum Sepolia testnet to get actual Etherscan links.
|
||||||
|
|
||||||
|
### Step 1: Get a Free RPC URL
|
||||||
|
|
||||||
|
1. Go to [alchemy.com](https://alchemy.com) (free account)
|
||||||
|
2. Create a new app → select "Ethereum Sepolia"
|
||||||
|
3. Copy the HTTPS URL
|
||||||
|
|
||||||
|
### Step 2: Get Free Sepolia ETH
|
||||||
|
|
||||||
|
1. Go to [sepoliafaucet.com](https://sepoliafaucet.com) or [faucets.chain.link](https://faucets.chain.link)
|
||||||
|
2. Paste your wallet address
|
||||||
|
3. Receive 0.5 Sepolia ETH (enough for hundreds of transactions)
|
||||||
|
|
||||||
|
### Step 3: Update .env
|
||||||
|
|
||||||
|
```env
|
||||||
|
BLOCKCHAIN_NETWORK=sepolia
|
||||||
|
BLOCKCHAIN_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
|
||||||
|
BLOCKCHAIN_PRIVATE_KEY=your_sepolia_wallet_private_key
|
||||||
|
BLOCKCHAIN_CONTRACT_ADDRESS= # Will be filled after deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Deploy to Sepolia
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set env vars for hardhat
|
||||||
|
set SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
|
||||||
|
set DEPLOYER_PRIVATE_KEY=your_private_key
|
||||||
|
|
||||||
|
cd blockchain
|
||||||
|
npx hardhat run scripts/deploy.ts --network sepolia
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Verify on Etherscan
|
||||||
|
|
||||||
|
After deploying, transactions will have real Etherscan links:
|
||||||
|
```
|
||||||
|
https://sepolia.etherscan.io/tx/0x...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Technology Choices & Rationale
|
||||||
|
|
||||||
|
| Technology | Why We Chose It |
|
||||||
|
|-----------|----------------|
|
||||||
|
| **Solidity 0.8.24** | Latest stable version with built-in overflow protection |
|
||||||
|
| **Hardhat** | Industry standard for Solidity development, free local blockchain |
|
||||||
|
| **ethers.js v6** | Modern, lightweight, TypeScript-native Ethereum library |
|
||||||
|
| **SHA-256** | Standard cryptographic hash, deterministic, collision-resistant |
|
||||||
|
| **Server-side wallet** | Users don't need MetaMask; enterprise-grade UX |
|
||||||
|
| **Sepolia testnet** | Official Ethereum testnet, free, has Etherscan explorer |
|
||||||
|
| **Graceful degradation** | Blockchain is optional; app works perfectly without it |
|
||||||
|
|
||||||
|
### Why NOT Web3j / Java?
|
||||||
|
|
||||||
|
The original project spec suggested Web3j (Java library). We chose ethers.js instead because:
|
||||||
|
1. Our backend is **Next.js/TypeScript**, not Spring Boot
|
||||||
|
2. ethers.js has **better TypeScript support** and is more actively maintained
|
||||||
|
3. Both libraries do the same job — interact with Ethereum — but ethers.js is native to our stack
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. File Reference
|
||||||
|
|
||||||
|
### Smart Contract Layer
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `blockchain/contracts/DocumentRegistry.sol` | Solidity smart contract |
|
||||||
|
| `blockchain/test/DocumentRegistry.test.ts` | 14 comprehensive tests |
|
||||||
|
| `blockchain/scripts/deploy.ts` | Deployment script |
|
||||||
|
| `blockchain/hardhat.config.ts` | Hardhat configuration |
|
||||||
|
| `blockchain/package.json` | Hardhat dependencies |
|
||||||
|
|
||||||
|
### Service Layer
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `lib/services/blockchain.service.ts` | Core blockchain interactions |
|
||||||
|
| `lib/services/blockchain.types.ts` | TypeScript type definitions |
|
||||||
|
|
||||||
|
### Server Actions
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `features/blockchain/api/blockchain.action.ts` | Blockchain server actions |
|
||||||
|
| `features/contracts/api/contract.action.ts` | Updated with auto-registration |
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `app/(dashboard)/blockchain/page.tsx` | Blockchain Explorer page |
|
||||||
|
| `app/(dashboard)/blockchain/layout.tsx` | Page metadata |
|
||||||
|
| `components/layout/navigation.tsx` | Updated with blockchain link |
|
||||||
|
|
||||||
|
### Database
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `prisma/schema.prisma` | Updated with blockchain fields |
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `.env` | Blockchain env vars |
|
||||||
|
| `.env.example` | Template for new developers |
|
||||||
|
| `.gitignore` | Blockchain artifacts excluded |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
|------|-----------|
|
||||||
|
| **Hash** | A fixed-size fingerprint of data. Same input → same output. |
|
||||||
|
| **SHA-256** | A specific hash algorithm producing 256-bit (32-byte) outputs |
|
||||||
|
| **Smart Contract** | A program stored on the blockchain that executes automatically |
|
||||||
|
| **Gas** | The fee for executing operations on Ethereum (free on testnet) |
|
||||||
|
| **Block** | A batch of transactions grouped together on the blockchain |
|
||||||
|
| **Transaction (Tx)** | A single operation on the blockchain (e.g., registering a hash) |
|
||||||
|
| **Tx Hash** | A unique identifier for a transaction (like a receipt number) |
|
||||||
|
| **Block Number** | The sequential number of the block containing a transaction |
|
||||||
|
| **Block Timestamp** | The time the block was created (proof of when the tx happened) |
|
||||||
|
| **Private Key** | Secret key used to sign transactions (like a password) |
|
||||||
|
| **Address** | Public identifier derived from the private key (like a username) |
|
||||||
|
| **ABI** | Application Binary Interface — the "API spec" of a smart contract |
|
||||||
|
| **Hardhat** | Development tool for writing, testing, and deploying smart contracts |
|
||||||
|
| **Sepolia** | Ethereum test network for free experimentation |
|
||||||
|
| **ethers.js** | JavaScript library for interacting with the Ethereum blockchain |
|
||||||
|
| **Faucet** | A service that gives free test ETH for development |
|
||||||
34
docs/blockchain-overview.md
Normal file
34
docs/blockchain-overview.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# LexiChain — Blockchain Module Overview
|
||||||
|
|
||||||
|
## 🔗 What does the Blockchain do here?
|
||||||
|
|
||||||
|
In the LexiChain BFSI platform, the blockchain acts as a **Digital Notary**. Its primary purpose is to provide **Proof of Existence** and **Proof of Integrity** for sensitive financial and insurance contracts.
|
||||||
|
|
||||||
|
### 1. Proof of Existence
|
||||||
|
When a contract is uploaded, its unique digital fingerprint (SHA-256 hash) is recorded on the blockchain. Because the blockchain is immutable, it provides indisputable proof that the document existed at a specific point in time.
|
||||||
|
|
||||||
|
### 2. Tamper Evidence
|
||||||
|
We don't store the actual PDF on the blockchain (for privacy and cost). We store its **Hash**. If even a single character in the PDF is changed, the hash will change, and the platform will flag the document as invalid or modified.
|
||||||
|
|
||||||
|
### 3. Zero-Knowledge Proof
|
||||||
|
Users can prove they have the original document to any third party (regulators, lawyers) by comparing the file's hash against the public blockchain record, without ever revealing the private contents of the document itself.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ The Blockchain Stack
|
||||||
|
|
||||||
|
| Technology | Role | Why we used it? |
|
||||||
|
|:---|:---|:---|
|
||||||
|
| **Solidity (0.8.24)** | **Smart Contract** | The industry-standard language for Ethereum. It allows us to write the "logic" of the registry (registration, verification, ownership) directly into the blockchain. |
|
||||||
|
| **Hardhat** | **Development Framework** | Provides a local Ethereum environment. It allows us to test transactions instantly for free and simulate real-world blockchain behavior on a developer machine. |
|
||||||
|
| **Ethers.js (v6)** | **Integration Library** | A powerful and lightweight library that bridges our Next.js backend with the Ethereum network. It handles the complex math of signing transactions and talking to the smart contract. |
|
||||||
|
| **Next.js Server Actions** | **Backend Bridge** | We use server-side logic to handle blockchain interactions. This means **users don't need MetaMask** or crypto-wallets; the platform handles the "gas fees" and signing automatically. |
|
||||||
|
| **Sepolia Testnet** | **Deployment Target** | A public Ethereum test network. It allows us to show the jury a "real" deployment on a global network with real block explorers (Etherscan) without using real money. |
|
||||||
|
| **SHA-256 Hashing** | **Security Standard** | A cryptographic algorithm that turns any file into a unique 64-character string. It is the "gold standard" for ensuring document integrity. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏆 Key Benefits
|
||||||
|
* **Trustless**: You don't have to trust the database administrator; you trust the math.
|
||||||
|
* **Transparent**: Transactions are visible to auditors via the Blockchain Explorer.
|
||||||
|
* **Privacy-First**: No personal data or document text ever touches the public blockchain.
|
||||||
315
features/blockchain/api/blockchain.action.ts
Normal file
315
features/blockchain/api/blockchain.action.ts
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
/**
|
||||||
|
* Blockchain Server Actions
|
||||||
|
*
|
||||||
|
* Server-side functions for blockchain operations:
|
||||||
|
* - Register a contract document on the blockchain
|
||||||
|
* - Verify a contract's on-chain proof
|
||||||
|
* - Get all blockchain transactions for the user
|
||||||
|
* - Get blockchain network stats
|
||||||
|
*
|
||||||
|
* These actions are called from the frontend via React Server Actions.
|
||||||
|
* All blockchain logic runs server-side (no MetaMask needed).
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { auth } from "@clerk/nextjs/server";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { prisma } from "@/lib/db/prisma";
|
||||||
|
import { BlockchainService } from "@/lib/services/blockchain.service";
|
||||||
|
import { NotificationService } from "@/lib/services/notification.service";
|
||||||
|
import { ContractService } from "@/lib/services/contract.service";
|
||||||
|
import type { BlockchainTransactionView, BlockchainStats } from "@/lib/services/blockchain.types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a contract's document on the blockchain.
|
||||||
|
*
|
||||||
|
* FLOW:
|
||||||
|
* 1. Authenticate user
|
||||||
|
* 2. Fetch contract from DB
|
||||||
|
* 3. Download PDF and compute SHA-256 hash
|
||||||
|
* 4. Send hash to the smart contract on-chain
|
||||||
|
* 5. Store proof data (txHash, blockNumber, etc.) in PostgreSQL
|
||||||
|
* 6. Create a BlockchainTransaction record for the explorer
|
||||||
|
* 7. Create a notification for the user
|
||||||
|
*
|
||||||
|
* @param contractId - The contract ID to register
|
||||||
|
*/
|
||||||
|
export async function registerContractOnBlockchain(contractId: string) {
|
||||||
|
try {
|
||||||
|
const { userId: clerkId } = await auth();
|
||||||
|
if (!clerkId) return { success: false, error: "Unauthorized" };
|
||||||
|
|
||||||
|
// Check if blockchain is configured
|
||||||
|
if (!BlockchainService.isConfigured()) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Blockchain not configured. Start a Hardhat node and check your .env.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get internal user
|
||||||
|
const user = await ContractService.getUserByClerkId(clerkId);
|
||||||
|
if (!user) return { success: false, error: "User not found" };
|
||||||
|
|
||||||
|
// Get the contract
|
||||||
|
const contract = await ContractService.getById(contractId);
|
||||||
|
if (contract.userId !== user.id) {
|
||||||
|
return { success: false, error: "Unauthorized" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already registered
|
||||||
|
if (contract.txHash && contract.txHash !== "already-registered") {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Contract already registered on blockchain",
|
||||||
|
proof: {
|
||||||
|
documentHash: contract.documentHash,
|
||||||
|
txHash: contract.txHash,
|
||||||
|
blockNumber: contract.blockNumber,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the document and register on-chain
|
||||||
|
const proof = await BlockchainService.hashAndRegister(
|
||||||
|
contract.fileUrl,
|
||||||
|
contract.fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save proof data to the Contract record
|
||||||
|
await prisma.contract.update({
|
||||||
|
where: { id: contractId },
|
||||||
|
data: {
|
||||||
|
documentHash: proof.documentHash,
|
||||||
|
txHash: proof.txHash,
|
||||||
|
blockNumber: proof.blockNumber,
|
||||||
|
blockTimestamp: proof.blockTimestamp,
|
||||||
|
blockchainNetwork: proof.network,
|
||||||
|
contractAddress: proof.contractAddress,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a BlockchainTransaction record for the explorer
|
||||||
|
await prisma.blockchainTransaction.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
contractId,
|
||||||
|
documentHash: proof.documentHash,
|
||||||
|
txHash: proof.txHash,
|
||||||
|
blockNumber: proof.blockNumber,
|
||||||
|
blockTimestamp: proof.blockTimestamp,
|
||||||
|
network: proof.network,
|
||||||
|
contractAddress: proof.contractAddress,
|
||||||
|
status: "CONFIRMED",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create success notification
|
||||||
|
await NotificationService.create({
|
||||||
|
userId: user.id,
|
||||||
|
type: "SUCCESS",
|
||||||
|
title: "🔗 Blockchain Verified",
|
||||||
|
message: `"${contract.title || contract.fileName}" has been registered on-chain. Tx: ${proof.txHash.slice(0, 16)}...`,
|
||||||
|
contractId,
|
||||||
|
actionType: "BLOCKCHAIN_REGISTERED",
|
||||||
|
icon: "Link2",
|
||||||
|
expiresIn: 14 * 24 * 60 * 60 * 1000, // 14 days
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidatePath("/contacts");
|
||||||
|
revalidatePath("/dashboard");
|
||||||
|
revalidatePath("/blockchain");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Document registered on blockchain!",
|
||||||
|
proof: {
|
||||||
|
documentHash: proof.documentHash,
|
||||||
|
txHash: proof.txHash,
|
||||||
|
blockNumber: proof.blockNumber,
|
||||||
|
blockTimestamp: proof.blockTimestamp.toISOString(),
|
||||||
|
network: proof.network,
|
||||||
|
explorerUrl: proof.explorerUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("❌ Blockchain registration error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Unknown blockchain error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a contract's on-chain proof by checking the blockchain directly.
|
||||||
|
*/
|
||||||
|
export async function verifyContractOnBlockchain(contractId: string) {
|
||||||
|
try {
|
||||||
|
const { userId: clerkId } = await auth();
|
||||||
|
if (!clerkId) return { success: false, error: "Unauthorized" };
|
||||||
|
|
||||||
|
if (!BlockchainService.isReadConfigured()) {
|
||||||
|
return { success: false, error: "Blockchain not configured for verification" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await ContractService.getUserByClerkId(clerkId);
|
||||||
|
if (!user) return { success: false, error: "User not found" };
|
||||||
|
|
||||||
|
const contract = await ContractService.getById(contractId);
|
||||||
|
if (contract.userId !== user.id) return { success: false, error: "Unauthorized" };
|
||||||
|
|
||||||
|
if (!contract.documentHash) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
verification: { exists: false, timestamp: 0, depositor: "" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const verification = await BlockchainService.verifyOnChain(contract.documentHash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
verification: {
|
||||||
|
exists: verification.exists,
|
||||||
|
timestamp: verification.timestamp,
|
||||||
|
depositor: verification.depositor,
|
||||||
|
documentHash: contract.documentHash,
|
||||||
|
txHash: contract.txHash,
|
||||||
|
blockNumber: contract.blockNumber,
|
||||||
|
network: contract.blockchainNetwork,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("❌ Blockchain verification error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a raw document hash on-chain (for the verification panel).
|
||||||
|
*/
|
||||||
|
export async function verifyDocumentHashOnBlockchain(documentHash: string) {
|
||||||
|
try {
|
||||||
|
const { userId: clerkId } = await auth();
|
||||||
|
if (!clerkId) return { success: false, error: "Unauthorized" };
|
||||||
|
|
||||||
|
if (!BlockchainService.isReadConfigured()) {
|
||||||
|
return { success: false, error: "Blockchain not configured for verification" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure proper format
|
||||||
|
const rawHash = documentHash.trim();
|
||||||
|
const hash = rawHash.startsWith("0x") ? rawHash : `0x${rawHash}`;
|
||||||
|
|
||||||
|
if (!/^0x[a-fA-F0-9]{64}$/.test(hash)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Invalid hash format. Expected a 32-byte SHA-256 hex string.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const verification = await BlockchainService.verifyOnChain(hash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
verification: {
|
||||||
|
exists: verification.exists,
|
||||||
|
timestamp: verification.timestamp,
|
||||||
|
depositor: verification.depositor,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("❌ Hash verification error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all blockchain transactions for the authenticated user.
|
||||||
|
* Used by the blockchain explorer page.
|
||||||
|
*/
|
||||||
|
export async function getBlockchainTransactions(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
transactions?: BlockchainTransactionView[];
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const { userId: clerkId } = await auth();
|
||||||
|
if (!clerkId) return { success: false, error: "Unauthorized" };
|
||||||
|
|
||||||
|
const user = await ContractService.getUserByClerkId(clerkId);
|
||||||
|
if (!user) return { success: false, error: "User not found" };
|
||||||
|
|
||||||
|
const txs = await prisma.blockchainTransaction.findMany({
|
||||||
|
where: { userId: user.id },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
include: {
|
||||||
|
contract: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
fileName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const transactions: BlockchainTransactionView[] = txs.map((tx) => ({
|
||||||
|
id: tx.id,
|
||||||
|
contractId: tx.contractId,
|
||||||
|
contractTitle: tx.contract.title,
|
||||||
|
contractFileName: tx.contract.fileName,
|
||||||
|
documentHash: tx.documentHash,
|
||||||
|
txHash: tx.txHash,
|
||||||
|
blockNumber: tx.blockNumber,
|
||||||
|
blockTimestamp: tx.blockTimestamp.toISOString(),
|
||||||
|
network: tx.network,
|
||||||
|
contractAddress: tx.contractAddress,
|
||||||
|
status: tx.status,
|
||||||
|
createdAt: tx.createdAt.toISOString(),
|
||||||
|
explorerUrl:
|
||||||
|
tx.network === "sepolia"
|
||||||
|
? `https://sepolia.etherscan.io/tx/${tx.txHash}`
|
||||||
|
: null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { success: true, transactions };
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("❌ Get transactions error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get blockchain network stats for the explorer page header.
|
||||||
|
*/
|
||||||
|
export async function getBlockchainStats(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
stats?: BlockchainStats;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const { userId: clerkId } = await auth();
|
||||||
|
if (!clerkId) return { success: false, error: "Unauthorized" };
|
||||||
|
|
||||||
|
const stats = await BlockchainService.getNetworkStats();
|
||||||
|
return { success: true, stats };
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("❌ Blockchain stats error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,12 +28,21 @@ import {
|
|||||||
import { AIService } from "@/lib/services/ai.service";
|
import { AIService } from "@/lib/services/ai.service";
|
||||||
import { RAGService } from "@/lib/services/rag.service";
|
import { RAGService } from "@/lib/services/rag.service";
|
||||||
import { NotificationService } from "@/lib/services/notification.service";
|
import { NotificationService } from "@/lib/services/notification.service";
|
||||||
|
import { BlockchainService } from "@/lib/services/blockchain.service";
|
||||||
|
import { prisma } from "@/lib/db/prisma";
|
||||||
import type { NormalizedAnalysis } from "@/lib/services/ai/analysis.types";
|
import type { NormalizedAnalysis } from "@/lib/services/ai/analysis.types";
|
||||||
|
|
||||||
type ContractListItem = Awaited<
|
type ContractListItem = Awaited<
|
||||||
ReturnType<typeof ContractService.getAll>
|
ReturnType<typeof ContractService.getAll>
|
||||||
>[number] & {
|
>[number] & {
|
||||||
_count?: { ragChunks?: number | null };
|
_count?: { ragChunks?: number | null };
|
||||||
|
// Blockchain proof fields (added to schema, Prisma returns them)
|
||||||
|
documentHash?: string | null;
|
||||||
|
txHash?: string | null;
|
||||||
|
blockNumber?: number | null;
|
||||||
|
blockTimestamp?: Date | null;
|
||||||
|
blockchainNetwork?: string | null;
|
||||||
|
contractAddress?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AnalysisWithMeta = NormalizedAnalysis & {
|
type AnalysisWithMeta = NormalizedAnalysis & {
|
||||||
@@ -196,6 +205,13 @@ export async function getContracts(filters?: Record<string, unknown>) {
|
|||||||
extractedText: contract.extractedText || null,
|
extractedText: contract.extractedText || null,
|
||||||
ragChunkCount: Number(contract?._count?.ragChunks ?? 0),
|
ragChunkCount: Number(contract?._count?.ragChunks ?? 0),
|
||||||
isRagged: Number(contract?._count?.ragChunks ?? 0) > 0,
|
isRagged: Number(contract?._count?.ragChunks ?? 0) > 0,
|
||||||
|
// Blockchain proof fields
|
||||||
|
documentHash: contract.documentHash || null,
|
||||||
|
txHash: contract.txHash || null,
|
||||||
|
blockNumber: contract.blockNumber || null,
|
||||||
|
blockTimestamp: contract.blockTimestamp ? contract.blockTimestamp.toISOString() : null,
|
||||||
|
blockchainNetwork: contract.blockchainNetwork || null,
|
||||||
|
contractAddress: contract.contractAddress || null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { success: true, contracts: serializedContracts };
|
return { success: true, contracts: serializedContracts };
|
||||||
@@ -501,6 +517,52 @@ export async function analyzeContractAction(id: string) {
|
|||||||
keyPoints: keyPointsWithLearning,
|
keyPoints: keyPointsWithLearning,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
// BLOCKCHAIN: Auto-register document on-chain
|
||||||
|
// This is non-blocking — if blockchain fails, analysis still succeeds
|
||||||
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
try {
|
||||||
|
if (BlockchainService.isConfigured()) {
|
||||||
|
const proof = await BlockchainService.hashAndRegister(
|
||||||
|
contract.fileUrl,
|
||||||
|
contract.fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save blockchain proof to the contract record
|
||||||
|
await prisma.contract.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
documentHash: proof.documentHash,
|
||||||
|
txHash: proof.txHash,
|
||||||
|
blockNumber: proof.blockNumber,
|
||||||
|
blockTimestamp: proof.blockTimestamp,
|
||||||
|
blockchainNetwork: proof.network,
|
||||||
|
contractAddress: proof.contractAddress,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create BlockchainTransaction for explorer
|
||||||
|
await prisma.blockchainTransaction.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
contractId: id,
|
||||||
|
documentHash: proof.documentHash,
|
||||||
|
txHash: proof.txHash,
|
||||||
|
blockNumber: proof.blockNumber,
|
||||||
|
blockTimestamp: proof.blockTimestamp,
|
||||||
|
network: proof.network,
|
||||||
|
contractAddress: proof.contractAddress,
|
||||||
|
status: "CONFIRMED",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🔗 Blockchain proof stored: ${proof.txHash.slice(0, 16)}...`);
|
||||||
|
}
|
||||||
|
} catch (blockchainError) {
|
||||||
|
// Blockchain failure should NOT fail the analysis
|
||||||
|
console.warn("⚠️ Blockchain registration skipped:", blockchainError);
|
||||||
|
}
|
||||||
|
|
||||||
// Create success notification with extracted info
|
// Create success notification with extracted info
|
||||||
const contractTitle = aiResults.title || "Contract";
|
const contractTitle = aiResults.title || "Contract";
|
||||||
const contractProvider = aiResults.provider || "Unknown Provider";
|
const contractProvider = aiResults.provider || "Unknown Provider";
|
||||||
|
|||||||
@@ -171,50 +171,71 @@ export function ContractUploadForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAutoAnalyzing && (
|
{isAutoAnalyzing && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 backdrop-blur-sm animate-in fade-in duration-300">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/40 backdrop-blur-md animate-in fade-in duration-500">
|
||||||
<div className="mx-4 max-w-md rounded-3xl border border-border/60 bg-[radial-gradient(circle_at_top_right,hsl(var(--primary)/0.22),transparent_45%),radial-gradient(circle_at_bottom_left,hsl(var(--secondary)/0.16),transparent_45%),hsl(var(--background))] p-8 shadow-2xl md:p-10 zoom-in-95 animate-in duration-300">
|
<div className="mx-4 max-w-md w-full rounded-[2.5rem] border border-white/20 bg-background/80 p-8 shadow-[0_32px_64px_-12px_rgba(0,0,0,0.3)] backdrop-blur-2xl md:p-10 zoom-in-95 animate-in duration-300 relative overflow-hidden group">
|
||||||
<div className="flex flex-col items-center text-center space-y-6">
|
{/* Premium Background Accents */}
|
||||||
|
<div className="absolute -right-24 -top-24 h-64 w-64 rounded-full bg-primary/20 blur-[80px] animate-pulse"></div>
|
||||||
|
<div className="absolute -left-24 -bottom-24 h-64 w-64 rounded-full bg-secondary/15 blur-[80px] animate-pulse"></div>
|
||||||
|
|
||||||
|
<div className="relative flex flex-col items-center text-center space-y-8">
|
||||||
|
{/* Icon Section */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 rounded-full bg-primary/30 blur-xl animate-pulse"></div>
|
<div className="absolute inset-0 rounded-full bg-primary/40 blur-2xl animate-pulse"></div>
|
||||||
<div className="relative rounded-full bg-gradient-to-br from-primary to-accent p-4">
|
<div className="relative h-20 w-20 rounded-3xl bg-gradient-to-br from-primary via-primary to-accent p-0.5 shadow-xl rotate-3 transition-transform group-hover:rotate-6">
|
||||||
<Sparkles className="h-8 w-8 animate-pulse text-white" />
|
<div className="flex h-full w-full items-center justify-center rounded-[calc(1.5rem-2px)] bg-slate-950/10 backdrop-blur-sm">
|
||||||
|
<Sparkles className="h-10 w-10 text-white animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute -bottom-2 -right-2 rounded-full bg-background border border-border/50 p-2 shadow-lg">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
{/* Text Section */}
|
||||||
<Loader2 className="h-11 w-11 animate-spin text-primary" />
|
<div className="space-y-3">
|
||||||
</div>
|
<h3 className="text-2xl font-bold tracking-tight text-foreground bg-clip-text text-transparent bg-gradient-to-b from-foreground to-foreground/70">
|
||||||
|
AI Extraction In Progress
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="text-xl font-semibold text-foreground">
|
|
||||||
Analyzing And Building RAG
|
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-base text-muted-foreground/90 font-medium">
|
||||||
Your contract is being analyzed and indexed for chat...
|
We're parsing your document and building a semantic index...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full space-y-2">
|
{/* Progress Section */}
|
||||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
<div className="w-full space-y-4 px-2">
|
||||||
<span>Processing</span>
|
<div className="flex items-center justify-between text-[13px] font-semibold">
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="text-primary flex items-center gap-2">
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-primary animate-bounce [animation-delay:-0.3s]"></span>
|
<Wand2 className="h-3.5 w-3.5" />
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-primary animate-bounce [animation-delay:-0.15s]"></span>
|
Processing RAG
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-primary animate-bounce"></span>
|
</span>
|
||||||
|
<span className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-primary/60 animate-bounce [animation-delay:-0.3s]"></span>
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-primary/60 animate-bounce [animation-delay:-0.15s]"></span>
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-primary/60 animate-bounce"></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
<div className="relative h-3 w-full overflow-hidden rounded-full bg-muted/40 border border-border/20">
|
||||||
<div className="h-full w-full rounded-full bg-gradient-to-r from-primary to-accent animate-progress-loading origin-left"></div>
|
<div className="absolute inset-0 bg-gradient-to-r from-primary via-accent to-secondary animate-progress-loading origin-left"></div>
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(90deg,transparent_0%,rgba(255,255,255,0.3)_50%,transparent_100%)] bg-[length:40px_100%] animate-shimmer"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center text-[11px] text-muted-foreground/70 font-medium uppercase tracking-wider">
|
||||||
|
<span>OCR Analysis</span>
|
||||||
|
<span>Vector Indexing</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground italic">
|
{/* Footer info */}
|
||||||
This may take up to 10 seconds
|
<div className="pt-4 border-t border-border/40 w-full">
|
||||||
|
<p className="text-xs text-muted-foreground italic flex items-center justify-center gap-1.5">
|
||||||
|
<AlertCircle className="h-3 w-3" />
|
||||||
|
Average processing time: 8-10 seconds
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
401
lib/services/blockchain.service.ts
Normal file
401
lib/services/blockchain.service.ts
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Blockchain Service — Server-Side Smart Contract Integration
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
//
|
||||||
|
// This service handles ALL interactions with the Ethereum blockchain
|
||||||
|
// from the Next.js server (server actions / route handlers).
|
||||||
|
//
|
||||||
|
// KEY DESIGN DECISIONS:
|
||||||
|
// ─────────────────────
|
||||||
|
// 1. SERVER-SIDE ONLY: Uses ethers.js JsonRpcProvider + Wallet
|
||||||
|
// (NOT BrowserProvider / MetaMask). Users don't need a wallet.
|
||||||
|
//
|
||||||
|
// 2. DUAL-NETWORK: Automatically connects to Hardhat (local dev)
|
||||||
|
// or Sepolia (demo/production) based on env vars.
|
||||||
|
//
|
||||||
|
// 3. DOCUMENT HASHING: Computes SHA-256 of the contract PDF file
|
||||||
|
// content, then converts to bytes32 for Solidity compatibility.
|
||||||
|
//
|
||||||
|
// FLOW:
|
||||||
|
// Upload PDF → Download → SHA-256 Hash → Send to Smart Contract
|
||||||
|
// → Store txHash + blockNumber in PostgreSQL
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
import { ethers } from "ethers";
|
||||||
|
import { createHash } from "crypto";
|
||||||
|
import type { BlockchainProof, BlockchainVerification, BlockchainStats } from "./blockchain.types";
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
// Smart Contract ABI (Application Binary Interface)
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
// This is the "API definition" of our Solidity contract.
|
||||||
|
// It tells ethers.js what functions exist and their signatures.
|
||||||
|
// Generated by `npx hardhat compile` from DocumentRegistry.sol
|
||||||
|
const DOCUMENT_REGISTRY_ABI = [
|
||||||
|
"function registerDocument(bytes32 _docHash) external",
|
||||||
|
"function verifyDocument(bytes32 _docHash) external view returns (bool exists, uint256 timestamp, address depositor)",
|
||||||
|
"function getTimestamp(bytes32 _docHash) external view returns (uint256)",
|
||||||
|
"function getDocumentsByDepositor(address _depositor) external view returns (bytes32[] memory)",
|
||||||
|
"function getDocumentCount(address _depositor) external view returns (uint256)",
|
||||||
|
"function totalDocuments() external view returns (uint256)",
|
||||||
|
"function owner() external view returns (address)",
|
||||||
|
"event DocumentRegistered(bytes32 indexed docHash, uint256 timestamp, address indexed depositor)",
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
// CONFIGURATION
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function getConfig() {
|
||||||
|
const network = process.env.BLOCKCHAIN_NETWORK || "hardhat";
|
||||||
|
const rpcUrl = process.env.BLOCKCHAIN_RPC_URL || "http://127.0.0.1:8545";
|
||||||
|
const contractAddress = process.env.BLOCKCHAIN_CONTRACT_ADDRESS || "";
|
||||||
|
const privateKey = process.env.BLOCKCHAIN_PRIVATE_KEY || "";
|
||||||
|
|
||||||
|
return { network, rpcUrl, contractAddress, privateKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBlockchainReadConfigured(): boolean {
|
||||||
|
const config = getConfig();
|
||||||
|
return !!(config.contractAddress && config.rpcUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBlockchainWriteConfigured(): boolean {
|
||||||
|
const config = getConfig();
|
||||||
|
return !!(config.contractAddress && config.privateKey && config.rpcUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
// PROVIDER & WALLET INSTANCES (lazy singletons)
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let _provider: ethers.JsonRpcProvider | null = null;
|
||||||
|
let _wallet: ethers.Wallet | null = null;
|
||||||
|
let _readContract: ethers.Contract | null = null;
|
||||||
|
let _writeContract: ethers.Contract | null = null;
|
||||||
|
|
||||||
|
function getProvider(): ethers.JsonRpcProvider {
|
||||||
|
if (!_provider) {
|
||||||
|
const { rpcUrl } = getConfig();
|
||||||
|
_provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
}
|
||||||
|
return _provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWallet(): ethers.Wallet {
|
||||||
|
if (!_wallet) {
|
||||||
|
const { privateKey } = getConfig();
|
||||||
|
if (!privateKey) {
|
||||||
|
throw new Error("BLOCKCHAIN_PRIVATE_KEY not configured");
|
||||||
|
}
|
||||||
|
_wallet = new ethers.Wallet(privateKey, getProvider());
|
||||||
|
}
|
||||||
|
return _wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReadContract(): ethers.Contract {
|
||||||
|
if (!_readContract) {
|
||||||
|
const { contractAddress } = getConfig();
|
||||||
|
if (!contractAddress) {
|
||||||
|
throw new Error("BLOCKCHAIN_CONTRACT_ADDRESS not configured");
|
||||||
|
}
|
||||||
|
_readContract = new ethers.Contract(
|
||||||
|
contractAddress,
|
||||||
|
DOCUMENT_REGISTRY_ABI,
|
||||||
|
getProvider()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _readContract;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWriteContract(): ethers.Contract {
|
||||||
|
if (!_writeContract) {
|
||||||
|
const { contractAddress } = getConfig();
|
||||||
|
if (!contractAddress) {
|
||||||
|
throw new Error("BLOCKCHAIN_CONTRACT_ADDRESS not configured");
|
||||||
|
}
|
||||||
|
_writeContract = new ethers.Contract(
|
||||||
|
contractAddress,
|
||||||
|
DOCUMENT_REGISTRY_ABI,
|
||||||
|
getWallet()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _writeContract;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset singletons (useful when env changes at runtime)
|
||||||
|
function resetInstances() {
|
||||||
|
_provider = null;
|
||||||
|
_wallet = null;
|
||||||
|
_readContract = null;
|
||||||
|
_writeContract = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// CORE SERVICE CLASS
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
export class BlockchainService {
|
||||||
|
/**
|
||||||
|
* Check if blockchain is properly configured.
|
||||||
|
* Returns false if env vars are missing — blockchain features
|
||||||
|
* are gracefully disabled without breaking the rest of the app.
|
||||||
|
*/
|
||||||
|
static isConfigured(): boolean {
|
||||||
|
return isBlockchainWriteConfigured();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if blockchain read operations are configured.
|
||||||
|
* Read operations (stats/verify) do not require private key.
|
||||||
|
*/
|
||||||
|
static isReadConfigured(): boolean {
|
||||||
|
return isBlockchainReadConfigured();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute SHA-256 hash of a document from its URL.
|
||||||
|
*
|
||||||
|
* HOW IT WORKS:
|
||||||
|
* 1. Downloads the PDF file from UploadThing URL
|
||||||
|
* 2. Computes SHA-256 hash of the raw bytes
|
||||||
|
* 3. Prepends "0x" and pads to bytes32 for Solidity
|
||||||
|
*
|
||||||
|
* WHY SHA-256:
|
||||||
|
* - Industry standard for document fingerprinting
|
||||||
|
* - Collision resistant (practically impossible for two different
|
||||||
|
* documents to produce the same hash)
|
||||||
|
* - The hash is deterministic: same file → same hash, always
|
||||||
|
*
|
||||||
|
* @param fileUrl - The URL of the document to hash
|
||||||
|
* @returns The SHA-256 hash as a 0x-prefixed hex string (bytes32)
|
||||||
|
*/
|
||||||
|
static async hashDocument(fileUrl: string): Promise<string> {
|
||||||
|
console.log("🔐 Computing document hash...");
|
||||||
|
|
||||||
|
// Download the file
|
||||||
|
const response = await fetch(fileUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to download file: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
// Compute SHA-256 hash
|
||||||
|
const hash = createHash("sha256").update(buffer).digest("hex");
|
||||||
|
|
||||||
|
// Convert to bytes32 format (0x-prefixed, 64 hex characters)
|
||||||
|
const bytes32Hash = "0x" + hash;
|
||||||
|
|
||||||
|
console.log(`✅ Document hash: ${bytes32Hash.slice(0, 18)}...`);
|
||||||
|
return bytes32Hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a document hash on the blockchain.
|
||||||
|
*
|
||||||
|
* HOW IT WORKS:
|
||||||
|
* 1. Creates a transaction calling registerDocument() on the smart contract
|
||||||
|
* 2. The server wallet signs the transaction with its private key
|
||||||
|
* 3. The transaction is broadcast to the Ethereum network
|
||||||
|
* 4. We wait for the transaction to be mined (included in a block)
|
||||||
|
* 5. The block number and timestamp become the proof
|
||||||
|
*
|
||||||
|
* WHAT GETS STORED ON-CHAIN:
|
||||||
|
* - The document hash (bytes32)
|
||||||
|
* - The block timestamp (when the tx was mined)
|
||||||
|
* - The depositor address (our server wallet)
|
||||||
|
* - The file name (for reference)
|
||||||
|
*
|
||||||
|
* @param documentHash - SHA-256 hash of the document (bytes32)
|
||||||
|
* @param fileName - Original file name
|
||||||
|
* @returns Proof data including txHash, block info
|
||||||
|
*/
|
||||||
|
static async registerOnChain(
|
||||||
|
documentHash: string,
|
||||||
|
fileName: string
|
||||||
|
): Promise<BlockchainProof> {
|
||||||
|
if (!this.isConfigured()) {
|
||||||
|
throw new Error("Blockchain not configured. Check your .env variables.");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||||
|
console.log("🔗 Registering document on blockchain...");
|
||||||
|
console.log(`📄 File: ${fileName}`);
|
||||||
|
console.log(`🔐 Hash: ${documentHash.slice(0, 18)}...`);
|
||||||
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||||
|
|
||||||
|
const contract = getWriteContract();
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send the transaction to the smart contract (fileName is omitted on-chain)
|
||||||
|
const tx = await contract.registerDocument(documentHash);
|
||||||
|
console.log(`📤 Transaction sent: ${tx.hash}`);
|
||||||
|
|
||||||
|
// Wait for the transaction to be mined
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
console.log(`✅ Transaction mined in block #${receipt.blockNumber}`);
|
||||||
|
|
||||||
|
// Get the block to extract the timestamp
|
||||||
|
const block = await getProvider().getBlock(receipt.blockNumber);
|
||||||
|
const blockTimestamp = block
|
||||||
|
? new Date(block.timestamp * 1000)
|
||||||
|
: new Date();
|
||||||
|
|
||||||
|
// Build explorer URL for Sepolia
|
||||||
|
const explorerUrl =
|
||||||
|
config.network === "sepolia"
|
||||||
|
? `https://sepolia.etherscan.io/tx/${tx.hash}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const proof: BlockchainProof = {
|
||||||
|
documentHash,
|
||||||
|
txHash: tx.hash,
|
||||||
|
blockNumber: receipt.blockNumber,
|
||||||
|
blockTimestamp,
|
||||||
|
network: config.network as "hardhat" | "sepolia",
|
||||||
|
contractAddress: config.contractAddress,
|
||||||
|
explorerUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||||
|
console.log("✅ Document registered on-chain!");
|
||||||
|
console.log(` Block: #${proof.blockNumber}`);
|
||||||
|
console.log(` Time: ${proof.blockTimestamp.toISOString()}`);
|
||||||
|
console.log(` Tx: ${proof.txHash}`);
|
||||||
|
if (explorerUrl) {
|
||||||
|
console.log(` View: ${explorerUrl}`);
|
||||||
|
}
|
||||||
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||||
|
|
||||||
|
return proof;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Check if the document was already registered
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
if (errorMessage.includes("already registered")) {
|
||||||
|
console.log("ℹ️ Document already registered on-chain (idempotent)");
|
||||||
|
|
||||||
|
// Retrieve existing proof data
|
||||||
|
const verification = await this.verifyOnChain(documentHash);
|
||||||
|
return {
|
||||||
|
documentHash,
|
||||||
|
txHash: "already-registered",
|
||||||
|
blockNumber: 0,
|
||||||
|
blockTimestamp: new Date(verification.timestamp * 1000),
|
||||||
|
network: config.network as "hardhat" | "sepolia",
|
||||||
|
contractAddress: config.contractAddress,
|
||||||
|
explorerUrl: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("❌ Blockchain registration failed:", errorMessage);
|
||||||
|
throw new Error(`Blockchain registration failed: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if a document exists on the blockchain.
|
||||||
|
*
|
||||||
|
* This is a READ-ONLY operation (no gas cost, no transaction).
|
||||||
|
* It queries the smart contract's state to check if a hash
|
||||||
|
* was previously registered.
|
||||||
|
*
|
||||||
|
* @param documentHash - The SHA-256 hash to verify
|
||||||
|
* @returns Verification result with existence, timestamp, depositor
|
||||||
|
*/
|
||||||
|
static async verifyOnChain(
|
||||||
|
documentHash: string
|
||||||
|
): Promise<BlockchainVerification> {
|
||||||
|
if (!this.isReadConfigured()) {
|
||||||
|
throw new Error("Blockchain read access not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
const contract = getReadContract();
|
||||||
|
|
||||||
|
const [exists, timestamp, depositor] =
|
||||||
|
await contract.verifyDocument(documentHash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
exists: exists as boolean,
|
||||||
|
timestamp: Number(timestamp),
|
||||||
|
depositor: depositor as string,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a document AND register it on-chain in one step.
|
||||||
|
* This is the main entry point used by the contract analysis flow.
|
||||||
|
*/
|
||||||
|
static async hashAndRegister(
|
||||||
|
fileUrl: string,
|
||||||
|
fileName: string
|
||||||
|
): Promise<BlockchainProof> {
|
||||||
|
const documentHash = await this.hashDocument(fileUrl);
|
||||||
|
return await this.registerOnChain(documentHash, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get blockchain network stats for the explorer page header.
|
||||||
|
*/
|
||||||
|
static async getNetworkStats(): Promise<BlockchainStats> {
|
||||||
|
if (!this.isReadConfigured()) {
|
||||||
|
return {
|
||||||
|
totalVerified: 0,
|
||||||
|
latestBlockNumber: null,
|
||||||
|
networkName: "Not Configured",
|
||||||
|
networkStatus: "disconnected",
|
||||||
|
walletAddress: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const provider = getProvider();
|
||||||
|
const contract = getReadContract();
|
||||||
|
const config = getConfig();
|
||||||
|
let walletAddress = "";
|
||||||
|
|
||||||
|
if (isBlockchainWriteConfigured()) {
|
||||||
|
try {
|
||||||
|
walletAddress = getWallet().address;
|
||||||
|
} catch {
|
||||||
|
walletAddress = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [blockNumber, totalDocs, networkObj] = await Promise.all([
|
||||||
|
provider.getBlockNumber(),
|
||||||
|
contract.totalDocuments(),
|
||||||
|
provider.getNetwork()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalVerified: Number(totalDocs),
|
||||||
|
latestBlockNumber: blockNumber,
|
||||||
|
networkName: config.network === "sepolia" ? "Ethereum Sepolia" : "Hardhat Local",
|
||||||
|
networkStatus: "connected",
|
||||||
|
walletAddress,
|
||||||
|
chainId: Number(networkObj.chainId),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
const config = getConfig();
|
||||||
|
return {
|
||||||
|
totalVerified: 0,
|
||||||
|
latestBlockNumber: null,
|
||||||
|
networkName: config.network === "sepolia" ? "Ethereum Sepolia" : "Hardhat Local",
|
||||||
|
networkStatus: "disconnected",
|
||||||
|
walletAddress: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset cached provider/wallet connections.
|
||||||
|
* Useful if env vars change during development.
|
||||||
|
*/
|
||||||
|
static resetConnections() {
|
||||||
|
resetInstances();
|
||||||
|
}
|
||||||
|
}
|
||||||
61
lib/services/blockchain.types.ts
Normal file
61
lib/services/blockchain.types.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Blockchain Types
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// TypeScript interfaces for all blockchain-related data structures
|
||||||
|
// used across the service layer, server actions, and UI components.
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the on-chain proof data for a registered document.
|
||||||
|
* This is what gets stored in PostgreSQL after a successful blockchain tx.
|
||||||
|
*/
|
||||||
|
export interface BlockchainProof {
|
||||||
|
documentHash: string;
|
||||||
|
txHash: string;
|
||||||
|
blockNumber: number;
|
||||||
|
blockTimestamp: Date;
|
||||||
|
network: "hardhat" | "sepolia";
|
||||||
|
contractAddress: string;
|
||||||
|
explorerUrl: string | null; // Sepolia etherscan link (null for hardhat)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of verifying a document hash on-chain.
|
||||||
|
*/
|
||||||
|
export interface BlockchainVerification {
|
||||||
|
exists: boolean;
|
||||||
|
timestamp: number; // Unix timestamp (seconds)
|
||||||
|
depositor: string; // Ethereum address
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialized blockchain transaction for frontend display.
|
||||||
|
* All dates are ISO strings for safe JSON serialization across server/client.
|
||||||
|
*/
|
||||||
|
export interface BlockchainTransactionView {
|
||||||
|
id: string;
|
||||||
|
contractId: string;
|
||||||
|
contractTitle: string | null;
|
||||||
|
contractFileName: string;
|
||||||
|
documentHash: string;
|
||||||
|
txHash: string;
|
||||||
|
blockNumber: number;
|
||||||
|
blockTimestamp: string; // ISO string
|
||||||
|
network: string;
|
||||||
|
contractAddress: string;
|
||||||
|
status: string;
|
||||||
|
createdAt: string; // ISO string
|
||||||
|
explorerUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stats displayed at the top of the blockchain explorer page.
|
||||||
|
*/
|
||||||
|
export interface BlockchainStats {
|
||||||
|
totalVerified: number;
|
||||||
|
latestBlockNumber: number | null;
|
||||||
|
networkName: string;
|
||||||
|
networkStatus: "connected" | "disconnected";
|
||||||
|
walletAddress: string;
|
||||||
|
chainId?: number;
|
||||||
|
}
|
||||||
107
package-lock.json
generated
107
package-lock.json
generated
@@ -47,6 +47,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"ethers": "^6.16.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
"jspdf-autotable": "^5.0.7",
|
"jspdf-autotable": "^5.0.7",
|
||||||
@@ -82,6 +83,12 @@
|
|||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@adraffy/ens-normalize": {
|
||||||
|
"version": "1.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
|
||||||
|
"integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
@@ -1718,6 +1725,30 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@noble/curves": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -4338,6 +4369,12 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/aes-js": {
|
||||||
|
"version": "4.0.0-beta.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
|
||||||
|
"integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||||
@@ -6154,6 +6191,55 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ethers": {
|
||||||
|
"version": "6.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz",
|
||||||
|
"integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/ethers-io/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.buymeacoffee.com/ricmoo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@adraffy/ens-normalize": "1.10.1",
|
||||||
|
"@noble/curves": "1.2.0",
|
||||||
|
"@noble/hashes": "1.3.2",
|
||||||
|
"@types/node": "22.7.5",
|
||||||
|
"aes-js": "4.0.0-beta.5",
|
||||||
|
"tslib": "2.7.0",
|
||||||
|
"ws": "8.17.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ethers/node_modules/@types/node": {
|
||||||
|
"version": "22.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
|
||||||
|
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.19.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ethers/node_modules/tslib": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/ethers/node_modules/undici-types": {
|
||||||
|
"version": "6.19.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/eventemitter3": {
|
"node_modules/eventemitter3": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||||
@@ -10231,6 +10317,27 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"ethers": "^6.16.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
"jspdf-autotable": "^5.0.7",
|
"jspdf-autotable": "^5.0.7",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ model User {
|
|||||||
|
|
||||||
contracts Contract[]
|
contracts Contract[]
|
||||||
notifications Notification[]
|
notifications Notification[]
|
||||||
|
blockchainTransactions BlockchainTransaction[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -53,14 +54,18 @@ model Contract {
|
|||||||
summary String? @db.Text
|
summary String? @db.Text
|
||||||
keyPoints Json?
|
keyPoints Json?
|
||||||
|
|
||||||
// Blockchain (later)
|
// Blockchain proof-of-deposit
|
||||||
documentHash String?
|
documentHash String? // SHA-256 hash of the document
|
||||||
txHash String?
|
txHash String? // Ethereum transaction hash
|
||||||
ipfsUrl String?
|
blockNumber Int? // Block number where tx was mined
|
||||||
|
blockTimestamp DateTime? // Timestamp of the block
|
||||||
|
blockchainNetwork String? // 'hardhat' | 'sepolia'
|
||||||
|
contractAddress String? // Smart contract address used
|
||||||
|
|
||||||
// Notifications for this contract
|
// Relations
|
||||||
notifications Notification[]
|
notifications Notification[]
|
||||||
ragChunks ContractRagChunk[]
|
ragChunks ContractRagChunk[]
|
||||||
|
blockchainTransactions BlockchainTransaction[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -120,6 +125,30 @@ model Notification {
|
|||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model BlockchainTransaction {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
contractId String
|
||||||
|
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
documentHash String // SHA-256 hash of the document
|
||||||
|
txHash String @unique // Ethereum transaction hash
|
||||||
|
blockNumber Int // Block number where tx was mined
|
||||||
|
blockTimestamp DateTime // Block timestamp = proof date
|
||||||
|
network String // 'hardhat' | 'sepolia'
|
||||||
|
contractAddress String // Smart contract address used
|
||||||
|
status String @default("CONFIRMED") // PENDING, CONFIRMED, FAILED
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([contractId])
|
||||||
|
@@index([txHash])
|
||||||
|
@@index([network])
|
||||||
|
}
|
||||||
|
|
||||||
enum NotificationType {
|
enum NotificationType {
|
||||||
SUCCESS // Successful action
|
SUCCESS // Successful action
|
||||||
WARNING // Warning/Alert
|
WARNING // Warning/Alert
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ export interface Contract {
|
|||||||
|
|
||||||
documentHash: string | null;
|
documentHash: string | null;
|
||||||
txHash: string | null;
|
txHash: string | null;
|
||||||
ipfsUrl: string | null;
|
blockNumber: number | null;
|
||||||
|
blockTimestamp: Date | null;
|
||||||
|
blockchainNetwork: string | null;
|
||||||
|
contractAddress: string | null;
|
||||||
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|||||||
Reference in New Issue
Block a user