Blockchain added
This commit is contained in:
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 { RAGService } from "@/lib/services/rag.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";
|
||||
|
||||
type ContractListItem = Awaited<
|
||||
ReturnType<typeof ContractService.getAll>
|
||||
>[number] & {
|
||||
_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 & {
|
||||
@@ -196,6 +205,13 @@ export async function getContracts(filters?: Record<string, unknown>) {
|
||||
extractedText: contract.extractedText || null,
|
||||
ragChunkCount: Number(contract?._count?.ragChunks ?? 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 };
|
||||
@@ -501,6 +517,52 @@ export async function analyzeContractAction(id: string) {
|
||||
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
|
||||
const contractTitle = aiResults.title || "Contract";
|
||||
const contractProvider = aiResults.provider || "Unknown Provider";
|
||||
|
||||
@@ -171,47 +171,68 @@ export function ContractUploadForm({
|
||||
</div>
|
||||
|
||||
{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="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="flex flex-col items-center text-center space-y-6">
|
||||
<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 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">
|
||||
{/* 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="absolute inset-0 rounded-full bg-primary/30 blur-xl animate-pulse"></div>
|
||||
<div className="relative rounded-full bg-gradient-to-br from-primary to-accent p-4">
|
||||
<Sparkles className="h-8 w-8 animate-pulse text-white" />
|
||||
<div className="absolute inset-0 rounded-full bg-primary/40 blur-2xl animate-pulse"></div>
|
||||
<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">
|
||||
<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 className="relative">
|
||||
<Loader2 className="h-11 w-11 animate-spin text-primary" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-semibold text-foreground">
|
||||
Analyzing And Building RAG
|
||||
{/* Text Section */}
|
||||
<div className="space-y-3">
|
||||
<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
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your contract is being analyzed and indexed for chat...
|
||||
<p className="text-base text-muted-foreground/90 font-medium">
|
||||
We're parsing your document and building a semantic index...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-2">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>Processing</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-primary animate-bounce [animation-delay:-0.3s]"></span>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-primary animate-bounce [animation-delay:-0.15s]"></span>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-primary animate-bounce"></span>
|
||||
{/* Progress Section */}
|
||||
<div className="w-full space-y-4 px-2">
|
||||
<div className="flex items-center justify-between text-[13px] font-semibold">
|
||||
<span className="text-primary flex items-center gap-2">
|
||||
<Wand2 className="h-3.5 w-3.5" />
|
||||
Processing RAG
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div className="h-full w-full rounded-full bg-gradient-to-r from-primary to-accent animate-progress-loading origin-left"></div>
|
||||
<div className="relative h-3 w-full overflow-hidden rounded-full bg-muted/40 border border-border/20">
|
||||
<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>
|
||||
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
This may take up to 10 seconds
|
||||
</p>
|
||||
{/* Footer info */}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user