diff --git a/.env.test b/.env.test
index f42629d..43326e1 100644
--- a/.env.test
+++ b/.env.test
@@ -1 +1 @@
-DUNE_API_KEY=
\ No newline at end of file
+DUNE_API_KEY=test
\ No newline at end of file
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index a67c3e4..acddc48 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -16,5 +16,8 @@ jobs:
- name: Install dependencies
run: npm install --no-audit --no-fund
- - name: "Test: prettier"
+ - name: "Test: Prettier"
run: npm run test:prettier
+
+ - name: "Test: Vitest"
+ run: npm test
diff --git a/__tests__/hooks/useTokenBalances.test.tsx b/__tests__/hooks/useEvmTokenBalances.test.tsx
similarity index 75%
rename from __tests__/hooks/useTokenBalances.test.tsx
rename to __tests__/hooks/useEvmTokenBalances.test.tsx
index f052bb7..161b975 100644
--- a/__tests__/hooks/useTokenBalances.test.tsx
+++ b/__tests__/hooks/useEvmTokenBalances.test.tsx
@@ -1,16 +1,16 @@
import React from "react";
import { renderHook, waitFor } from "@testing-library/react";
import { DuneProvider } from "../../src/provider";
-import { useTokenBalances } from "../../src/useTokenBalances";
-import { fetchBalances } from "../../src/duneApi";
+import { useEvmTokenBalances } from "../../src/evm/useEvmTokenBalances";
+import { fetchEvmBalances } from "../../src/evm/duneApi";
import { vi } from "vitest";
// Mock the Dune API
-vi.mock("../../src/duneApi", () => ({
- fetchBalances: vi.fn(),
+vi.mock("../../src/evm/duneApi", () => ({
+ fetchEvmBalances: vi.fn(),
}));
-const mockFetchBalances = fetchBalances as jest.Mock;
+const mockFetchEvmBalances = fetchEvmBalances as jest.Mock;
// A wrapper for the hook that provides the required context
const wrapper = ({ children }: { children: React.ReactNode }) => (
@@ -25,7 +25,9 @@ describe("useTokenBalances", () => {
});
it("should return null if the wallet address is not a valid address", () => {
- const { result } = renderHook(() => useTokenBalances("0x123"), { wrapper });
+ const { result } = renderHook(() => useEvmTokenBalances("0x123"), {
+ wrapper,
+ });
expect(result.current).toEqual({
data: null,
@@ -54,9 +56,9 @@ describe("useTokenBalances", () => {
},
],
};
- mockFetchBalances.mockResolvedValueOnce(mockResponse);
+ mockFetchEvmBalances.mockResolvedValueOnce(mockResponse);
- const { result } = renderHook(() => useTokenBalances(walletAddress), {
+ const { result } = renderHook(() => useEvmTokenBalances(walletAddress), {
wrapper,
});
@@ -68,7 +70,7 @@ describe("useTokenBalances", () => {
expect(result.current.isLoading).toBe(false);
});
- expect(mockFetchBalances).toHaveBeenCalledWith(
+ expect(mockFetchEvmBalances).toHaveBeenCalledWith(
walletAddress,
{},
process.env.DUNE_API_KEY
@@ -82,9 +84,9 @@ describe("useTokenBalances", () => {
const walletAddress = "0x1234567890abcdef1234567890abcdef12345678";
const mockError = new Error("Failed to fetch token balances");
- mockFetchBalances.mockRejectedValueOnce(mockError);
+ mockFetchEvmBalances.mockRejectedValueOnce(mockError);
- const { result } = renderHook(() => useTokenBalances(walletAddress), {
+ const { result } = renderHook(() => useEvmTokenBalances(walletAddress), {
wrapper,
});
@@ -96,7 +98,7 @@ describe("useTokenBalances", () => {
expect(result.current.isLoading).toBe(false);
});
- expect(mockFetchBalances).toHaveBeenCalledWith(
+ expect(mockFetchEvmBalances).toHaveBeenCalledWith(
walletAddress,
{},
process.env.DUNE_API_KEY
@@ -113,11 +115,11 @@ describe("useTokenBalances", () => {
{children}
);
- const { result } = renderHook(() => useTokenBalances(walletAddress), {
+ const { result } = renderHook(() => useEvmTokenBalances(walletAddress), {
wrapper: localWrapper,
});
- expect(mockFetchBalances).not.toHaveBeenCalled();
+ expect(mockFetchEvmBalances).not.toHaveBeenCalled();
expect(result.current).toEqual({
data: null,
error: null,
@@ -126,9 +128,9 @@ describe("useTokenBalances", () => {
});
it("should not fetch data if the wallet address is missing", () => {
- const { result } = renderHook(() => useTokenBalances(""), { wrapper });
+ const { result } = renderHook(() => useEvmTokenBalances(""), { wrapper });
- expect(mockFetchBalances).not.toHaveBeenCalled();
+ expect(mockFetchEvmBalances).not.toHaveBeenCalled();
expect(result.current).toEqual({
data: null,
error: null,
diff --git a/__tests__/hooks/useTransactions.test.tsx b/__tests__/hooks/useEvmTransactions.test.tsx
similarity index 82%
rename from __tests__/hooks/useTransactions.test.tsx
rename to __tests__/hooks/useEvmTransactions.test.tsx
index 1286c02..43a2ab3 100644
--- a/__tests__/hooks/useTransactions.test.tsx
+++ b/__tests__/hooks/useEvmTransactions.test.tsx
@@ -1,16 +1,16 @@
import React from "react";
import { renderHook, act, waitFor } from "@testing-library/react";
import { DuneProvider } from "../../src/provider";
-import { useTransactions } from "../../src/useTransactions";
-import { fetchTransactions } from "../../src/duneApi";
+import { useEvmTransactions } from "../../src/evm/useEvmTransactions";
+import { fetchEvmTransactions } from "../../src/evm/duneApi";
import { vi } from "vitest";
// Mock the Dune API
-vi.mock("../../src/duneApi", () => ({
- fetchTransactions: vi.fn(),
+vi.mock("../../src/evm/duneApi", () => ({
+ fetchEvmTransactions: vi.fn(),
}));
-const mockFetchTransactions = fetchTransactions as jest.Mock;
+const mockFetchEvmTransactions = fetchEvmTransactions as jest.Mock;
// A wrapper for the hook that provides the required context
const wrapper = ({ children }: { children: React.ReactNode }) => (
@@ -25,7 +25,9 @@ describe("useTransactions", () => {
});
it("should return null if the wallet address is not a valid address", () => {
- const { result } = renderHook(() => useTransactions("0x123"), { wrapper });
+ const { result } = renderHook(() => useEvmTransactions("0x123"), {
+ wrapper,
+ });
expect(result.current).toEqual({
data: null,
@@ -72,9 +74,9 @@ describe("useTransactions", () => {
],
next_offset: "offset1",
};
- mockFetchTransactions.mockResolvedValueOnce(mockResponse);
+ mockFetchEvmTransactions.mockResolvedValueOnce(mockResponse);
- const { result } = renderHook(() => useTransactions(walletAddress), {
+ const { result } = renderHook(() => useEvmTransactions(walletAddress), {
wrapper,
});
@@ -86,7 +88,7 @@ describe("useTransactions", () => {
expect(result.current.isLoading).toBe(false);
});
- expect(mockFetchTransactions).toHaveBeenCalledWith(
+ expect(mockFetchEvmTransactions).toHaveBeenCalledWith(
walletAddress,
{ offset: undefined },
process.env.DUNE_API_KEY
@@ -100,9 +102,9 @@ describe("useTransactions", () => {
const walletAddress = "0x1234567890abcdef1234567890abcdef12345678";
const mockError = new Error("Failed to fetch transactions");
- mockFetchTransactions.mockRejectedValueOnce(mockError);
+ mockFetchEvmTransactions.mockRejectedValueOnce(mockError);
- const { result } = renderHook(() => useTransactions(walletAddress), {
+ const { result } = renderHook(() => useEvmTransactions(walletAddress), {
wrapper,
});
@@ -114,7 +116,7 @@ describe("useTransactions", () => {
expect(result.current.isLoading).toBe(false);
});
- expect(mockFetchTransactions).toHaveBeenCalledWith(
+ expect(mockFetchEvmTransactions).toHaveBeenCalledWith(
walletAddress,
{ offset: undefined },
process.env.DUNE_API_KEY
@@ -139,12 +141,12 @@ describe("useTransactions", () => {
next_offset: "offset2",
};
- mockFetchTransactions
+ mockFetchEvmTransactions
.mockResolvedValueOnce(page1Response)
.mockResolvedValueOnce(page2Response)
.mockResolvedValueOnce(page1Response);
- const { result } = renderHook(() => useTransactions(walletAddress), {
+ const { result } = renderHook(() => useEvmTransactions(walletAddress), {
wrapper,
});
@@ -190,11 +192,11 @@ describe("useTransactions", () => {
{children}
);
- const { result } = renderHook(() => useTransactions(walletAddress), {
+ const { result } = renderHook(() => useEvmTransactions(walletAddress), {
wrapper: localWrapper,
});
- expect(mockFetchTransactions).not.toHaveBeenCalled();
+ expect(mockFetchEvmTransactions).not.toHaveBeenCalled();
expect(result.current).toEqual({
data: null,
error: null,
@@ -208,9 +210,9 @@ describe("useTransactions", () => {
});
it("should not fetch data if the wallet address is missing", () => {
- const { result } = renderHook(() => useTransactions(""), { wrapper });
+ const { result } = renderHook(() => useEvmTransactions(""), { wrapper });
- expect(mockFetchTransactions).not.toHaveBeenCalled();
+ expect(mockFetchEvmTransactions).not.toHaveBeenCalled();
expect(result.current).toEqual({
data: null,
error: null,
diff --git a/__tests__/hooks/useSvmTokenBalances.test.tsx b/__tests__/hooks/useSvmTokenBalances.test.tsx
new file mode 100644
index 0000000..09ea00c
--- /dev/null
+++ b/__tests__/hooks/useSvmTokenBalances.test.tsx
@@ -0,0 +1,141 @@
+import React from "react";
+import { renderHook, waitFor } from "@testing-library/react";
+import { DuneProvider } from "../../src/provider";
+import { useSvmTokenBalances } from "../../src/svm/useSvmTokenBalances";
+import { fetchSvmBalances } from "../../src/svm/duneApi";
+import { vi } from "vitest";
+
+// Mock the Dune API
+vi.mock("../../src/svm/duneApi", () => ({
+ fetchSvmBalances: vi.fn(),
+}));
+
+const mockFetchSvmBalances = fetchSvmBalances as jest.Mock;
+
+// A wrapper for the hook that provides the required context
+const wrapper = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+describe("useTokenBalances", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("should fetch token balances successfully", async () => {
+ const walletAddress = "0x1234567890abcdef1234567890abcdef12345678";
+
+ const mockResponse = {
+ request_time: "2025-01-16T18:09:37.116ZZ",
+ response_time: "2025-01-16T18:09:37.156ZZ",
+ wallet_address: walletAddress,
+ balances: [
+ {
+ chain: "ethereum",
+ chain_id: 1,
+ address: "native",
+ amount: "121458493673814687",
+ symbol: "ETH",
+ decimals: 18,
+ price_usd: 3344.858473355283,
+ value_usd: 406.26147172582813,
+ },
+ ],
+ };
+
+ mockFetchSvmBalances.mockResolvedValueOnce(mockResponse);
+
+ const { result: svmResult } = renderHook(
+ () => useSvmTokenBalances(walletAddress),
+ {
+ wrapper,
+ }
+ );
+
+ // Initially, `isLoading` should be true
+ expect(svmResult.current.isLoading).toBe(true);
+
+ // Wait for the hook to update
+ await waitFor(() => {
+ expect(svmResult.current.isLoading).toBe(false);
+ });
+
+ expect(mockFetchSvmBalances).toHaveBeenCalledWith(
+ walletAddress,
+ {},
+ process.env.DUNE_API_KEY
+ );
+ expect(svmResult.current.isLoading).toBe(false);
+ expect(svmResult.current.error).toBeNull();
+ expect(svmResult.current.data).toEqual(mockResponse);
+ });
+
+ it("should handle errors when fetching token balances", async () => {
+ const walletAddress = "0x1234567890abcdef1234567890abcdef12345678";
+
+ const mockError = new Error("Failed to fetch token balances");
+
+ mockFetchSvmBalances.mockRejectedValueOnce(mockError);
+
+ const { result: svmResult } = renderHook(
+ () => useSvmTokenBalances(walletAddress),
+ {
+ wrapper,
+ }
+ );
+
+ // Initially, `isLoading` should be true
+ expect(svmResult.current.isLoading).toBe(true);
+
+ // Wait for the hook to update
+ await waitFor(() => {
+ expect(svmResult.current.isLoading).toBe(false);
+ });
+
+ expect(mockFetchSvmBalances).toHaveBeenCalledWith(
+ walletAddress,
+ {},
+ process.env.DUNE_API_KEY
+ );
+ expect(svmResult.current.isLoading).toBe(false);
+ expect(svmResult.current.error).toEqual(mockError);
+ expect(svmResult.current.data).toBeNull();
+ });
+
+ it("should not fetch data if the API key is missing", () => {
+ const walletAddress = "0x1234567890abcdef1234567890abcdef12345678";
+
+ const localWrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ );
+
+ const { result: svmResult } = renderHook(
+ () => useSvmTokenBalances(walletAddress),
+ {
+ wrapper: localWrapper,
+ }
+ );
+
+ expect(mockFetchSvmBalances).not.toHaveBeenCalled();
+ expect(svmResult.current).toEqual({
+ data: null,
+ error: null,
+ isLoading: false,
+ });
+ });
+
+ it("should not fetch data if the wallet address is missing", () => {
+ const { result: svmResult } = renderHook(() => useSvmTokenBalances(""), {
+ wrapper,
+ });
+
+ expect(mockFetchSvmBalances).not.toHaveBeenCalled();
+ expect(svmResult.current).toEqual({
+ data: null,
+ error: null,
+ isLoading: false,
+ });
+ });
+});
diff --git a/__tests__/hooks/useSvmTransactions.test.tsx b/__tests__/hooks/useSvmTransactions.test.tsx
new file mode 100644
index 0000000..143b4c2
--- /dev/null
+++ b/__tests__/hooks/useSvmTransactions.test.tsx
@@ -0,0 +1,225 @@
+import React from "react";
+import { renderHook, act, waitFor } from "@testing-library/react";
+import { DuneProvider } from "../../src/provider";
+import { useSvmTransactions } from "../../src/svm/useSvmTransactions";
+import { fetchSvmTransactions } from "../../src/svm/duneApi";
+import { vi } from "vitest";
+
+// Mock the Dune API
+vi.mock("../../src/svm/duneApi", () => ({
+ fetchSvmTransactions: vi.fn(),
+}));
+
+const mockFetchSvmTransactions = fetchSvmTransactions as jest.Mock;
+
+// A wrapper for the hook that provides the required context
+const wrapper = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+describe("useTransactions", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("should fetch transactions successfully", async () => {
+ const walletAddress = "0x1234567890abcdef1234567890abcdef12345678";
+
+ const mockResponse = {
+ rrequest_time: "2025-01-16T18:09:37.116ZZ",
+ response_time: "2025-01-16T18:09:37.156ZZ",
+ wallet_address: walletAddress,
+ transactions: [
+ {
+ chain: "ethereum",
+ chain_id: 1,
+ address: "0x1234567890abcdef1234567890abcdef12345678",
+ block_time: "2025-01-15T08:00:11+00:00",
+ block_number: 21628549,
+ index: 187,
+ hash: "0xda2c542c33c62fdf9f9a46e271e9b730086f6bf05f19e2a886079ae71a470a49",
+ block_hash:
+ "0x38e3851e2c21edf1d71c46ef2d28afdfc7c571b8c57b00cfe8f3d3c34ef62375",
+ value: "0x3b9aca00",
+ transaction_type: "Receiver",
+ from: "0xf0ee8fc751a28d4f8f2716e74cf5da46e55ff437",
+ to: "0x1234567890abcdef1234567890abcdef12345678",
+ nonce: "0x8",
+ gas_price: "0xc0dbeb5e",
+ gas_used: "0x5208",
+ effective_gas_price: "0xc0dbeb5e",
+ success: true,
+ data: "0x",
+ logs: [],
+ },
+ ],
+ next_offset: "offset1",
+ };
+
+ mockFetchSvmTransactions.mockResolvedValueOnce(mockResponse);
+
+ const { result: svmResult } = renderHook(
+ () => useSvmTransactions(walletAddress),
+ {
+ wrapper,
+ }
+ );
+
+ // Initially, `isLoading` should be true
+ expect(svmResult.current.isLoading).toBe(true);
+
+ // Wait for the hook to update
+ await waitFor(() => {
+ expect(svmResult.current.isLoading).toBe(false);
+ });
+
+ expect(mockFetchSvmTransactions).toHaveBeenCalledWith(
+ walletAddress,
+ { offset: undefined },
+ process.env.DUNE_API_KEY
+ );
+ expect(svmResult.current.data).toEqual(mockResponse);
+ expect(svmResult.current.nextOffset).toBe("offset1");
+ expect(svmResult.current.error).toBeNull();
+ });
+
+ it("should handle errors when fetching transactions", async () => {
+ const walletAddress = "0x1234567890abcdef1234567890abcdef12345678";
+
+ const mockError = new Error("Failed to fetch transactions");
+ mockFetchSvmTransactions.mockRejectedValueOnce(mockError);
+
+ const { result: svmResult } = renderHook(
+ () => useSvmTransactions(walletAddress),
+ {
+ wrapper,
+ }
+ );
+
+ // Initially, `isLoading` should be true
+ expect(svmResult.current.isLoading).toBe(true);
+
+ // Wait for the hook to update
+ await waitFor(() => {
+ expect(svmResult.current.isLoading).toBe(false);
+ });
+
+ expect(mockFetchSvmTransactions).toHaveBeenCalledWith(
+ walletAddress,
+ { offset: undefined },
+ process.env.DUNE_API_KEY
+ );
+ expect(svmResult.current.error).toEqual(mockError);
+ expect(svmResult.current.data).toBeNull();
+ });
+
+ it("should handle pagination: next and previous pages", async () => {
+ const walletAddress = "0x1234567890abcdef1234567890abcdef12345678";
+
+ const page1Response = {
+ transactions: [
+ { hash: "0x1", from: "0xabc", to: "0xdef", block_number: 123 },
+ ],
+ next_offset: "offset1",
+ };
+ const page2Response = {
+ transactions: [
+ { hash: "0x2", from: "0xghi", to: "0xjkl", block_number: 124 },
+ ],
+ next_offset: "offset2",
+ };
+
+ mockFetchSvmTransactions
+ .mockResolvedValueOnce(page1Response)
+ .mockResolvedValueOnce(page2Response)
+ .mockResolvedValueOnce(page1Response);
+
+ const { result: svmResult } = renderHook(
+ () => useSvmTransactions(walletAddress),
+ {
+ wrapper,
+ }
+ );
+
+ // Wait for the first page
+ await waitFor(() => {
+ expect(svmResult.current.isLoading).toBe(false);
+ });
+
+ expect(svmResult.current.data).toEqual(page1Response);
+ expect(svmResult.current.currentPage).toBe(0);
+
+ // Fetch the next page
+ act(() => {
+ svmResult.current.nextPage();
+ });
+
+ // Wait for the second page
+ await waitFor(() => {
+ expect(svmResult.current.isLoading).toBe(false);
+ });
+
+ expect(svmResult.current.data).toEqual(page2Response);
+ expect(svmResult.current.currentPage).toBe(1);
+
+ // Fetch the previous page
+ act(() => {
+ svmResult.current.previousPage();
+ });
+
+ // Wait for the first page again
+ await waitFor(() => {
+ expect(svmResult.current.isLoading).toBe(false);
+ });
+
+ expect(svmResult.current.data).toEqual(page1Response);
+ expect(svmResult.current.currentPage).toBe(0);
+ });
+
+ it("should not fetch data if the API key is missing", () => {
+ const walletAddress = "0x1234567890abcdef1234567890abcdef12345678";
+
+ const localWrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ );
+
+ const { result: svmResult } = renderHook(
+ () => useSvmTransactions(walletAddress),
+ {
+ wrapper: localWrapper,
+ }
+ );
+
+ expect(mockFetchSvmTransactions).not.toHaveBeenCalled();
+ expect(svmResult.current).toEqual({
+ data: null,
+ error: null,
+ isLoading: false,
+ nextOffset: null,
+ offsets: [],
+ currentPage: 0,
+ nextPage: expect.any(Function),
+ previousPage: expect.any(Function),
+ });
+ });
+
+ it("should not fetch data if the wallet address is missing", () => {
+ const { result: svmResult } = renderHook(() => useSvmTransactions(""), {
+ wrapper,
+ });
+
+ expect(mockFetchSvmTransactions).not.toHaveBeenCalled();
+ expect(svmResult.current).toEqual({
+ data: null,
+ error: null,
+ isLoading: false,
+ nextOffset: null,
+ offsets: [],
+ currentPage: 0,
+ nextPage: expect.any(Function),
+ previousPage: expect.any(Function),
+ });
+ });
+});
diff --git a/src/svm/useSvmTransactions.ts b/src/svm/useSvmTransactions.ts
index b1a7eba..33314c9 100644
--- a/src/svm/useSvmTransactions.ts
+++ b/src/svm/useSvmTransactions.ts
@@ -30,7 +30,7 @@ export const useSvmTransactions = (
// Function to fetch data for a specific page
const fetchDataAsync = async (offset: string | null) => {
- if (!walletAddress) return;
+ if (!apiKey || !walletAddress) return;
setState((prevState) => ({ ...prevState, isLoading: true }));