Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/avacertify-v2/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# files
/.dashboard_lighthouse_report.pdf
45 changes: 41 additions & 4 deletions frontend/avacertify-v2/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,19 @@ export default function Dashboard() {

const allCertificates: Certificate[] = [];
const BLOCK_RANGE = 2000; // Stay under 2048 limit
const MAX_BLOCKS_TO_SCAN = 1000000000; // Scan up to 1000 million blocks back

const MAX_BLOCKS_TO_SCAN = 50000000; // Scan up to 50 million blocks back (deployed block for first contract)
const currentBlock = await provider.getBlockNumber();
const startBlock = Math.max(0, currentBlock - MAX_BLOCKS_TO_SCAN);

// Load from cache
const lastScannedBlock = parseInt(localStorage.getItem("lastScannedBlock") || "0");
const cachedCertificates = JSON.parse(localStorage.getItem("cachedCertificates") || "[]") as Certificate[];

allCertificates.push(...cachedCertificates);

const startBlock = lastScannedBlock > 0
? lastScannedBlock + 1
: Math.max(0, currentBlock - MAX_BLOCKS_TO_SCAN);

console.log(`Scanning from block ${startBlock} to ${currentBlock}`);
setLoadingProgress(`Scanning blocks ${startBlock} to ${currentBlock}...`);
Expand Down Expand Up @@ -170,11 +179,18 @@ export default function Dashboard() {
console.error("Failed to query NFT certificate events:", error);
}

// Merge with cached certificates
const allCertificatesWithCache = [...cachedCertificates, ...allCertificates];

// Remove duplicates
const uniqueCertificates = Array.from(
new Map(allCertificates.map(cert => [cert.id, cert])).values()
new Map(allCertificatesWithCache.map(cert => [cert.id, cert])).values()
);

// Save to cache
localStorage.setItem("lastScannedBlock", currentBlock.toString());
localStorage.setItem("cachedCertificates", JSON.stringify(uniqueCertificates));

// Sort by issue date (newest first)
uniqueCertificates.sort((a, b) => {
const dateA = new Date(a.issueDate).getTime();
Expand Down Expand Up @@ -211,6 +227,8 @@ export default function Dashboard() {
}
}, [toast, checkNetwork]);



/**
* Filters certificates based on search query
*/
Expand Down Expand Up @@ -266,6 +284,17 @@ export default function Dashboard() {
await fetchCertificatesFromBlockchain();
};

// 3. Add Clear Cache handler:

const handleClearCache = () => {
localStorage.removeItem("lastScannedBlock");
localStorage.removeItem("cachedCertificates");
toast({
title: "Cache Cleared",
description: "Next refresh will perform a full rescan",
});
};

/**
* Copies text to clipboard with user feedback
*/
Expand Down Expand Up @@ -325,6 +354,14 @@ export default function Dashboard() {
</>
)}
</Button>
<Button
onClick={handleClearCache}
disabled={isLoading}
variant="outline"
size="sm"
>
Clear Cache
</Button>
</div>

