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
2 changes: 2 additions & 0 deletions .github/workflows/vscode-extension-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ on:
- main
paths:
- "vscode-extension/**"
- ".devcontainer/**"
- ".github/workflows/vscode-extension-ci.yml"
pull_request:
branches:
- main
paths:
- "vscode-extension/**"
- ".devcontainer/**"

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
Expand Down
9 changes: 9 additions & 0 deletions DOCUMENTATION_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@
- Security best practices and monitoring
- Integration examples for contract developers

### Live Testnet Status Widget

**[LIVE_TESTNET.md](LIVE_TESTNET.md)** — deployed contract addresses and on-chain verification
- Three live Soroban Testnet contracts with real-time status
- Live widget available at `/` (home page) via the `TestnetStatusWidget` component
([frontend/app/components/TestnetStatusWidget.tsx](frontend/app/components/TestnetStatusWidget.tsx))
- API route: `GET /api/testnet-status` — polls Soroban RPC + Stellar Expert, cached 30 s
- See also: [docs/soroban-deployment.md](docs/soroban-deployment.md) for re-deployment instructions

### Runtime Guard Wrapper Contract

**[contracts/runtime-guard-wrapper/README.md](contracts/runtime-guard-wrapper/README.md)**
Expand Down
131 changes: 131 additions & 0 deletions frontend/app/api/testnet-status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { NextResponse } from "next/server";

const SOROBAN_RPC = "https://soroban-testnet.stellar.org";
const STELLAR_EXPERT_BASE =
"https://api.stellar.expert/explorer/testnet/contract";

const CONTRACTS = [
{
id: "runtime-guard-wrapper",
label: "Runtime Guard Wrapper",
address: "CBLDEREKXK6AIZ7ZSKC6VYCK4MKF4FZ4ANJEU67QZAQUG57I4KGZMTXB",
},
{
id: "vulnerable-contract",
label: "Vulnerable Contract (demo)",
address: "CABBT5FKG7AE7IEEA4KR2J5AVYRSZAWKTXZ2KFX3UNJQAMMLMCXNLMIB",
},
{
id: "reentrancy-guard",
label: "Reentrancy Guard",
address: "CDDVM5A5IVDAG5FZ2OU2CLWAHC7A2T7LHQHZSDVKZPE6SDMDO2JCR3UY",
},
] as const;

export type ContractStatus = {
id: string;
label: string;
address: string;
alive: boolean;
explorerUrl: string;
errorMessage?: string;
};

export type TestnetStatusResponse = {
networkHealthy: boolean;
ledger?: number;
contracts: ContractStatus[];
fetchedAt: string;
};

async function getRpcHealth(): Promise<{ healthy: boolean; ledger?: number }> {
const body = JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "getLatestLedger",
params: {},
});

const res = await fetch(SOROBAN_RPC, {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
signal: AbortSignal.timeout(8_000),
});

if (!res.ok) return { healthy: false };

const json = (await res.json()) as {
result?: { sequence?: number };
error?: unknown;
};

if (json.error || !json.result) return { healthy: false };
return { healthy: true, ledger: json.result.sequence };
}

async function getContractInfo(
address: string,
): Promise<{ alive: boolean; errorMessage?: string }> {
const url = `${STELLAR_EXPERT_BASE}/${address}`;
try {
const res = await fetch(url, {
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(8_000),
});
if (res.status === 404) {
return { alive: false, errorMessage: "Contract not found on testnet" };
}
if (!res.ok) {
return {
alive: false,
errorMessage: `HTTP ${res.status} from Stellar Expert`,
};
}
const json = (await res.json()) as { id?: string };
return { alive: Boolean(json.id) };
} catch (err) {
return {
alive: false,
errorMessage: err instanceof Error ? err.message : "Fetch error",
};
}
}

