diff --git a/src/components/address/address-view.tsx b/src/components/address/address-view.tsx
index 97d3405..976f8a7 100644
--- a/src/components/address/address-view.tsx
+++ b/src/components/address/address-view.tsx
@@ -116,7 +116,11 @@ export default function AddressView({ addresses }: { addresses: string }) {
() => addresses.split(",").filter(Boolean),
[addresses],
);
- const { resolvedAddresses } = useAlgorandAddresses(addressesArray);
+ const {
+ resolvedAddresses,
+ loading: addressLoading,
+ hasError: addressError,
+ } = useAlgorandAddresses(addressesArray);
// Function to update addresses in both state and URL
const handleAddAddresses = (newAddresses: string[]) => {
@@ -215,6 +219,8 @@ export default function AddressView({ addresses }: { addresses: string }) {
showFilters={showFilters}
setShowFilters={setShowFilters}
blocks={filteredBlocks}
+ loading={addressLoading}
+ hasError={addressError}
/>
- {resolvedAddresses.length === 1 && (
+ {resolvedAddresses.length === 1 && resolvedAddresses[0] && (
- {displayAlgoAddress(resolvedAddresses[0].address)}
+ {displayAlgoAddress(resolvedAddresses[0]?.address)}
- {resolvedAddresses[0].address}
+ {resolvedAddresses[0]?.address}
diff --git a/src/hooks/useAlgorandAddress.ts b/src/hooks/useAlgorandAddress.ts
index a02e81f..08a74ab 100644
--- a/src/hooks/useAlgorandAddress.ts
+++ b/src/hooks/useAlgorandAddress.ts
@@ -1,6 +1,7 @@
import { resolveNFD } from "@/hooks/queries/useNFD";
import * as React from "react";
import { ResolvedAddress } from "@/components/heatmap/types.ts";
+import { toast } from "sonner";
export const useAlgorandAddresses = (addresses: string[]) => {
const [resolvedAddresses, setResolvedAddresses] = React.useState<
@@ -16,13 +17,30 @@ export const useAlgorandAddresses = (addresses: string[]) => {
const resolved = await Promise.all(
addresses.map(async (address) => {
if (address.toLowerCase().endsWith(".algo")) {
- return { address: await resolveNFD(address), nfd: address };
+ const resolvedAddr = await resolveNFD(address);
+ // If NFD resolution fails (expired, not found, etc.), skip it
+ if (!resolvedAddr || resolvedAddr.length !== 58) {
+ toast.error(
+ `NFD "${address}" could not be resolved. It may be expired or not found.`,
+ );
+ return null;
+ }
+ return { address: resolvedAddr, nfd: address };
}
return { address, nfd: null };
}),
);
- setResolvedAddresses(resolved);
+ // Filter out any null values from failed NFD resolutions
+ const validAddresses = resolved.filter(
+ (addr): addr is ResolvedAddress => addr !== null,
+ );
+ setResolvedAddresses(validAddresses);
+
+ // If we had addresses but none resolved successfully, that's an error state
+ if (addresses.length > 0 && validAddresses.length === 0) {
+ setError(true);
+ }
} catch (err) {
console.error(err);
setError(true);
diff --git a/src/lib/block-storage.ts b/src/lib/block-storage.ts
index b71505a..c0579c7 100644
--- a/src/lib/block-storage.ts
+++ b/src/lib/block-storage.ts
@@ -15,107 +15,160 @@ interface BlockCache {
lastUpdated: number;
}
+let isIndexedDBAvailable: boolean | null = null;
+
+function checkIndexedDBAvailability(): boolean {
+ if (isIndexedDBAvailable !== null) {
+ return isIndexedDBAvailable;
+ }
+
+ try {
+ // Check if indexedDB exists and is accessible
+ if (typeof indexedDB === "undefined" || indexedDB === null) {
+ isIndexedDBAvailable = false;
+ return false;
+ }
+
+ // Try to access a property to ensure it's not a restricted context
+ // Some contexts throw on property access
+ const testAccess = indexedDB.open;
+ if (!testAccess) {
+ isIndexedDBAvailable = false;
+ return false;
+ }
+
+ isIndexedDBAvailable = true;
+ return true;
+ } catch (error) {
+ console.warn("IndexedDB is not available in this context:", error);
+ isIndexedDBAvailable = false;
+ return false;
+ }
+}
+
export async function initDB(): Promise
{
+ // Check availability first without throwing
+ if (!checkIndexedDBAvailability()) {
+ return Promise.reject(
+ new Error("IndexedDB is not available in this context"),
+ );
+ }
+
return new Promise((resolve, reject) => {
- const request = indexedDB.open(DB_NAME, DB_VERSION);
-
- request.onerror = () => {
- reject(
- new Error(
- `Failed to open database: ${request.error}. Operation: initDB`,
- ),
- );
- };
-
- request.onsuccess = () => {
- resolve(request.result);
- };
-
- request.onupgradeneeded = (event) => {
- const db = (event.target as IDBOpenDBRequest).result;
-
- // Create the store if it doesn't exist
- if (!db.objectStoreNames.contains(BLOCKS_STORE)) {
- const store = db.createObjectStore(BLOCKS_STORE, {
- keyPath: "address",
- });
- store.createIndex("address", "address", { unique: true });
- }
- };
+ try {
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
+
+ request.onerror = () => {
+ reject(
+ new Error(
+ `Failed to open database: ${request.error}. Operation: initDB`,
+ ),
+ );
+ };
+
+ request.onsuccess = () => {
+ resolve(request.result);
+ };
+
+ request.onupgradeneeded = (event) => {
+ const db = (event.target as IDBOpenDBRequest).result;
+
+ // Create the store if it doesn't exist
+ if (!db.objectStoreNames.contains(BLOCKS_STORE)) {
+ const store = db.createObjectStore(BLOCKS_STORE, {
+ keyPath: "address",
+ });
+ store.createIndex("address", "address", { unique: true });
+ }
+ };
+ } catch (error) {
+ reject(new Error(`IndexedDB access denied: ${error}`));
+ }
});
}
export async function getBlocksFromCache(
address: string,
): Promise {
- const db = await initDB();
-
- return new Promise((resolve, reject) => {
- const transaction = db.transaction([BLOCKS_STORE], "readonly");
- const store = transaction.objectStore(BLOCKS_STORE);
- const request = store.get(address);
-
- request.onerror = () => {
- reject(
- new Error(
- `Failed to get blocks for ${address}: ${request.error}. Operation: getBlocksFromCache`,
- ),
- );
- };
-
- request.onsuccess = () => {
- const result = request.result as BlockCache | undefined;
-
- if (!result || !result.blocks) {
- resolve(null);
- return;
- }
-
- const blocks = result.blocks.map(fromSerializableBlock);
- resolve(blocks);
- };
-
- transaction.oncomplete = () => {
- db.close();
- };
- });
+ try {
+ const db = await initDB();
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction([BLOCKS_STORE], "readonly");
+ const store = transaction.objectStore(BLOCKS_STORE);
+ const request = store.get(address);
+
+ request.onerror = () => {
+ reject(
+ new Error(
+ `Failed to get blocks for ${address}: ${request.error}. Operation: getBlocksFromCache`,
+ ),
+ );
+ };
+
+ request.onsuccess = () => {
+ const result = request.result as BlockCache | undefined;
+
+ if (!result || !result.blocks) {
+ resolve(null);
+ return;
+ }
+
+ const blocks = result.blocks.map(fromSerializableBlock);
+ resolve(blocks);
+ };
+
+ transaction.oncomplete = () => {
+ db.close();
+ };
+ });
+ } catch (error) {
+ console.warn("Cache unavailable, returning null:", error);
+ return null;
+ }
}
export async function saveBlocksToCache(
address: string,
blocks: MinimalBlock[],
): Promise {
- const db = await initDB();
-
- return new Promise((resolve, reject) => {
- const transaction = db.transaction([BLOCKS_STORE], "readwrite");
- const store = transaction.objectStore(BLOCKS_STORE);
- const serializableBlocks = blocks.map(toSerializableBlock);
-
- const cache: BlockCache = {
- address,
- blocks: serializableBlocks,
- lastUpdated: Date.now(),
- };
-
- const request = store.put(cache);
-
- request.onerror = () => {
- reject(
- new Error(
- `Failed to save blocks for ${address}: ${request.error}. Operation: saveBlocksToCache`,
- ),
- );
- };
-
- request.onsuccess = () => {
- resolve();
- };
-
- transaction.oncomplete = () => {
- db.close();
- };
- });
+ try {
+ const db = await initDB();
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction([BLOCKS_STORE], "readwrite");
+ const store = transaction.objectStore(BLOCKS_STORE);
+ const serializableBlocks = blocks.map(toSerializableBlock);
+
+ const cache: BlockCache = {
+ address,
+ blocks: serializableBlocks,
+ lastUpdated: Date.now(),
+ };
+
+ const request = store.put(cache);
+
+ request.onerror = () => {
+ reject(
+ new Error(
+ `Failed to save blocks for ${address}: ${request.error}. Operation: saveBlocksToCache`,
+ ),
+ );
+ };
+
+ request.onsuccess = () => {
+ resolve();
+ };
+
+ transaction.oncomplete = () => {
+ db.close();
+ };
+ });
+ } catch (error) {
+ console.warn("Cache unavailable, skipping save:", error);
+ // Gracefully continue without caching
+ return Promise.resolve();
+ }
}
export async function getMaxRoundFromCache(
@@ -131,51 +184,61 @@ export async function getMaxRoundFromCache(
}
export async function clearCacheForAddress(address: string): Promise {
- const db = await initDB();
-
- return new Promise((resolve, reject) => {
- const transaction = db.transaction([BLOCKS_STORE], "readwrite");
- const store = transaction.objectStore(BLOCKS_STORE);
- const request = store.delete(address);
-
- request.onerror = () => {
- reject(
- new Error(
- `Failed to clear cache for ${address}: ${request.error}. Operation: clearCacheForAddress`,
- ),
- );
- };
-
- request.onsuccess = () => {
- resolve();
- };
-
- transaction.oncomplete = () => {
- db.close();
- };
- });
+ try {
+ const db = await initDB();
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction([BLOCKS_STORE], "readwrite");
+ const store = transaction.objectStore(BLOCKS_STORE);
+ const request = store.delete(address);
+
+ request.onerror = () => {
+ reject(
+ new Error(
+ `Failed to clear cache for ${address}: ${request.error}. Operation: clearCacheForAddress`,
+ ),
+ );
+ };
+
+ request.onsuccess = () => {
+ resolve();
+ };
+
+ transaction.oncomplete = () => {
+ db.close();
+ };
+ });
+ } catch (error) {
+ console.warn("Cache unavailable, skipping clear:", error);
+ return Promise.resolve();
+ }
}
export async function clearAllCache(): Promise {
- const db = await initDB();
-
- return new Promise((resolve, reject) => {
- const transaction = db.transaction([BLOCKS_STORE], "readwrite");
- const store = transaction.objectStore(BLOCKS_STORE);
- const request = store.clear();
-
- request.onerror = () => {
- reject(new Error(`Failed to clear all cache: ${request.error}`));
- };
-
- request.onsuccess = () => {
- resolve();
- };
-
- transaction.oncomplete = () => {
- db.close();
- };
- });
+ try {
+ const db = await initDB();
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction([BLOCKS_STORE], "readwrite");
+ const store = transaction.objectStore(BLOCKS_STORE);
+ const request = store.clear();
+
+ request.onerror = () => {
+ reject(new Error(`Failed to clear all cache: ${request.error}`));
+ };
+
+ request.onsuccess = () => {
+ resolve();
+ };
+
+ transaction.oncomplete = () => {
+ db.close();
+ };
+ });
+ } catch (error) {
+ console.warn("Cache unavailable, skipping clear:", error);
+ return Promise.resolve();
+ }
}
export async function getAllCachedAddresses(): Promise<
@@ -186,94 +249,111 @@ export async function getAllCachedAddresses(): Promise<
sizeInBytes: number;
}>
> {
- const db = await initDB();
-
- return new Promise((resolve, reject) => {
- const transaction = db.transaction([BLOCKS_STORE], "readonly");
- const store = transaction.objectStore(BLOCKS_STORE);
- const request = store.getAll();
-
- request.onsuccess = () => {
- const allCaches: BlockCache[] = request.result;
- const results = allCaches.map((cache) => {
- const sizeInBytes = new Blob([JSON.stringify(cache)]).size;
-
- return {
- address: cache.address,
- blockCount: cache.blocks.length,
- lastUpdated: cache.lastUpdated,
- sizeInBytes,
- };
- });
- resolve(results);
- };
-
- request.onerror = () => {
- reject(new Error(`Failed to get all cached addresses: ${request.error}`));
- };
-
- transaction.oncomplete = () => {
- db.close();
- };
- });
+ try {
+ const db = await initDB();
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction([BLOCKS_STORE], "readonly");
+ const store = transaction.objectStore(BLOCKS_STORE);
+ const request = store.getAll();
+
+ request.onsuccess = () => {
+ const allCaches: BlockCache[] = request.result;
+ const results = allCaches.map((cache) => {
+ const sizeInBytes = new Blob([JSON.stringify(cache)]).size;
+
+ return {
+ address: cache.address,
+ blockCount: cache.blocks.length,
+ lastUpdated: cache.lastUpdated,
+ sizeInBytes,
+ };
+ });
+ resolve(results);
+ };
+
+ request.onerror = () => {
+ reject(
+ new Error(`Failed to get all cached addresses: ${request.error}`),
+ );
+ };
+
+ transaction.oncomplete = () => {
+ db.close();
+ };
+ });
+ } catch (error) {
+ console.warn("Cache unavailable, returning empty array:", error);
+ return [];
+ }
}
export async function getCachedAddresses(): Promise {
- const db = await initDB();
-
- return new Promise((resolve, reject) => {
- const transaction = db.transaction([BLOCKS_STORE], "readonly");
- const store = transaction.objectStore(BLOCKS_STORE);
- const request = store.getAllKeys();
-
- request.onerror = () => {
- reject(new Error(`Failed to get cached addresses: ${request.error}`));
- };
-
- request.onsuccess = () => {
- resolve(request.result as string[]);
- };
-
- transaction.oncomplete = () => {
- db.close();
- };
- });
+ try {
+ const db = await initDB();
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction([BLOCKS_STORE], "readonly");
+ const store = transaction.objectStore(BLOCKS_STORE);
+ const request = store.getAllKeys();
+
+ request.onerror = () => {
+ reject(new Error(`Failed to get cached addresses: ${request.error}`));
+ };
+
+ request.onsuccess = () => {
+ resolve(request.result as string[]);
+ };
+
+ transaction.oncomplete = () => {
+ db.close();
+ };
+ });
+ } catch (error) {
+ console.warn("Cache unavailable, returning empty array:", error);
+ return [];
+ }
}
export async function getCacheMetadata(
address: string,
): Promise<{ lastUpdated: number; blockCount: number } | null> {
- const db = await initDB();
-
- return new Promise((resolve, reject) => {
- const transaction = db.transaction([BLOCKS_STORE], "readonly");
- const store = transaction.objectStore(BLOCKS_STORE);
- const request = store.get(address);
-
- request.onerror = () => {
- reject(
- new Error(
- `Failed to get cache metadata for ${address}: ${request.error}`,
- ),
- );
- };
-
- request.onsuccess = () => {
- const result = request.result as BlockCache | undefined;
-
- if (!result) {
- resolve(null);
- return;
- }
-
- resolve({
- lastUpdated: result.lastUpdated,
- blockCount: result.blocks.length,
- });
- };
-
- transaction.oncomplete = () => {
- db.close();
- };
- });
+ try {
+ const db = await initDB();
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction([BLOCKS_STORE], "readonly");
+ const store = transaction.objectStore(BLOCKS_STORE);
+ const request = store.get(address);
+
+ request.onerror = () => {
+ reject(
+ new Error(
+ `Failed to get cache metadata for ${address}: ${request.error}`,
+ ),
+ );
+ };
+
+ request.onsuccess = () => {
+ const result = request.result as BlockCache | undefined;
+
+ if (!result) {
+ resolve(null);
+ return;
+ }
+
+ resolve({
+ lastUpdated: result.lastUpdated,
+ blockCount: result.blocks.length,
+ });
+ };
+
+ transaction.oncomplete = () => {
+ db.close();
+ };
+ });
+ } catch (error) {
+ console.warn("Cache unavailable, returning null:", error);
+ return null;
+ }
}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index a288576..2965070 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -5,7 +5,13 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
-export function displayAlgoAddress(address: string, lettersToDisplay = 5) {
+export function displayAlgoAddress(
+ address: string | undefined,
+ lettersToDisplay = 5,
+) {
+ if (!address || address.length < lettersToDisplay * 2) {
+ return address || "";
+ }
return `${address.slice(0, lettersToDisplay)}...${address.slice(
-lettersToDisplay,
)}`;