{isLoading && (
Expand Down
8 changes: 4 additions & 4 deletions frontend/avacertify-v2/app/verify/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ export default function Verify() {

setVerificationStatus("loading");

try {
try {
await certificateService.init();
const isValid = await certificateService.verifyCertificate(id, isNFT);
const cert = await certificateService.getCertificate(id, isNFT);

const isValid = await certificateService.verifyCertificateReadOnly(id, isNFT);
const cert = await certificateService.getCertificateReadOnly(id, isNFT);

if (cert && isValid) {
setCertificate({ ...cert, isNFT });
Expand Down
95 changes: 72 additions & 23 deletions frontend/avacertify-v2/utils/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ declare global {

export class CertificateService {
private provider: ethers.BrowserProvider | null = null;
/**
* Public JSON-RPC provider for read-only operations (no wallet required)
*/
private publicProvider: ethers.JsonRpcProvider | null = null;
private contract: (ethers.Contract & ContractMethods) | null = null;
private nftContract: (ethers.Contract & NFTContractMethods) | null = null;
private signer: ethers.Signer | null = null;
Expand All @@ -69,21 +73,37 @@ export class CertificateService {
return;
}

// Set up a public read-only provider
try {
// Use the RPC from your network config, or hardcode if needed
const rpcUrl =
(Array.isArray(AVALANCHE_FUJI_CONFIG.rpcUrls) &&
AVALANCHE_FUJI_CONFIG.rpcUrls[0]) ||
(AVALANCHE_FUJI_CONFIG as any).rpcUrls?.[0] ||
"https://api.avax-test.network/ext/bc/C/rpc";

this.publicProvider = new ethers.JsonRpcProvider(rpcUrl);
console.log("✅ Public RPC provider initialized");
} catch (error) {
console.error("❌ Failed to initialize public RPC provider:", error);
}

// Browser provider (wallet) is optional for read-only flows
if (typeof window === "undefined" || !window.ethereum) {
console.warn("No Web3 provider found");
console.warn("No Web3 wallet provider found (MetaMask/Core). Read-only RPC still available.");
this.isInitialized = true;
return;
}

try {
this.provider = new ethers.BrowserProvider(window.ethereum);
this.isInitialized = true;
console.log("✅ Provider initialized");
console.log("✅ Wallet provider initialized");
} catch (error) {
console.error("❌ Failed to initialize provider:", error);
console.error("❌ Failed to initialize wallet provider:", error);
throw new Error("Failed to initialize wallet connection");
}
}

private async validateConnection(): Promise<void> {
if (!this.isInitialized) {
throw new Error("Service not initialized. Call init() first.");
Expand Down Expand Up @@ -605,54 +625,53 @@ export class CertificateService {

/**
* Get a read-only instance of the certificate contract.
* Does not require a wallet connection, useful for querying events.
* Uses public RPC, does not require a wallet.
*/
async getReadOnlyContract(): Promise<ethers.Contract & ContractMethods> {
if (!this.provider) {
throw new Error("Provider not initialized. Call init() first.");
if (!this.publicProvider) {
throw new Error("Public provider not initialized. Call init() first.");
}
// Create a read-only contract instance using the provider
return new ethers.Contract(
CERTIFICATE_SYSTEM_ADDRESS,
CERTIFICATE_SYSTEM_ABI,
this.provider
) as ethers.Contract & ContractMethods;
CERTIFICATE_SYSTEM_ADDRESS,
CERTIFICATE_SYSTEM_ABI,
this.publicProvider
) as ethers.Contract & ContractMethods;
}

/**
* Get a read-only instance of the NFT certificate contract.
* Does not require a wallet connection, useful for querying events.
* Uses public RPC, does not require a wallet.
*/
async getReadOnlyNFTContract(): Promise<ethers.Contract & NFTContractMethods> {
if (!this.provider) {
throw new Error("Provider not initialized. Call init() first.");
if (!this.publicProvider) {
throw new Error("Public provider not initialized. Call init() first.");
}
// Create a read-only contract instance using the provider
return new ethers.Contract(
NFT_CERTIFICATE_ADDRESS,
NFT_CERTIFICATE_ABI,
this.provider
) as ethers.Contract & NFTContractMethods;
NFT_CERTIFICATE_ADDRESS,
NFT_CERTIFICATE_ABI,
this.publicProvider
) as ethers.Contract & NFTContractMethods;
}

/**
* Get certificate details without requiring wallet connection (read-only).
* Used for public certificate viewing.
*/
async getCertificateReadOnly(certificateId: string, isNFT: boolean = false): Promise<Certificate | null> {
if (!this.provider) {
throw new Error("Provider not initialized. Call init() first.");
if (!this.publicProvider) {
throw new Error("Public provider not initialized. Call init() first.");
}

if (!certificateId?.trim()) {
throw new Error("Certificate ID is required");
}


try {
if (isNFT) {
const nftContract = await this.getReadOnlyNFTContract();
const owner = await nftContract.ownerOf(certificateId);
if (owner === ethers.ZeroAddress) {
if (!owner || owner === ethers.ZeroAddress) {
return null;
}
const tokenURI = await nftContract.tokenURI(certificateId);
Expand All @@ -666,6 +685,9 @@ export class CertificateService {
issueDate: new Date().toISOString(),
institutionName: "AvaCertify",
status: "active",
transactionHash: undefined,
documentHash: undefined,
documentUrl: tokenURI,
isNFT: true,
};
} else {
Expand All @@ -692,6 +714,33 @@ export class CertificateService {
throw new Error("Failed to retrieve certificate");
}
}

/**
* Verify certificate without requiring wallet connection (read-only).
*/
async verifyCertificateReadOnly(certificateId: string, isNFT: boolean = false): Promise<boolean> {
if (!this.publicProvider) {
throw new Error("Public provider not initialized. Call init() first.");
}
if (!certificateId?.trim()) {
throw new Error("Certificate ID is required");
}

try {
if (isNFT) {
const nftContract = await this.getReadOnlyNFTContract();
const owner = await nftContract.ownerOf(certificateId);
return !!owner && owner !== ethers.ZeroAddress;
} else {
const contract = await this.getReadOnlyContract();
const isValid = await contract.verifyCertificate(certificateId);
return Boolean(isValid);
}
} catch (error: any) {
console.error("Error verifying certificate (read-only):", error);
throw new Error("Failed to verify certificate");
}
}

}

Expand Down
Loading