316 lines
9.4 KiB
TypeScript
316 lines
9.4 KiB
TypeScript
/**
|
|
* 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",
|
|
};
|
|
}
|
|
}
|