export async function GET(): Promise<NextResponse<TestnetStatusResponse>> {
const [networkResult, ...contractResults] = await Promise.allSettled([
getRpcHealth(),
...CONTRACTS.map((c) => getContractInfo(c.address)),
]);

const { healthy: networkHealthy, ledger } =
networkResult.status === "fulfilled"
? networkResult.value
: { healthy: false, ledger: undefined };

const contracts: ContractStatus[] = CONTRACTS.map((c, i) => {
const result = contractResults[i];
const { alive, errorMessage } =
result?.status === "fulfilled"
? result.value
: { alive: false, errorMessage: "Request failed" };

return {
id: c.id,
label: c.label,
address: c.address,
alive,
explorerUrl: `https://stellar.expert/explorer/testnet/contract/${c.address}`,
...(errorMessage ? { errorMessage } : {}),
};
});

return NextResponse.json(
{ networkHealthy, ledger, contracts, fetchedAt: new Date().toISOString() },
{
headers: {
"Cache-Control": "public, s-maxage=30, stale-while-revalidate=60",
},
},
);
}
215 changes: 215 additions & 0 deletions frontend/app/components/TestnetStatusWidget.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { TestnetStatusWidget } from "./TestnetStatusWidget";
import type { TestnetStatusResponse } from "../api/testnet-status/route";

const LIVE_RESPONSE: TestnetStatusResponse = {
networkHealthy: true,
ledger: 12345678,
contracts: [
{
id: "runtime-guard-wrapper",
label: "Runtime Guard Wrapper",
address: "CBLDEREKXK6AIZ7ZSKC6VYCK4MKF4FZ4ANJEU67QZAQUG57I4KGZMTXB",
alive: true,
explorerUrl:
"https://stellar.expert/explorer/testnet/contract/CBLDEREKXK6AIZ7ZSKC6VYCK4MKF4FZ4ANJEU67QZAQUG57I4KGZMTXB",
},
{
id: "vulnerable-contract",
label: "Vulnerable Contract (demo)",
address: "CABBT5FKG7AE7IEEA4KR2J5AVYRSZAWKTXZ2KFX3UNJQAMMLMCXNLMIB",
alive: true,
explorerUrl:
"https://stellar.expert/explorer/testnet/contract/CABBT5FKG7AE7IEEA4KR2J5AVYRSZAWKTXZ2KFX3UNJQAMMLMCXNLMIB",
},
{
id: "reentrancy-guard",
label: "Reentrancy Guard",
address: "CDDVM5A5IVDAG5FZ2OU2CLWAHC7A2T7LHQHZSDVKZPE6SDMDO2JCR3UY",
alive: true,
explorerUrl:
"https://stellar.expert/explorer/testnet/contract/CDDVM5A5IVDAG5FZ2OU2CLWAHC7A2T7LHQHZSDVKZPE6SDMDO2JCR3UY",
},
],
fetchedAt: "2026-06-26T00:00:00.000Z",
};

function mockFetch(response: TestnetStatusResponse | null, status = 200) {
return vi.spyOn(global, "fetch").mockResolvedValue({
ok: status >= 200 && status < 300,
status,
json: async () => response,
} as Response);
}

afterEach(() => {
vi.restoreAllMocks();
});

describe("TestnetStatusWidget – loading state", () => {
it("renders a loading indicator before data arrives", () => {
vi.spyOn(global, "fetch").mockReturnValue(new Promise(() => {}));
render(<TestnetStatusWidget />);
expect(screen.getByRole("status")).toBeInTheDocument();
});

it("loading indicator has an accessible label", () => {
vi.spyOn(global, "fetch").mockReturnValue(new Promise(() => {}));
render(<TestnetStatusWidget />);
expect(screen.getByLabelText(/loading testnet status/i)).toBeInTheDocument();
});
});

