diff --git a/features/path-payment-slippage-calculator/README.md b/features/path-payment-slippage-calculator/README.md
new file mode 100644
index 00000000..89876b20
--- /dev/null
+++ b/features/path-payment-slippage-calculator/README.md
@@ -0,0 +1,7 @@
+# Path Payment Slippage Calculator
+
+Loads the operations for one settled transaction and audits its path payment against the bound committed in the operation.
+
+Strict-send compares actual destination amount with the minimum destination bound. Strict-receive compares actual source amount with the maximum send bound. Decimal amounts are converted to seven-decimal integer units and the percentage is computed as basis points with `BigInt`; floating point is never used.
+
+The selected network is part of the result. The tool only reads public Horizon data, never signs or submits transactions and never accepts a secret key.
diff --git a/features/path-payment-slippage-calculator/__tests__/PathPaymentSlippageCalculatorPanel.test.tsx b/features/path-payment-slippage-calculator/__tests__/PathPaymentSlippageCalculatorPanel.test.tsx
new file mode 100644
index 00000000..339c5c1b
--- /dev/null
+++ b/features/path-payment-slippage-calculator/__tests__/PathPaymentSlippageCalculatorPanel.test.tsx
@@ -0,0 +1,17 @@
+import { describe, expect, it } from "vitest";
+import { renderFeature, screen } from "@/core/testing/render";
+import { PathPaymentSlippageCalculatorPanel } from "@/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorPanel";
+import { copy } from "@/features/path-payment-slippage-calculator/copy";
+
+describe("PathPaymentSlippageCalculatorPanel", () => {
+ it("renders the empty state before any input", () => {
+ renderFeature();
+ expect(screen.getByText(copy.emptyTitle)).toBeInTheDocument();
+ });
+
+ it("shows a validation error when submitted empty", async () => {
+ const { user } = renderFeature();
+ await user.click(screen.getByRole("button", { name: copy.submit }));
+ expect(await screen.findByRole("alert")).toBeInTheDocument();
+ });
+});
diff --git a/features/path-payment-slippage-calculator/__tests__/a11y.test.tsx b/features/path-payment-slippage-calculator/__tests__/a11y.test.tsx
new file mode 100644
index 00000000..c720eaf5
--- /dev/null
+++ b/features/path-payment-slippage-calculator/__tests__/a11y.test.tsx
@@ -0,0 +1,18 @@
+import { describe, it } from "vitest";
+import { renderFeature, screen } from "@/core/testing/render";
+import { expectNoAxeViolations } from "@/core/testing/axe";
+import { PathPaymentSlippageCalculatorPanel } from "@/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorPanel";
+import { copy } from "@/features/path-payment-slippage-calculator/copy";
+
+describe("PathPaymentSlippageCalculatorPanel accessibility", () => {
+ it("has no WCAG A/AA violations in its initial state", async () => {
+ const { container } = renderFeature();
+ await expectNoAxeViolations(container);
+ });
+ it("has no WCAG A/AA violations after validation", async () => {
+ const { container, user } = renderFeature();
+ await user.click(screen.getByRole("button", { name: copy.submit }));
+ await screen.findByRole("alert");
+ await expectNoAxeViolations(container);
+ });
+});
diff --git a/features/path-payment-slippage-calculator/__tests__/declared-bound.test.ts b/features/path-payment-slippage-calculator/__tests__/declared-bound.test.ts
new file mode 100644
index 00000000..b4236c09
--- /dev/null
+++ b/features/path-payment-slippage-calculator/__tests__/declared-bound.test.ts
@@ -0,0 +1,7 @@
+import { describe, expect, it } from "vitest";
+import { declaredBound } from "@/features/path-payment-slippage-calculator/lib/declared-bound";
+import { extractPathPayment } from "@/features/path-payment-slippage-calculator/lib/path-payment-extract";
+import { strictReceiveRecord } from "@/features/path-payment-slippage-calculator/fixtures/strict-receive-payment.fixture";
+describe("declaredBound", () => {
+ it("labels strict-receive as a maximum-send bound", () => { const record = extractPathPayment([strictReceiveRecord]); expect(record.ok && declaredBound(record.value)).toEqual({ ok: true, value: { direction: "maximum-send", amount: "12.0000000" } }); });
+});
diff --git a/features/path-payment-slippage-calculator/__tests__/format.test.ts b/features/path-payment-slippage-calculator/__tests__/format.test.ts
new file mode 100644
index 00000000..02c394bc
--- /dev/null
+++ b/features/path-payment-slippage-calculator/__tests__/format.test.ts
@@ -0,0 +1,6 @@
+import { describe, expect, it } from "vitest";
+import { formatAsset, formatBasisPoints } from "@/features/path-payment-slippage-calculator/lib/format";
+describe("slippage formatting", () => {
+ it("formats signed basis points", () => expect(formatBasisPoints("-526")).toBe("-5.26%"));
+ it("formats native assets", () => expect(formatAsset({ type: "native", code: "XLM", issuer: null })).toBe("XLM"));
+});
diff --git a/features/path-payment-slippage-calculator/__tests__/path-payment-extract.test.ts b/features/path-payment-slippage-calculator/__tests__/path-payment-extract.test.ts
new file mode 100644
index 00000000..f38086c2
--- /dev/null
+++ b/features/path-payment-slippage-calculator/__tests__/path-payment-extract.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, it } from "vitest";
+import { extractPathPayment } from "@/features/path-payment-slippage-calculator/lib/path-payment-extract";
+import { strictReceiveRecord } from "@/features/path-payment-slippage-calculator/fixtures/strict-receive-payment.fixture";
+import { strictSendRecord } from "@/features/path-payment-slippage-calculator/fixtures/strict-send-payment.fixture";
+describe("extractPathPayment", () => {
+ it("extracts strict-send bounds and path", () => { const result = extractPathPayment([strictSendRecord]); expect(result.ok && result.value.variant).toBe("strict-send"); expect(result.ok && result.value.path).toHaveLength(1); });
+ it("distinguishes a transaction with no path payment", () => expect(extractPathPayment([{ type: "payment" }])).toEqual({ ok: false, code: "no_path_payment_operation" }));
+ it("reads Horizon destination_min and destination asset fields", () => {
+ const result = extractPathPayment([strictSendRecord]);
+ expect(result.ok && result.value.declaredBound).toBe("9.5000000");
+ expect(result.ok && result.value.actualAmount).toBe("10.0000000");
+ expect(result.ok && result.value.boundAsset).toEqual({
+ type: "credit_alphanum4", code: "USDC", issuer: strictSendRecord.asset_issuer,
+ });
+ });
+ it("reads Horizon source_max and the executed source amount", () => {
+ const result = extractPathPayment([strictReceiveRecord]);
+ expect(result.ok && result.value.variant).toBe("strict-receive");
+ expect(result.ok && result.value.declaredBound).toBe("12.0000000");
+ expect(result.ok && result.value.actualAmount).toBe("11.4000000");
+ expect(result.ok && result.value.boundAsset).toEqual({ type: "native", code: "XLM", issuer: null });
+ });
+
+});
diff --git a/features/path-payment-slippage-calculator/__tests__/pathPaymentSlippageCalculator.test.ts b/features/path-payment-slippage-calculator/__tests__/pathPaymentSlippageCalculator.test.ts
new file mode 100644
index 00000000..37b214ae
--- /dev/null
+++ b/features/path-payment-slippage-calculator/__tests__/pathPaymentSlippageCalculator.test.ts
@@ -0,0 +1,6 @@
+import { describe, expect, it } from "vitest";
+import { statusToPathPaymentError } from "@/features/path-payment-slippage-calculator/lib/pathPaymentSlippageCalculator.errors";
+describe("path payment request outcomes", () => {
+ it("maps not found distinctly", () => expect(statusToPathPaymentError(404)).toBe("transaction_not_found"));
+ it("maps rate limits distinctly", () => expect(statusToPathPaymentError(429)).toBe("rate_limited"));
+});
diff --git a/features/path-payment-slippage-calculator/__tests__/schema.test.ts b/features/path-payment-slippage-calculator/__tests__/schema.test.ts
new file mode 100644
index 00000000..a54950f7
--- /dev/null
+++ b/features/path-payment-slippage-calculator/__tests__/schema.test.ts
@@ -0,0 +1,6 @@
+import { describe, expect, it } from "vitest";
+import { parsePathPaymentSlippageCalculatorInput } from "@/features/path-payment-slippage-calculator/schema";
+describe("parsePathPaymentSlippageCalculatorInput", () => {
+ it("accepts and normalizes a transaction hash", () => expect(parsePathPaymentSlippageCalculatorInput(" " + "AB".repeat(32) + " ")).toEqual({ ok: true, value: { hash: "ab".repeat(32) } }));
+ it("rejects malformed hashes", () => expect(parsePathPaymentSlippageCalculatorInput("123")).toEqual({ ok: false, code: "invalid_hash" }));
+});
diff --git a/features/path-payment-slippage-calculator/__tests__/slippage-math.test.ts b/features/path-payment-slippage-calculator/__tests__/slippage-math.test.ts
new file mode 100644
index 00000000..3565f9ee
--- /dev/null
+++ b/features/path-payment-slippage-calculator/__tests__/slippage-math.test.ts
@@ -0,0 +1,7 @@
+import { describe, expect, it } from "vitest";
+import { calculateSlippageBasisPoints } from "@/features/path-payment-slippage-calculator/lib/slippage-math";
+describe("calculateSlippageBasisPoints", () => {
+ it("measures strict-send above the minimum", () => expect(calculateSlippageBasisPoints("strict-send", "9.5000000", "10.0000000")).toEqual({ ok: true, value: "526" }));
+ it("measures strict-receive below the maximum", () => expect(calculateSlippageBasisPoints("strict-receive", "12.0000000", "11.4000000")).toEqual({ ok: true, value: "500" }));
+ it("rejects malformed amounts", () => expect(calculateSlippageBasisPoints("strict-send", "0", "1")).toEqual({ ok: false, code: "malformed_effect" }));
+});
diff --git a/features/path-payment-slippage-calculator/__tests__/usePathPaymentSlippageCalculator.test.tsx b/features/path-payment-slippage-calculator/__tests__/usePathPaymentSlippageCalculator.test.tsx
new file mode 100644
index 00000000..cec327a1
--- /dev/null
+++ b/features/path-payment-slippage-calculator/__tests__/usePathPaymentSlippageCalculator.test.tsx
@@ -0,0 +1,24 @@
+import { describe, expect, it } from "vitest";
+import { renderHook, act, waitFor } from "@testing-library/react";
+import { NetworkProvider } from "@/core/network/NetworkProvider";
+import { usePathPaymentSlippageCalculator } from "@/features/path-payment-slippage-calculator/hooks/usePathPaymentSlippageCalculator";
+
+function wrapper({ children }: { children: React.ReactNode }) {
+ return {children};
+}
+
+describe("usePathPaymentSlippageCalculator", () => {
+ it("starts idle", () => {
+ const { result } = renderHook(() => usePathPaymentSlippageCalculator(), { wrapper });
+ expect(result.current.state.status).toBe("idle");
+ });
+
+ it("reports invalid_hash for empty input", async () => {
+ const { result } = renderHook(() => usePathPaymentSlippageCalculator(), { wrapper });
+ await act(async () => {
+ await result.current.submit("");
+ });
+ await waitFor(() => expect(result.current.state.status).toBe("error"));
+ expect(result.current.state).toEqual({ status: "error", code: "invalid_hash" });
+ });
+});
diff --git a/features/path-payment-slippage-calculator/components/DeclaredBoundNotice.tsx b/features/path-payment-slippage-calculator/components/DeclaredBoundNotice.tsx
new file mode 100644
index 00000000..902e9643
--- /dev/null
+++ b/features/path-payment-slippage-calculator/components/DeclaredBoundNotice.tsx
@@ -0,0 +1,4 @@
+import { copy, variantCopy } from "@/features/path-payment-slippage-calculator/copy";
+import { formatAsset } from "@/features/path-payment-slippage-calculator/lib/format";
+import type { AssetIdentity, PathVariant } from "@/features/path-payment-slippage-calculator/types";
+export function DeclaredBoundNotice({ variant, amount, asset }: { variant: PathVariant; amount: string; asset: AssetIdentity }) { return
{copy.declaredBound}: {amount} {formatAsset(asset)} — {variantCopy[variant]}
; }
diff --git a/features/path-payment-slippage-calculator/components/ExecutedPathRows.tsx b/features/path-payment-slippage-calculator/components/ExecutedPathRows.tsx
new file mode 100644
index 00000000..f0bc36a3
--- /dev/null
+++ b/features/path-payment-slippage-calculator/components/ExecutedPathRows.tsx
@@ -0,0 +1,4 @@
+import { copy } from "@/features/path-payment-slippage-calculator/copy";
+import { formatAsset } from "@/features/path-payment-slippage-calculator/lib/format";
+import type { AssetIdentity } from "@/features/path-payment-slippage-calculator/types";
+export function ExecutedPathRows({ path }: { path: AssetIdentity[] }) { return {copy.pathTitle}
{path.length ? {path.map((asset, index) => - {index + 1}. {formatAsset(asset)}
)}
: {copy.directPath}
}; }
diff --git a/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorEmptyState.tsx b/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorEmptyState.tsx
new file mode 100644
index 00000000..43682ebf
--- /dev/null
+++ b/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorEmptyState.tsx
@@ -0,0 +1,9 @@
+import { Sparkles } from "lucide-react";
+import { EmptyState } from "@/core/ui/EmptyState";
+import { copy } from "@/features/path-payment-slippage-calculator/copy";
+
+export function PathPaymentSlippageCalculatorEmptyState() {
+ return (
+
+ );
+}
diff --git a/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorForm.tsx b/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorForm.tsx
new file mode 100644
index 00000000..b7d8ed37
--- /dev/null
+++ b/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorForm.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import { useState, type FormEvent } from "react";
+import { Button } from "@/core/ui/Button";
+import { Field } from "@/core/ui/Field";
+import { Input } from "@/core/ui/Input";
+import { copy } from "@/features/path-payment-slippage-calculator/copy";
+
+export function PathPaymentSlippageCalculatorForm({
+ onSubmit,
+ pending
+}: {
+ onSubmit: (value: string) => void;
+ pending: boolean;
+}) {
+ const [value, setValue] = useState("");
+
+ function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+ onSubmit(value);
+ }
+
+ return (
+
+ );
+}
diff --git a/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorPanel.tsx b/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorPanel.tsx
new file mode 100644
index 00000000..137bb1bf
--- /dev/null
+++ b/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorPanel.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { Card } from "@/core/ui/Card";
+import { StatusMessage } from "@/core/ui/StatusMessage";
+import { usePathPaymentSlippageCalculator } from "@/features/path-payment-slippage-calculator/hooks/usePathPaymentSlippageCalculator";
+import { errorCopy } from "@/features/path-payment-slippage-calculator/copy";
+import { PathPaymentSlippageCalculatorForm } from "@/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorForm";
+import { PathPaymentSlippageCalculatorResult } from "@/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorResult";
+import { PathPaymentSlippageCalculatorEmptyState } from "@/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorEmptyState";
+
+export function PathPaymentSlippageCalculatorPanel() {
+ const { state, submit } = usePathPaymentSlippageCalculator();
+
+ return (
+
+
+
+
+
+ {state.status === "error" ? (
+
+ ) : null}
+
+ {state.status === "success" ?
: null}
+
+ {state.status === "idle" ?
: null}
+
+ );
+}
diff --git a/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorResult.tsx b/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorResult.tsx
new file mode 100644
index 00000000..181e4a58
--- /dev/null
+++ b/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorResult.tsx
@@ -0,0 +1,7 @@
+import { Card, CardHeader, CardTitle } from "@/core/ui/Card";
+import { copy } from "@/features/path-payment-slippage-calculator/copy";
+import { DeclaredBoundNotice } from "@/features/path-payment-slippage-calculator/components/DeclaredBoundNotice";
+import { ExecutedPathRows } from "@/features/path-payment-slippage-calculator/components/ExecutedPathRows";
+import { SlippageResult } from "@/features/path-payment-slippage-calculator/components/SlippageResult";
+import type { PathPaymentSlippageCalculatorResult as Result } from "@/features/path-payment-slippage-calculator/types";
+export function PathPaymentSlippageCalculatorResult({ result }: { result: Result }) { return {copy.resultTitle}{copy.actualAmount}: {result.actualAmount}
; }
diff --git a/features/path-payment-slippage-calculator/components/SlippageResult.tsx b/features/path-payment-slippage-calculator/components/SlippageResult.tsx
new file mode 100644
index 00000000..4a238cb7
--- /dev/null
+++ b/features/path-payment-slippage-calculator/components/SlippageResult.tsx
@@ -0,0 +1,3 @@
+import { copy } from "@/features/path-payment-slippage-calculator/copy";
+import { formatBasisPoints } from "@/features/path-payment-slippage-calculator/lib/format";
+export function SlippageResult({ basisPoints }: { basisPoints: string }) { return {copy.resultTitle}: {formatBasisPoints(basisPoints)}
; }
diff --git a/features/path-payment-slippage-calculator/copy.ts b/features/path-payment-slippage-calculator/copy.ts
new file mode 100644
index 00000000..e7900ba7
--- /dev/null
+++ b/features/path-payment-slippage-calculator/copy.ts
@@ -0,0 +1,20 @@
+import type { PathPaymentSlippageCalculatorErrorCode, PathVariant } from "@/features/path-payment-slippage-calculator/types";
+export const copy = {
+ formLabel: "Settled transaction hash", formHint: "Enter the 64-character hexadecimal hash.",
+ submit: "Calculate slippage", working: "Loading execution…", emptyTitle: "No path payment inspected",
+ emptyDescription: "Load one settled transaction to compare its executed amount with the declared bound.",
+ resultTitle: "Realized slippage", declaredBound: "Declared bound", actualAmount: "Actual executed amount",
+ pathTitle: "Executed path", directPath: "Direct conversion; no intermediate assets were reported."
+} as const;
+export const variantCopy: Record = {
+ "strict-send": "Minimum destination bound (strict send)",
+ "strict-receive": "Maximum send bound (strict receive)"
+};
+export const errorCopy: Record = {
+ invalid_hash: { title: "Transaction hash is invalid", description: "Enter exactly 64 hexadecimal characters." },
+ transaction_not_found: { title: "Transaction was not found", description: "Check the hash and selected network, then try again." },
+ no_path_payment_operation: { title: "No path payment found", description: "Use a settled transaction containing a strict-send or strict-receive path payment." },
+ malformed_effect: { title: "Execution data is incomplete", description: "Horizon returned a path payment without the amounts needed for an exact audit." },
+ rate_limited: { title: "Horizon rate limit reached", description: "Wait briefly, then retry the request." },
+ request_failed: { title: "Horizon request failed", description: "Check your connection and retry." }
+};
diff --git a/features/path-payment-slippage-calculator/e2e/path-payment-slippage-calculator.spec.ts b/features/path-payment-slippage-calculator/e2e/path-payment-slippage-calculator.spec.ts
new file mode 100644
index 00000000..3c72789c
--- /dev/null
+++ b/features/path-payment-slippage-calculator/e2e/path-payment-slippage-calculator.spec.ts
@@ -0,0 +1,15 @@
+/**
+ * End-to-end specification for the Path Payment Slippage Calculator tool.
+ *
+ * Documented as executable steps so the behaviour is reviewable even before a
+ * browser runner is wired into CI.
+ */
+export const spec = {
+ route: "/tools/path-payment-slippage-calculator",
+ steps: [
+ { action: "visit", target: "/tools/path-payment-slippage-calculator" },
+ { action: "expect", target: "heading", value: "Path Payment Slippage Calculator" },
+ { action: "click", target: "submit" },
+ { action: "expect", target: "alert" }
+ ]
+} as const;
diff --git a/features/path-payment-slippage-calculator/e2e/strict-receive-slippage.spec.ts b/features/path-payment-slippage-calculator/e2e/strict-receive-slippage.spec.ts
new file mode 100644
index 00000000..1343b85b
--- /dev/null
+++ b/features/path-payment-slippage-calculator/e2e/strict-receive-slippage.spec.ts
@@ -0,0 +1,2 @@
+/** Browser journey: strict-receive labels its maximum-send bound and exact result. */
+export const strictReceiveSpec = { route: "/tools/path-payment-slippage-calculator", expectedBound: "Maximum send bound (strict receive)", expectedSlippage: "5.00%" } as const;
diff --git a/features/path-payment-slippage-calculator/fixtures/pathPaymentSlippageCalculator.fixture.ts b/features/path-payment-slippage-calculator/fixtures/pathPaymentSlippageCalculator.fixture.ts
new file mode 100644
index 00000000..8d300bd6
--- /dev/null
+++ b/features/path-payment-slippage-calculator/fixtures/pathPaymentSlippageCalculator.fixture.ts
@@ -0,0 +1,5 @@
+import type { PathPaymentSlippageCalculatorResult } from "@/features/path-payment-slippage-calculator/types";
+export const pathPaymentSlippageCalculatorFixture: PathPaymentSlippageCalculatorResult = {
+ variant: "strict-send", declaredBound: "9.5", actualAmount: "10", slippageBasisPoints: "526",
+ boundAsset: { type: "native", code: "XLM", issuer: null }, path: [], network: "testnet"
+};
diff --git a/features/path-payment-slippage-calculator/fixtures/strict-receive-payment.fixture.ts b/features/path-payment-slippage-calculator/fixtures/strict-receive-payment.fixture.ts
new file mode 100644
index 00000000..b70262c2
--- /dev/null
+++ b/features/path-payment-slippage-calculator/fixtures/strict-receive-payment.fixture.ts
@@ -0,0 +1,4 @@
+export const strictReceiveRecord = {
+ type: "path_payment_strict_receive", source_max: "12.0000000", source_amount: "11.4000000",
+ source_asset_type: "native", path: [{ asset_type: "credit_alphanum4", asset_code: "EURC", asset_issuer: "GBRPYHIL2CI3CWB5GTXL7OMXNUJ6BGL5IABL5DRQO2IIGYF5BJSL5TLT" }]
+};
diff --git a/features/path-payment-slippage-calculator/fixtures/strict-send-payment.fixture.ts b/features/path-payment-slippage-calculator/fixtures/strict-send-payment.fixture.ts
new file mode 100644
index 00000000..8fb63b53
--- /dev/null
+++ b/features/path-payment-slippage-calculator/fixtures/strict-send-payment.fixture.ts
@@ -0,0 +1,6 @@
+export const strictSendRecord = {
+ type: "path_payment_strict_send", destination_min: "9.5000000", amount: "10.0000000",
+ asset_type: "credit_alphanum4", asset_code: "USDC",
+ asset_issuer: "GBRPYHIL2CI3CWB5GTXL7OMXNUJ6BGL5IABL5DRQO2IIGYF5BJSL5TLT",
+ path: [{ asset_type: "native" }]
+};
diff --git a/features/path-payment-slippage-calculator/hooks/usePathPaymentSlippageCalculator.ts b/features/path-payment-slippage-calculator/hooks/usePathPaymentSlippageCalculator.ts
new file mode 100644
index 00000000..df9a9265
--- /dev/null
+++ b/features/path-payment-slippage-calculator/hooks/usePathPaymentSlippageCalculator.ts
@@ -0,0 +1,61 @@
+"use client";
+
+import { useCallback, useRef, useState } from "react";
+import { useNetwork } from "@/core/network/NetworkProvider";
+import { isErr, type Result } from "@/core/result/result";
+import { parsePathPaymentSlippageCalculatorInput } from "@/features/path-payment-slippage-calculator/schema";
+import { runPathPaymentSlippageCalculator } from "@/features/path-payment-slippage-calculator/lib/pathPaymentSlippageCalculator";
+import { toPathPaymentSlippageCalculatorErrorCode } from "@/features/path-payment-slippage-calculator/lib/pathPaymentSlippageCalculator.errors";
+import type { PathPaymentSlippageCalculatorErrorCode, PathPaymentSlippageCalculatorResult } from "@/features/path-payment-slippage-calculator/types";
+
+export type PathPaymentSlippageCalculatorState =
+ | { status: "idle" }
+ | { status: "loading" }
+ | { status: "success"; result: PathPaymentSlippageCalculatorResult }
+ | { status: "error"; code: PathPaymentSlippageCalculatorErrorCode };
+
+export function usePathPaymentSlippageCalculator() {
+ const { network } = useNetwork();
+ const [state, setState] = useState({ status: "idle" });
+ const controller = useRef(null);
+
+ const submit = useCallback(
+ async (raw: string) => {
+ controller.current?.abort();
+ const parsed = parsePathPaymentSlippageCalculatorInput(raw);
+ if (isErr(parsed)) {
+ setState({ status: "error", code: parsed.code });
+ return;
+ }
+
+ const next = new AbortController();
+ controller.current = next;
+ setState({ status: "loading" });
+
+ try {
+ const result: Result = await runPathPaymentSlippageCalculator(
+ parsed.value,
+ network,
+ next.signal
+ );
+ if (next.signal.aborted) return;
+ setState(
+ result.ok
+ ? { status: "success", result: result.value }
+ : { status: "error", code: result.code }
+ );
+ } catch (error) {
+ if (next.signal.aborted) return;
+ setState({ status: "error", code: toPathPaymentSlippageCalculatorErrorCode(error) });
+ }
+ },
+ [network]
+ );
+
+ const reset = useCallback(() => {
+ controller.current?.abort();
+ setState({ status: "idle" });
+ }, []);
+
+ return { state, submit, reset };
+}
diff --git a/features/path-payment-slippage-calculator/lib/declared-bound.ts b/features/path-payment-slippage-calculator/lib/declared-bound.ts
new file mode 100644
index 00000000..f9057a23
--- /dev/null
+++ b/features/path-payment-slippage-calculator/lib/declared-bound.ts
@@ -0,0 +1,6 @@
+import { err, ok, type Result } from "@/core/result/result";
+import type { PathPaymentSlippageCalculatorErrorCode, PathPaymentRecord } from "@/features/path-payment-slippage-calculator/types";
+export function declaredBound(record: PathPaymentRecord): Result<{ direction: "minimum-destination" | "maximum-send"; amount: string }, PathPaymentSlippageCalculatorErrorCode> {
+ if (!record.declaredBound) return err("malformed_effect");
+ return ok({ direction: record.variant === "strict-send" ? "minimum-destination" : "maximum-send", amount: record.declaredBound });
+}
diff --git a/features/path-payment-slippage-calculator/lib/format.ts b/features/path-payment-slippage-calculator/lib/format.ts
new file mode 100644
index 00000000..3e7cb031
--- /dev/null
+++ b/features/path-payment-slippage-calculator/lib/format.ts
@@ -0,0 +1,8 @@
+import type { AssetIdentity } from "@/features/path-payment-slippage-calculator/types";
+export function formatBasisPoints(value: string): string {
+ const bps = BigInt(value); const sign = bps < 0n ? "-" : ""; const absolute = bps < 0n ? -bps : bps;
+ return `${sign}${absolute / 100n}.${(absolute % 100n).toString().padStart(2, "0")}%`;
+}
+export function formatAsset(asset: AssetIdentity): string {
+ return asset.type === "native" ? "XLM" : `${asset.code}:${asset.issuer}`;
+}
diff --git a/features/path-payment-slippage-calculator/lib/path-payment-extract.ts b/features/path-payment-slippage-calculator/lib/path-payment-extract.ts
new file mode 100644
index 00000000..ca4be711
--- /dev/null
+++ b/features/path-payment-slippage-calculator/lib/path-payment-extract.ts
@@ -0,0 +1,19 @@
+import { err, ok, type Result } from "@/core/result/result";
+import type { AssetIdentity, PathPaymentRecord, PathPaymentSlippageCalculatorErrorCode } from "@/features/path-payment-slippage-calculator/types";
+interface HorizonOperation { type?: string; destination_min?: string; source_max?: string; amount?: string; source_amount?: string; asset_type?: string; asset_code?: string; asset_issuer?: string; source_asset_type?: string; source_asset_code?: string; source_asset_issuer?: string; path?: Array> }
+function asset(type = "", code = "", issuer?: string): AssetIdentity { return type === "native" ? { type: "native", code: "XLM", issuer: null } : { type, code, issuer: issuer ?? null }; }
+export function extractPathPayment(records: HorizonOperation[]): Result {
+ const operation = records.find((item) => item.type === "path_payment_strict_send" || item.type === "path_payment_strict_receive");
+ if (!operation) return err("no_path_payment_operation");
+ const strictSend = operation.type === "path_payment_strict_send";
+ const declared = strictSend ? operation.destination_min : operation.source_max;
+ const actual = strictSend ? operation.amount : operation.source_amount;
+ if (!declared || !actual) return err("malformed_effect");
+ return ok({
+ variant: strictSend ? "strict-send" : "strict-receive", declaredBound: declared, actualAmount: actual,
+ boundAsset: strictSend
+ ? asset(operation.asset_type, operation.asset_code, operation.asset_issuer)
+ : asset(operation.source_asset_type, operation.source_asset_code, operation.source_asset_issuer),
+ path: (operation.path ?? []).map((item) => asset(item.asset_type, item.asset_code, item.asset_issuer))
+ });
+}
diff --git a/features/path-payment-slippage-calculator/lib/pathPaymentSlippageCalculator.errors.ts b/features/path-payment-slippage-calculator/lib/pathPaymentSlippageCalculator.errors.ts
new file mode 100644
index 00000000..12fa2c4c
--- /dev/null
+++ b/features/path-payment-slippage-calculator/lib/pathPaymentSlippageCalculator.errors.ts
@@ -0,0 +1,7 @@
+import type { PathPaymentSlippageCalculatorErrorCode } from "@/features/path-payment-slippage-calculator/types";
+export function statusToPathPaymentError(status: number): PathPaymentSlippageCalculatorErrorCode {
+ if (status === 404) return "transaction_not_found";
+ if (status === 429) return "rate_limited";
+ return "request_failed";
+}
+export function toPathPaymentSlippageCalculatorErrorCode(_error: unknown): PathPaymentSlippageCalculatorErrorCode { return "request_failed"; }
diff --git a/features/path-payment-slippage-calculator/lib/pathPaymentSlippageCalculator.ts b/features/path-payment-slippage-calculator/lib/pathPaymentSlippageCalculator.ts
new file mode 100644
index 00000000..f6bb62e5
--- /dev/null
+++ b/features/path-payment-slippage-calculator/lib/pathPaymentSlippageCalculator.ts
@@ -0,0 +1,17 @@
+import { err, ok, type Result } from "@/core/result/result";
+import { horizonUrl } from "@/core/horizon/client";
+import type { StellarNetwork } from "@/core/network/types";
+import { extractPathPayment } from "@/features/path-payment-slippage-calculator/lib/path-payment-extract";
+import { calculateSlippageBasisPoints } from "@/features/path-payment-slippage-calculator/lib/slippage-math";
+import { statusToPathPaymentError } from "@/features/path-payment-slippage-calculator/lib/pathPaymentSlippageCalculator.errors";
+import type { PathPaymentSlippageCalculatorErrorCode, PathPaymentSlippageCalculatorInput, PathPaymentSlippageCalculatorResult } from "@/features/path-payment-slippage-calculator/types";
+export async function runPathPaymentSlippageCalculator(input: PathPaymentSlippageCalculatorInput, network: StellarNetwork, signal?: AbortSignal): Promise> {
+ const response = await fetch(horizonUrl(network, `/transactions/${input.hash}/operations`, { limit: 200 }), { signal });
+ if (!response.ok) return err(statusToPathPaymentError(response.status));
+ const page = await response.json() as { _embedded?: { records?: unknown[] } };
+ const extracted = extractPathPayment((page._embedded?.records ?? []) as never[]);
+ if (!extracted.ok) return extracted;
+ const slippage = calculateSlippageBasisPoints(extracted.value.variant, extracted.value.declaredBound, extracted.value.actualAmount);
+ if (!slippage.ok) return slippage;
+ return ok({ ...extracted.value, network, slippageBasisPoints: slippage.value });
+}
diff --git a/features/path-payment-slippage-calculator/lib/slippage-math.ts b/features/path-payment-slippage-calculator/lib/slippage-math.ts
new file mode 100644
index 00000000..b7e0637f
--- /dev/null
+++ b/features/path-payment-slippage-calculator/lib/slippage-math.ts
@@ -0,0 +1,13 @@
+import { err, ok, type Result } from "@/core/result/result";
+import type { PathPaymentSlippageCalculatorErrorCode, PathVariant } from "@/features/path-payment-slippage-calculator/types";
+function units(value: string): bigint | null {
+ if (!/^\d+(?:\.\d{1,7})?$/.test(value)) return null;
+ const [whole, fraction = ""] = value.split(".");
+ return BigInt(whole) * 10_000_000n + BigInt(fraction.padEnd(7, "0"));
+}
+export function calculateSlippageBasisPoints(variant: PathVariant, bound: string, actual: string): Result {
+ const boundUnits = units(bound); const actualUnits = units(actual);
+ if (boundUnits === null || actualUnits === null || boundUnits === 0n) return err("malformed_effect");
+ const delta = variant === "strict-send" ? actualUnits - boundUnits : boundUnits - actualUnits;
+ return ok(((delta * 10_000n) / boundUnits).toString());
+}
diff --git a/features/path-payment-slippage-calculator/manifest.ts b/features/path-payment-slippage-calculator/manifest.ts
new file mode 100644
index 00000000..cbbddcd2
--- /dev/null
+++ b/features/path-payment-slippage-calculator/manifest.ts
@@ -0,0 +1,8 @@
+import { Route } from "lucide-react";
+import type { FeatureManifest } from "@/core/registry/types";
+export const manifest: FeatureManifest = {
+ slug: "path-payment-slippage-calculator", title: "Path Payment Slippage Calculator",
+ description: "Audit realized path-payment execution against its declared bound.", character: "Follow every hop; measure every stroop.",
+ category: "payments", status: "beta", icon: Route, networks: ["testnet", "mainnet"],
+ keywords: ["path payment", "slippage", "strict send", "strict receive"]
+};
diff --git a/features/path-payment-slippage-calculator/msw/handlers.ts b/features/path-payment-slippage-calculator/msw/handlers.ts
new file mode 100644
index 00000000..3a7ba77d
--- /dev/null
+++ b/features/path-payment-slippage-calculator/msw/handlers.ts
@@ -0,0 +1,6 @@
+import { http, HttpResponse } from "msw";
+
+/** Request mocks used by this slice's tests. Keep responses realistic. */
+export const handlers = [
+ http.get("https://horizon-testnet.stellar.org/*", () => HttpResponse.json({}))
+];
diff --git a/features/path-payment-slippage-calculator/panel.tsx b/features/path-payment-slippage-calculator/panel.tsx
new file mode 100644
index 00000000..d3b82e54
--- /dev/null
+++ b/features/path-payment-slippage-calculator/panel.tsx
@@ -0,0 +1 @@
+export { PathPaymentSlippageCalculatorPanel as default } from "@/features/path-payment-slippage-calculator/components/PathPaymentSlippageCalculatorPanel";
diff --git a/features/path-payment-slippage-calculator/schema.ts b/features/path-payment-slippage-calculator/schema.ts
new file mode 100644
index 00000000..07431538
--- /dev/null
+++ b/features/path-payment-slippage-calculator/schema.ts
@@ -0,0 +1,7 @@
+import { err, ok, type Result } from "@/core/result/result";
+import { normalizeInput } from "@/core/lib/strings";
+import type { PathPaymentSlippageCalculatorErrorCode, PathPaymentSlippageCalculatorInput } from "@/features/path-payment-slippage-calculator/types";
+export function parsePathPaymentSlippageCalculatorInput(raw: string): Result {
+ const hash = normalizeInput(raw).toLowerCase();
+ return /^[0-9a-f]{64}$/.test(hash) ? ok({ hash }) : err("invalid_hash");
+}
diff --git a/features/path-payment-slippage-calculator/types.ts b/features/path-payment-slippage-calculator/types.ts
new file mode 100644
index 00000000..0356219f
--- /dev/null
+++ b/features/path-payment-slippage-calculator/types.ts
@@ -0,0 +1,18 @@
+import type { StellarNetwork } from "@/core/network/types";
+export type PathVariant = "strict-send" | "strict-receive";
+export interface PathPaymentSlippageCalculatorInput { hash: string }
+export interface AssetIdentity { type: string; code: string; issuer: string | null }
+export interface PathPaymentRecord {
+ variant: PathVariant;
+ declaredBound: string;
+ actualAmount: string;
+ boundAsset: AssetIdentity;
+ path: AssetIdentity[];
+}
+export interface PathPaymentSlippageCalculatorResult extends PathPaymentRecord {
+ network: StellarNetwork;
+ slippageBasisPoints: string;
+}
+export type PathPaymentSlippageCalculatorErrorCode =
+ | "invalid_hash" | "transaction_not_found" | "no_path_payment_operation"
+ | "malformed_effect" | "rate_limited" | "request_failed";