diff --git a/frontend/avacertify-v2/.gitignore b/frontend/avacertify-v2/.gitignore
index fd3dbb5..1843fa1 100644
--- a/frontend/avacertify-v2/.gitignore
+++ b/frontend/avacertify-v2/.gitignore
@@ -34,3 +34,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+# files
+/.dashboard_lighthouse_report.pdf
\ No newline at end of file
diff --git a/frontend/avacertify-v2/app/dashboard/page.tsx b/frontend/avacertify-v2/app/dashboard/page.tsx
index 16598a5..b300d0b 100644
--- a/frontend/avacertify-v2/app/dashboard/page.tsx
+++ b/frontend/avacertify-v2/app/dashboard/page.tsx
@@ -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}...`);
@@ -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();
@@ -211,6 +227,8 @@ export default function Dashboard() {
}
}, [toast, checkNetwork]);
+
+
/**
* Filters certificates based on search query
*/
@@ -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
*/
@@ -325,6 +354,14 @@ export default function Dashboard() {
>
)}
+
{isLoading && (
diff --git a/frontend/avacertify-v2/app/verify/page.tsx b/frontend/avacertify-v2/app/verify/page.tsx
index d72b5ba..96e76e7 100644
--- a/frontend/avacertify-v2/app/verify/page.tsx
+++ b/frontend/avacertify-v2/app/verify/page.tsx
@@ -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 });
diff --git a/frontend/avacertify-v2/utils/blockchain.ts b/frontend/avacertify-v2/utils/blockchain.ts
index 61b7687..faae926 100644
--- a/frontend/avacertify-v2/utils/blockchain.ts
+++ b/frontend/avacertify-v2/utils/blockchain.ts
@@ -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;
@@ -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 {
if (!this.isInitialized) {
throw new Error("Service not initialized. Call init() first.");
@@ -605,34 +625,32 @@ 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 {
- 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 {
- 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;
}
/**
@@ -640,19 +658,20 @@ export class CertificateService {
* Used for public certificate viewing.
*/
async getCertificateReadOnly(certificateId: string, isNFT: boolean = false): Promise {
- 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);
@@ -666,6 +685,9 @@ export class CertificateService {
issueDate: new Date().toISOString(),
institutionName: "AvaCertify",
status: "active",
+ transactionHash: undefined,
+ documentHash: undefined,
+ documentUrl: tokenURI,
isNFT: true,
};
} else {
@@ -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 {
+ 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");
+ }
+ }
}