describe("TestnetStatusWidget – success state", () => {
it("renders all three contract labels after successful fetch", async () => {
mockFetch(LIVE_RESPONSE);
render(<TestnetStatusWidget />);
await waitFor(() => {
expect(screen.getByText("Runtime Guard Wrapper")).toBeInTheDocument();
});
expect(screen.getByText("Vulnerable Contract (demo)")).toBeInTheDocument();
expect(screen.getByText("Reentrancy Guard")).toBeInTheDocument();
});

it("shows Live badge for each online contract", async () => {
mockFetch(LIVE_RESPONSE);
render(<TestnetStatusWidget />);
await waitFor(() => {
const badges = screen.getAllByText("Live");
expect(badges).toHaveLength(3);
});
});

it("renders the current ledger number", async () => {
mockFetch(LIVE_RESPONSE);
render(<TestnetStatusWidget />);
await waitFor(() => {
expect(screen.getByText(/12,345,678/)).toBeInTheDocument();
});
});

it("renders explorer links for each contract", async () => {
mockFetch(LIVE_RESPONSE);
render(<TestnetStatusWidget />);
await waitFor(() => {
const links = screen.getAllByRole("link", { name: /view .* on stellar expert/i });
expect(links).toHaveLength(3);
});
});

it("explorer links point to stellar.expert testnet", async () => {
mockFetch(LIVE_RESPONSE);
render(<TestnetStatusWidget />);
await waitFor(() => {
const links = screen.getAllByRole("link", { name: /view .* on stellar expert/i });
links.forEach((link) => {
expect(link).toHaveAttribute(
"href",
expect.stringContaining("stellar.expert/explorer/testnet"),
);
});
});
});

it("shows last-updated time after fetch completes", async () => {
mockFetch(LIVE_RESPONSE);
render(<TestnetStatusWidget />);
await waitFor(() => {
expect(screen.getByText(/last updated/i)).toBeInTheDocument();
});
});
});

describe("TestnetStatusWidget – offline contract", () => {
it("shows Offline badge for a contract that is down", async () => {
const offlineResponse: TestnetStatusResponse = {
...LIVE_RESPONSE,
contracts: [
{ ...LIVE_RESPONSE.contracts[0], alive: false, errorMessage: "Contract not found" },
...LIVE_RESPONSE.contracts.slice(1),
],
};
mockFetch(offlineResponse);
render(<TestnetStatusWidget />);
await waitFor(() => {
expect(screen.getByText("Offline")).toBeInTheDocument();
});
});

it("renders the error message returned by the API", async () => {
const offlineResponse: TestnetStatusResponse = {
...LIVE_RESPONSE,
contracts: [
{
...LIVE_RESPONSE.contracts[0],
alive: false,
errorMessage: "Contract not found on testnet",
},
...LIVE_RESPONSE.contracts.slice(1),
],
};
mockFetch(offlineResponse);
render(<TestnetStatusWidget />);
await waitFor(() => {
expect(screen.getByText("Contract not found on testnet")).toBeInTheDocument();
});
});
});

describe("TestnetStatusWidget – network error", () => {
it("shows an error message when the API call fails", async () => {
vi.spyOn(global, "fetch").mockRejectedValue(new Error("Network error"));
render(<TestnetStatusWidget />);
await waitFor(() => {
expect(screen.getByRole("alert")).toBeInTheDocument();
});
});

it("shows the error message text", async () => {
vi.spyOn(global, "fetch").mockRejectedValue(new Error("Network error"));
render(<TestnetStatusWidget />);
await waitFor(() => {
expect(screen.getByText(/network error/i)).toBeInTheDocument();
});
});

it("shows an error when the API returns HTTP 500", async () => {
mockFetch(null, 500);
render(<TestnetStatusWidget />);
await waitFor(() => {
expect(screen.getByRole("alert")).toBeInTheDocument();
});
});
});

describe("TestnetStatusWidget – refresh button", () => {
it("calls fetch again when Refresh is clicked", async () => {
const spy = mockFetch(LIVE_RESPONSE);
render(<TestnetStatusWidget />);
await waitFor(() => screen.getByText(/refresh/i));

await userEvent.click(screen.getByRole("button", { name: /refresh/i }));
expect(spy).toHaveBeenCalledTimes(2);
});
});

describe("TestnetStatusWidget – accessibility", () => {
it("section has an accessible name", () => {
vi.spyOn(global, "fetch").mockReturnValue(new Promise(() => {}));
render(<TestnetStatusWidget />);
expect(
screen.getByRole("region", { name: /testnet contract status/i }),
).toBeInTheDocument();
});

it("online status dots have aria-label Online", async () => {
mockFetch(LIVE_RESPONSE);
render(<TestnetStatusWidget />);
await waitFor(() => {
const dots = screen.getAllByLabelText("Online");
expect(dots.length).toBeGreaterThan(0);
});
});
});
Loading
Loading