Skip to content
Open
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
1,384 changes: 1,377 additions & 7 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,21 @@
"react-dom": ">=18.0.0"
},
"dependencies": {
"@stellar/stellar-sdk": "^13.0.0",
"@stellar/freighter-api": "^2.0.0"
"@stellar/freighter-api": "^2.0.0",
"@stellar/stellar-sdk": "^13.0.0"
},
"devDependencies": {
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^7.0.2",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.57.0",
"jsdom": "^29.1.1",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-test-renderer": "^18.3.0",
"tsup": "^8.0.0",
"typescript": "^5.4.0",
"vitest": "^1.6.0"
Expand Down
83 changes: 2 additions & 81 deletions src/__tests__/useClaimableBalance.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { useFreighter } from "../hooks/useFreighter";

// ─── Mock React hooks ─────────────────────────────────────────────────────────

Expand Down Expand Up @@ -83,7 +82,7 @@ vi.mock("../hooks/useFreighter", () => ({

// ─── Import AFTER mocks ───────────────────────────────────────────────────────

import { useClaimableBalances, useClaimBalance } from "../hooks/useClaimableBalance";
import { useClaimBalance } from "../hooks/useClaimableBalance";
import { useReducer } from "react";

// ─── Helpers ──────────────────────────────────────────────────────────────────
Expand All @@ -102,17 +101,6 @@ function setupReducer(stateOverride = {}) {
] as unknown as ReturnType<typeof useReducer>);
}

const sampleRecord = {
id: "balance-id-1",
asset: "native",
amount: "100.0000000",
sponsor: "GSPONSOR",
last_modified_ledger: 123456,
claimants: [
{ destination: "GPUBLICKEY", predicate: { unconditional: true } },
],
};

// ─── Tests ────────────────────────────────────────────────────────────────────

describe("useClaimBalance", () => {
Expand Down Expand Up @@ -161,76 +149,9 @@ describe("useClaimBalance", () => {
});

it("throws when publicKey is null", async () => {
// Call the async function directly with publicKey set to null in closure
const claimFn = async (balanceId: string) => {
const publicKey: string | null = null;
if (!publicKey) {
throw new Error("Freighter is not connected. Call connect() first.");
await expect(() => { throw new Error("Freighter is not connected. Call connect() first."); }).toThrow("Freighter is not connected");
}
};

await expect(claimFn("balance-id-1")).rejects.toThrow(
"Freighter is not connected"
);
});
});

describe("useClaimBalance", () => {
beforeEach(() => {
vi.clearAllMocks();
setupReducer();
// Ensure Freighter is connected for every test in this block
vi.doMock("../hooks/useFreighter", () => ({
useFreighter: () => ({
publicKey: "GPUBLICKEY",
signTransaction: mockSignTransaction,
}),
}));
});

it("returns correct initial state", () => {
const hook = useClaimBalance();
expect(hook.status).toBe("idle");
expect(hook.hash).toBeNull();
expect(hook.error).toBeNull();
expect(hook.isLoading).toBe(false);
expect(hook.isSuccess).toBe(false);
expect(hook.isError).toBe(false);
expect(typeof hook.claim).toBe("function");
expect(typeof hook.reset).toBe("function");
});

it("builds, signs, and submits a claim transaction", async () => {
const hook = useClaimBalance();
await hook.claim("balance-id-1");

expect(mockSignTransaction).toHaveBeenCalledWith("built-xdr", {
networkPassphrase: "Test SDF Network ; September 2015",
});
expect(mockSubmitXdr).toHaveBeenCalledWith("signed-xdr");
});

it("calls claimClaimableBalance with the correct balanceId", async () => {
const { Operation } = await import("@stellar/stellar-sdk");
const hook = useClaimBalance();
await hook.claim("balance-id-abc");

expect(Operation.claimClaimableBalance).toHaveBeenCalledWith({
balanceId: "balance-id-abc",
});
});

it("throws when publicKey is null", async () => {
// Call the async function directly with publicKey set to null in closure
const claimFn = async (balanceId: string) => {
const publicKey: string | null = null;
if (!publicKey) {
throw new Error("Freighter is not connected. Call connect() first.");
}
};

await expect(claimFn("balance-id-1")).rejects.toThrow(
"Freighter is not connected"
);
});
});
129 changes: 129 additions & 0 deletions src/__tests__/useFreighter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { act, renderHook } from "@testing-library/react-hooks";
import { waitFor } from "@testing-library/react";

const mockIsConnected = vi.hoisted(() => vi.fn());
const mockGetPublicKey = vi.hoisted(() => vi.fn());
const mockGetNetwork = vi.hoisted(() => vi.fn());
const mockRequestAccess = vi.hoisted(() => vi.fn());
const mockSignTransaction = vi.hoisted(() => vi.fn());
const mockSignAuthEntry = vi.hoisted(() => vi.fn());
const mockSignBlob = vi.hoisted(() => vi.fn());

vi.mock("@stellar/freighter-api", () => ({
isConnected: mockIsConnected,
getPublicKey: mockGetPublicKey,
getNetwork: mockGetNetwork,
requestAccess: mockRequestAccess,
signTransaction: mockSignTransaction,
signAuthEntry: mockSignAuthEntry,
signBlob: mockSignBlob,
}));

import { useFreighter } from "../hooks/useFreighter";

describe("useFreighter", () => {

beforeEach(() => {
vi.clearAllMocks();
});

it("connects automatically when Freighter is already authorised", async () => {
mockIsConnected.mockResolvedValue(true);
mockGetPublicKey.mockResolvedValue("GABC123");
mockGetNetwork.mockResolvedValue("Test SDF Network ; September 2015");

const { result } = renderHook(() => useFreighter());

await waitFor(() => {
expect(result.current.isConnected).toBe(true);
});

expect(result.current.publicKey).toBe("GABC123");
expect(result.current.network).toBe("Test SDF Network ; September 2015");
expect(result.current.networkPassphrase).toBe("Test SDF Network ; September 2015");
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
});

it("marks Freighter as not installed when it is unavailable", async () => {
mockIsConnected.mockResolvedValue(false);

const { result } = renderHook(() => useFreighter());

await waitFor(() => {
expect(result.current.isInstalled).toBe(false);
});

expect(result.current.isConnected).toBe(false);
expect(result.current.publicKey).toBeNull();
expect(result.current.isLoading).toBe(false);
});

it("connect() requests access and updates state", async () => {
mockIsConnected.mockResolvedValue(false);
mockRequestAccess.mockResolvedValue(undefined);
mockGetPublicKey.mockResolvedValue("GNEW123");
mockGetNetwork.mockResolvedValue("Test SDF Network ; September 2015");

const { result } = renderHook(() => useFreighter());

await waitFor(() => {
expect(result.current.isInstalled).toBe(false);
});

await act(async () => {
await result.current.connect();
});

expect(mockRequestAccess).toHaveBeenCalled();
expect(result.current.isConnected).toBe(true);
expect(result.current.publicKey).toBe("GNEW123");
expect(result.current.networkPassphrase).toBe("Test SDF Network ; September 2015");
});

it("disconnect() resets the connection state", async () => {
mockIsConnected.mockResolvedValue(true);
mockGetPublicKey.mockResolvedValue("GABC123");
mockGetNetwork.mockResolvedValue("Test SDF Network ; September 2015");

const { result } = renderHook(() => useFreighter());
await waitFor(() => {
expect(result.current.isConnected).toBe(true);
});

act(() => {
result.current.disconnect();
});

expect(result.current.isConnected).toBe(false);
expect(result.current.publicKey).toBeNull();
expect(result.current.network).toBeNull();
});

it("signTransaction returns signed XDR and throws on error", async () => {
mockSignTransaction.mockResolvedValue("signed-xdr");
const { result } = renderHook(() => useFreighter());

await act(async () => {
const signed = await result.current.signTransaction("xdr", { networkPassphrase: "Test Passphrase" });
expect(signed).toBe("signed-xdr");
});

mockSignTransaction.mockRejectedValueOnce(new Error("failure"));
await expect(result.current.signTransaction("xdr")).rejects.toThrow("failure");
});

it("signAuthEntry and signBlob forward to Freighter", async () => {
mockSignAuthEntry.mockResolvedValue("signed-auth");
mockSignBlob.mockResolvedValue("signed-blob");
const { result } = renderHook(() => useFreighter());

await act(async () => {
const auth = await result.current.signAuthEntry("entry-xdr");
const blob = await result.current.signBlob("blob", { accountToSign: "GABC" });
expect(auth).toBe("signed-auth");
expect(blob).toBe("signed-blob");
});
});
});
86 changes: 86 additions & 0 deletions src/__tests__/useStellarAccount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { act, renderHook } from "@testing-library/react-hooks";
import { waitFor } from "@testing-library/react";
const mockLoadAccount = vi.fn();

vi.mock("../context", () => ({
useStellarContext: () => ({
config: {
horizonUrl: "https://horizon-testnet.stellar.org",
networkPassphrase: "Test SDF Network ; September 2015",
},
}),
}));

vi.mock("@stellar/stellar-sdk", () => ({
Horizon: {
Server: vi.fn(() => ({
loadAccount: mockLoadAccount,
})),
},
}));

import { useStellarAccount } from "../hooks/useStellarAccount";

const mockRawAccount = {
account_id: "GABC123",
sequence: "1234567890",
subentry_count: 0,
thresholds: { low_threshold: 0, med_threshold: 0, high_threshold: 0 },
flags: {
auth_required: false,
auth_revocable: false,
auth_immutable: false,
auth_clawback_enabled: false,
},
balances: [
{
asset_type: "native",
balance: "42.0000000",
buying_liabilities: "0.0000000",
selling_liabilities: "0.0000000",
},
],
};

describe("useStellarAccount", () => {
beforeEach(() => {
vi.clearAllMocks();
mockLoadAccount.mockResolvedValue(mockRawAccount);
});

it("fetches account data on mount and exposes parsed result", async () => {
const { result } = renderHook(() => useStellarAccount("GABC123"));

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(mockLoadAccount).toHaveBeenCalledWith("GABC123");
expect(result.current.data?.accountId).toBe("GABC123");
expect(result.current.data?.balances).toHaveLength(1);
expect(result.current.error).toBeNull();
});

it("returns reset state when publicKey is null", () => {
const { result } = renderHook(() => useStellarAccount(null));

expect(result.current.data).toBeNull();
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
});

it("calls refetch again when refetch() is invoked", async () => {
const { result } = renderHook(() => useStellarAccount("GABC123"));

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

await act(async () => {
await result.current.refetch();
});

expect(mockLoadAccount).toHaveBeenCalledTimes(2);
});
});
Loading