diff --git a/apps/web/app/anchors/page.tsx b/apps/web/app/anchors/page.tsx
index 45b729c..65ddc4d 100644
--- a/apps/web/app/anchors/page.tsx
+++ b/apps/web/app/anchors/page.tsx
@@ -13,6 +13,7 @@ import {
Select,
ValidationStateAlert,
} from "@/components/ui";
+import { CapabilityMatrixCard } from "@/components/CapabilityMatrixCard";
import {
anchorStatusToUserMessage,
anchorValidationUiState,
@@ -31,6 +32,7 @@ import {
transition,
ALLOWED_TRANSITIONS,
} from "@anchorkit/anchor-utils";
+import { DEFAULT_ANCHOR_CAPABILITY_MATRIX } from "@anchorkit/config";
import { anchorRecordToReceipt } from "@anchorkit/stellar-kit";
import { validateCallbackUrl } from "@anchorkit/validators";
import type { ValidationResult } from "@anchorkit/validators";
@@ -544,6 +546,18 @@ export default function AnchorsPage() {
)}
+
+ {/* ── Capability matrix (issue #54) ───────────────────────────────── */}
+
+ Anchor capability matrix
+
+ The matrix below describes which payment rails and assets are supported by the mock
+ anchor integration, and whether deposit and withdrawal flows are available for each.
+ Experimental and disabled behaviours are called out explicitly so contributors can avoid
+ building unsupported flows.
+
+
+
);
}
diff --git a/apps/web/components/CapabilityMatrixCard.tsx b/apps/web/components/CapabilityMatrixCard.tsx
new file mode 100644
index 0000000..28c64eb
--- /dev/null
+++ b/apps/web/components/CapabilityMatrixCard.tsx
@@ -0,0 +1,254 @@
+"use client";
+
+import clsx from "clsx";
+import type {
+ AnchorCapabilityMatrix,
+ AnchorRailCapability,
+ AnchorAssetCapability,
+ AnchorRailCapabilityState,
+} from "@anchorkit/types";
+import { Card, CapabilityBadge } from "@/components/ui";
+
+// ─── State badge ──────────────────────────────────────────────────────────────
+
+/**
+ * Renders a small pill badge for `AnchorRailCapabilityState`. Reuses the same
+ * colour semantics as `CapabilityBadge` but accepts the extended rail state
+ * union (which includes `"unsupported"`).
+ */
+function RailStateBadge({ state }: { state: AnchorRailCapabilityState }) {
+ // Map the extended rail states onto the base CapabilityState where possible
+ const mapped = state === "unsupported" ? "unavailable" : state;
+ return ;
+}
+
+// ─── Rail row ─────────────────────────────────────────────────────────────────
+
+function RailRow({ rail }: { rail: AnchorRailCapability }) {
+ return (
+
+
+ {rail.name}
+
+
+
+
+ Deposit:{" "}
+
+ {rail.depositSupported ? "✓" : "✗"}
+
+
+
+ Withdrawal:{" "}
+
+ {rail.withdrawalSupported ? "✓" : "✗"}
+
+
+ Currencies: {rail.currencies.join(", ")}
+ Countries: {rail.countries.join(", ")}
+
+ {rail.note && (
+ {rail.note}
+ )}
+
+ );
+}
+
+// ─── Asset row ────────────────────────────────────────────────────────────────
+
+function AssetRow({ asset }: { asset: AnchorAssetCapability }) {
+ return (
+
+
+ {asset.code}
+
+ {asset.enabled ? "enabled" : "disabled"}
+
+
+
+
+ Deposit:{" "}
+
+ {asset.depositEnabled ? "✓" : "✗"}
+
+
+
+ Withdrawal:{" "}
+
+ {asset.withdrawalEnabled ? "✓" : "✗"}
+
+
+ {asset.feeFixed && Fee (fixed): {asset.feeFixed}}
+ {asset.feePercent && Fee (%): {asset.feePercent}%}
+
+ {asset.note && (
+ {asset.note}
+ )}
+
+ );
+}
+
+// ─── Behaviours section ────────────────────────────────────────────────────────
+
+function BehavioursSection({
+ title,
+ behaviours,
+ tone,
+}: {
+ title: string;
+ behaviours: Record;
+ tone: "amber" | "red";
+}) {
+ const entries = Object.entries(behaviours);
+ if (entries.length === 0) return null;
+
+ const classes =
+ tone === "amber"
+ ? "border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-300"
+ : "border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-300";
+
+ return (
+
+
{title}
+
+ {entries.map(([key, desc]) => (
+ -
+ {key}: {desc}
+
+ ))}
+
+
+ );
+}
+
+// ─── Main component ────────────────────────────────────────────────────────────
+
+/**
+ * Renders the full anchor capability matrix as a dashboard card. Displays the
+ * overall anchor state, all rail capabilities, all asset capabilities, and any
+ * experimental/disabled behaviour notes.
+ *
+ * Used on the Anchors page to give contributors a single-glance view of what
+ * the anchor integration supports.
+ */
+export function CapabilityMatrixCard({
+ matrix,
+}: {
+ matrix: AnchorCapabilityMatrix;
+}) {
+ return (
+
+ {/* Header */}
+
+
+
{matrix.anchorName}
+ {matrix.isMock && (
+
+ Mock anchor — no real transactions are submitted
+
+ )}
+
+
+
+
+
+
+ {/* Flow states */}
+
+
+ Deposit flow:
+
+
+
+ Withdrawal flow:
+
+
+
+
+ {/* Payment rails */}
+
+
+ Payment rails ({matrix.rails.length})
+
+
+ {matrix.rails.map((rail) => (
+
+ ))}
+
+
+
+ {/* Supported assets */}
+
+
+ Supported assets ({matrix.assets.length})
+
+
+ {matrix.assets.map((asset) => (
+
+ ))}
+
+
+
+ {/* Experimental behaviours */}
+ {matrix.experimentalBehaviours &&
+ Object.keys(matrix.experimentalBehaviours).length > 0 && (
+
+ )}
+
+ {/* Disabled behaviours */}
+ {matrix.disabledBehaviours &&
+ Object.keys(matrix.disabledBehaviours).length > 0 && (
+
+ )}
+
+ {/* Docs link */}
+ {matrix.docsHref && (
+
+ Anchor capability docs →
+
+ )}
+
+ );
+}
diff --git a/apps/web/test/capability-matrix.test.ts b/apps/web/test/capability-matrix.test.ts
new file mode 100644
index 0000000..8cb5c92
--- /dev/null
+++ b/apps/web/test/capability-matrix.test.ts
@@ -0,0 +1,58 @@
+/**
+ * Tests for the anchor capability matrix integration in the web dashboard (issue #54).
+ *
+ * Verifies that `DEFAULT_ANCHOR_CAPABILITY_MATRIX` is structurally valid and
+ * that the `@anchorkit/anchor-utils` parse helpers accept it, without
+ * importing React or mounting the component (Next.js RSC compatibility).
+ */
+
+import { describe, it, expect } from "vitest";
+import { DEFAULT_ANCHOR_CAPABILITY_MATRIX } from "@anchorkit/config";
+import {
+ isRailDepositReady,
+ isRailWithdrawalReady,
+} from "@anchorkit/config";
+import { isAnchorCapabilityMatrixValid } from "@anchorkit/anchor-utils";
+import { ANCHOR_RAIL_CAPABILITY_STATES } from "@anchorkit/types";
+
+describe("DEFAULT_ANCHOR_CAPABILITY_MATRIX (web integration)", () => {
+ it("is a valid AnchorCapabilityMatrix according to the validator", () => {
+ expect(isAnchorCapabilityMatrixValid(DEFAULT_ANCHOR_CAPABILITY_MATRIX)).toBe(true);
+ });
+
+ it("exposes SEPA as deposit-ready", () => {
+ expect(isRailDepositReady(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "SEPA")).toBe(true);
+ });
+
+ it("exposes ACH as withdrawal-ready", () => {
+ expect(isRailWithdrawalReady(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "ACH")).toBe(true);
+ });
+
+ it("marks CARD as not deposit-ready (unavailable)", () => {
+ expect(isRailDepositReady(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "CARD")).toBe(false);
+ });
+
+ it("marks WIRE as not withdrawal-ready (experimental, withdrawal unsupported)", () => {
+ expect(isRailWithdrawalReady(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "WIRE")).toBe(false);
+ });
+
+ it("all rail states are valid ANCHOR_RAIL_CAPABILITY_STATES", () => {
+ for (const rail of DEFAULT_ANCHOR_CAPABILITY_MATRIX.rails) {
+ expect(ANCHOR_RAIL_CAPABILITY_STATES).toContain(rail.state);
+ }
+ });
+
+ it("at least one asset is deposit-enabled", () => {
+ const depositEnabled = DEFAULT_ANCHOR_CAPABILITY_MATRIX.assets.filter(
+ (a) => a.enabled && a.depositEnabled
+ );
+ expect(depositEnabled.length).toBeGreaterThan(0);
+ });
+
+ it("at least one asset is withdrawal-enabled", () => {
+ const withdrawalEnabled = DEFAULT_ANCHOR_CAPABILITY_MATRIX.assets.filter(
+ (a) => a.enabled && a.withdrawalEnabled
+ );
+ expect(withdrawalEnabled.length).toBeGreaterThan(0);
+ });
+});
diff --git a/docs/ANCHOR_RAILS_CAPABILITY_MATRIX.md b/docs/ANCHOR_RAILS_CAPABILITY_MATRIX.md
new file mode 100644
index 0000000..eda3433
--- /dev/null
+++ b/docs/ANCHOR_RAILS_CAPABILITY_MATRIX.md
@@ -0,0 +1,172 @@
+# Anchor Rails Capability Matrix
+
+This document describes the typed capability matrix for anchor rails, supported assets, deposit flows, withdrawal flows, and disabled or experimental behaviours.
+
+## Overview
+
+The capability matrix gives contributors a single-source-of-truth model for what the mock anchor supports. It prevents building unsupported flows by making the following explicit:
+
+- Which **payment rails** exist and whether each is `mock`, `experimental`, `unavailable`, or `unsupported`.
+- Which **assets** are enabled and whether **deposit** and **withdrawal** are supported per asset.
+- Which **behaviours** are explicitly **experimental** or **disabled**, with a human-readable reason.
+
+The matrix is rendered as a card on the **Anchors** dashboard page (`/anchors`).
+
+---
+
+## Types
+
+All types are exported from `@anchorkit/types`.
+
+### `AnchorRailCapabilityState`
+
+Extends the base `CapabilityState` with one additional value:
+
+| Value | Meaning |
+| :--- | :--- |
+| `implemented` | Fully implemented and tested |
+| `mock` | UI/data shape exists; backed by local state, not a real integration |
+| `testnet-only` | Works against Stellar testnet; not mainnet |
+| `experimental` | Preview code; may change or be disabled without notice |
+| `unavailable` | Not built; card is disabled |
+| `unsupported` | Structurally known but explicitly not offered by this anchor |
+
+### `AnchorRailCapability`
+
+```ts
+interface AnchorRailCapability {
+ railId: string; // stable id, e.g. "SEPA", "ACH"
+ name: string; // display name
+ state: AnchorRailCapabilityState;
+ depositSupported: boolean;
+ withdrawalSupported: boolean;
+ currencies: string[]; // ISO 4217 codes
+ countries: string[]; // ISO 3166-1 alpha-2 codes
+ note?: string; // explains non-implemented state
+}
+```
+
+### `AnchorAssetCapability`
+
+```ts
+interface AnchorAssetCapability {
+ code: string; // e.g. "USDC", "XLM"
+ issuer: string | null; // null for native XLM
+ enabled: boolean;
+ depositEnabled: boolean;
+ withdrawalEnabled: boolean;
+ depositMinAmount?: string;
+ depositMaxAmount?: string;
+ withdrawalMinAmount?: string;
+ withdrawalMaxAmount?: string;
+ feeFixed?: string;
+ feePercent?: string;
+ note?: string;
+}
+```
+
+### `AnchorCapabilityMatrix`
+
+```ts
+interface AnchorCapabilityMatrix {
+ anchorName: string;
+ overallState: AnchorRailCapabilityState;
+ isMock: boolean;
+ depositState: AnchorRailCapabilityState;
+ withdrawalState: AnchorRailCapabilityState;
+ rails: AnchorRailCapability[];
+ assets: AnchorAssetCapability[];
+ experimentalBehaviours?: Record;
+ disabledBehaviours?: Record;
+ docsHref?: string;
+}
+```
+
+---
+
+## Default Capability Matrix
+
+The default matrix (`DEFAULT_ANCHOR_CAPABILITY_MATRIX`) is exported from `@anchorkit/config` and used by the web dashboard's Anchors page.
+
+### Payment rails
+
+| Rail | State | Deposit | Withdrawal | Currencies | Countries |
+| :--- | :--- | :--- | :--- | :--- | :--- |
+| SEPA | `mock` | ✓ | ✓ | EUR | DE, FR, ES, IT, NL, BE, AT, PT, IE, FI |
+| ACH | `mock` | ✓ | ✓ | USD | US |
+| WIRE | `experimental` | ✓ | ✗ | USD, EUR, GBP | US, GB, DE, FR |
+| CARD | `unavailable` | ✗ | ✗ | USD, EUR | US, DE |
+
+### Supported assets
+
+| Asset | Deposit | Withdrawal | Notes |
+| :--- | :--- | :--- | :--- |
+| XLM (native) | ✓ | ✓ | Fee: 1.5 fixed + 0.1% |
+| USDC | ✓ | ✓ | Fee: 0.50 fixed + 0.2% |
+| EURC | ✓ | ✗ | Withdrawal experimental, currently disabled |
+
+### Disabled behaviours
+
+| Key | Reason |
+| :--- | :--- |
+| `card_payments` | Card payment rails are not available in this release |
+| `eurc_withdrawal` | EURC withdrawal is not yet supported |
+| `wire_withdrawal` | International wire withdrawal is not yet supported |
+
+---
+
+## Where the code lives
+
+| Purpose | Location |
+| :--- | :--- |
+| Type definitions | `packages/types/src/railCapability.ts` |
+| Zod schemas | `packages/validators/src/schemas/railCapability.ts` |
+| Default matrix & query helpers | `packages/config/src/railConfig.ts` |
+| Parse/validate utilities | `packages/anchor-utils/src/railCapability.ts` |
+| Fixtures | `packages/fixtures/src/railCapability.ts` |
+| Dashboard card component | `apps/web/components/CapabilityMatrixCard.tsx` |
+| Dashboard page integration | `apps/web/app/anchors/page.tsx` |
+
+---
+
+## Query helpers (`@anchorkit/config`)
+
+```ts
+// Filter rails by state
+getRailsByState(matrix, "mock") // → AnchorRailCapability[]
+
+// Filter assets by flow support
+getDepositEnabledAssets(matrix) // → AnchorAssetCapability[]
+getWithdrawalEnabledAssets(matrix) // → AnchorAssetCapability[]
+
+// Lookup by ID / code
+findRailById(matrix, "SEPA") // → AnchorRailCapability | undefined
+findAssetByCode(matrix, "USDC") // → AnchorAssetCapability | undefined
+
+// Readiness checks
+isRailDepositReady(matrix, "SEPA") // → boolean
+isRailWithdrawalReady(matrix, "ACH") // → boolean
+```
+
+## Validate helpers (`@anchorkit/anchor-utils`)
+
+```ts
+parseAnchorCapabilityMatrix(input) // → SafeParseReturnType (never throws)
+isAnchorCapabilityMatrixValid(input) // → boolean
+parseAnchorRailCapability(input) // → SafeParseReturnType
+isAnchorRailCapabilityValid(input) // → boolean
+parseAnchorAssetCapability(input) // → SafeParseReturnType
+isAnchorAssetCapabilityValid(input) // → boolean
+```
+
+---
+
+## Extending the matrix
+
+To add a new rail or asset:
+
+1. Add a `AnchorRailCapability` or `AnchorAssetCapability` entry to `DEFAULT_ANCHOR_CAPABILITY_MATRIX` in `packages/config/src/railConfig.ts`.
+2. If the state is `experimental` or `unavailable`, add a matching entry to `experimentalBehaviours` or `disabledBehaviours`.
+3. Add a fixture in `packages/fixtures/src/railCapability.ts` if needed for testing.
+4. Update the table in this document.
+5. Run `pnpm verify` to confirm all tests and types pass.
diff --git a/packages/anchor-utils/src/capabilities.ts b/packages/anchor-utils/src/capabilities.ts
index 9852d8e..4316287 100644
--- a/packages/anchor-utils/src/capabilities.ts
+++ b/packages/anchor-utils/src/capabilities.ts
@@ -40,6 +40,12 @@ export const ANCHOR_UTILS_CAPABILITIES: PackageCapability = {
state: "implemented",
description: "Pre-built arrays representing successful deposits and failed/refunded withdrawals.",
},
+ {
+ id: "rail-capability",
+ label: "Rail Capability Matrix Validators",
+ state: "implemented",
+ description: "Parse and validate AnchorCapabilityMatrix, AnchorRailCapability, and AnchorAssetCapability objects with typed Zod-backed helpers.",
+ },
],
docsHref: "/docs#anchor-utils",
};
diff --git a/packages/anchor-utils/src/index.ts b/packages/anchor-utils/src/index.ts
index 139c21a..ed50322 100644
--- a/packages/anchor-utils/src/index.ts
+++ b/packages/anchor-utils/src/index.ts
@@ -353,3 +353,6 @@ export function anchorValidationUiState(
export * from "./capabilities";
+// ─── Anchor rails capability matrix (issue #54) ──────────────────────────────
+export * from "./railCapability";
+
diff --git a/packages/anchor-utils/src/railCapability.ts b/packages/anchor-utils/src/railCapability.ts
new file mode 100644
index 0000000..71c49d9
--- /dev/null
+++ b/packages/anchor-utils/src/railCapability.ts
@@ -0,0 +1,79 @@
+/**
+ * Anchor capability matrix utilities (issue #54).
+ *
+ * Provides parse/validate wrappers and convenience helpers for working with
+ * `AnchorCapabilityMatrix`, `AnchorRailCapability`, and `AnchorAssetCapability`
+ * from within `@anchorkit/anchor-utils`.
+ */
+
+import type {
+ AnchorCapabilityMatrix,
+ AnchorRailCapability,
+ AnchorAssetCapability,
+} from "@anchorkit/types";
+import {
+ AnchorCapabilityMatrixSchema,
+ AnchorRailCapabilitySchema,
+ AnchorAssetCapabilitySchema,
+} from "@anchorkit/validators";
+import type { SafeParseReturnType } from "zod";
+
+// ─── Parse helpers ────────────────────────────────────────────────────────────
+
+/**
+ * Safely parse and validate an `AnchorCapabilityMatrix` object.
+ * Returns a Zod `SafeParseReturnType` — never throws.
+ */
+export function parseAnchorCapabilityMatrix(
+ input: unknown
+): SafeParseReturnType {
+ return AnchorCapabilityMatrixSchema.safeParse(input) as SafeParseReturnType<
+ unknown,
+ AnchorCapabilityMatrix
+ >;
+}
+
+/**
+ * Returns `true` when the input is a structurally valid `AnchorCapabilityMatrix`.
+ */
+export function isAnchorCapabilityMatrixValid(input: unknown): boolean {
+ return parseAnchorCapabilityMatrix(input).success;
+}
+
+/**
+ * Safely parse and validate an `AnchorRailCapability` object.
+ */
+export function parseAnchorRailCapability(
+ input: unknown
+): SafeParseReturnType {
+ return AnchorRailCapabilitySchema.safeParse(input) as SafeParseReturnType<
+ unknown,
+ AnchorRailCapability
+ >;
+}
+
+/**
+ * Returns `true` when the input is a structurally valid `AnchorRailCapability`.
+ */
+export function isAnchorRailCapabilityValid(input: unknown): boolean {
+ return parseAnchorRailCapability(input).success;
+}
+
+/**
+ * Safely parse and validate an `AnchorAssetCapability` object.
+ */
+export function parseAnchorAssetCapability(
+ input: unknown
+): SafeParseReturnType {
+ return AnchorAssetCapabilitySchema.safeParse(input) as SafeParseReturnType<
+ unknown,
+ AnchorAssetCapability
+ >;
+}
+
+/**
+ * Returns `true` when the input is a structurally valid `AnchorAssetCapability`.
+ */
+export function isAnchorAssetCapabilityValid(input: unknown): boolean {
+ return parseAnchorAssetCapability(input).success;
+}
diff --git a/packages/anchor-utils/test/railCapability.test.ts b/packages/anchor-utils/test/railCapability.test.ts
new file mode 100644
index 0000000..186a0c8
--- /dev/null
+++ b/packages/anchor-utils/test/railCapability.test.ts
@@ -0,0 +1,150 @@
+/**
+ * Tests for anchor-utils rail capability parse/validate helpers (issue #54).
+ */
+
+import { describe, it, expect } from "vitest";
+import {
+ parseAnchorCapabilityMatrix,
+ isAnchorCapabilityMatrixValid,
+ parseAnchorRailCapability,
+ isAnchorRailCapabilityValid,
+ parseAnchorAssetCapability,
+ isAnchorAssetCapabilityValid,
+} from "../src/railCapability";
+
+// ─── Shared valid stubs ───────────────────────────────────────────────────────
+
+const validRail = {
+ railId: "SEPA",
+ name: "SEPA Credit Transfer",
+ state: "mock",
+ depositSupported: true,
+ withdrawalSupported: true,
+ currencies: ["EUR"],
+ countries: ["DE"],
+};
+
+const validAsset = {
+ code: "XLM",
+ issuer: null,
+ enabled: true,
+ depositEnabled: true,
+ withdrawalEnabled: true,
+};
+
+const validMatrix = {
+ anchorName: "Test Anchor",
+ overallState: "mock",
+ isMock: true,
+ depositState: "mock",
+ withdrawalState: "mock",
+ rails: [validRail],
+ assets: [validAsset],
+};
+
+// ─── parseAnchorCapabilityMatrix ──────────────────────────────────────────────
+
+describe("parseAnchorCapabilityMatrix", () => {
+ it("returns success for a valid matrix", () => {
+ const result = parseAnchorCapabilityMatrix(validMatrix);
+ expect(result.success).toBe(true);
+ });
+
+ it("returns failure for a matrix missing anchorName", () => {
+ const { anchorName: _, ...bad } = validMatrix;
+ const result = parseAnchorCapabilityMatrix(bad);
+ expect(result.success).toBe(false);
+ });
+
+ it("returns failure for null input", () => {
+ expect(parseAnchorCapabilityMatrix(null).success).toBe(false);
+ });
+
+ it("never throws on invalid input", () => {
+ expect(() => parseAnchorCapabilityMatrix(undefined)).not.toThrow();
+ expect(() => parseAnchorCapabilityMatrix("bad")).not.toThrow();
+ expect(() => parseAnchorCapabilityMatrix(42)).not.toThrow();
+ });
+});
+
+describe("isAnchorCapabilityMatrixValid", () => {
+ it("returns true for a valid matrix", () => {
+ expect(isAnchorCapabilityMatrixValid(validMatrix)).toBe(true);
+ });
+
+ it("returns false for an empty object", () => {
+ expect(isAnchorCapabilityMatrixValid({})).toBe(false);
+ });
+
+ it("returns false for null", () => {
+ expect(isAnchorCapabilityMatrixValid(null)).toBe(false);
+ });
+});
+
+// ─── parseAnchorRailCapability ────────────────────────────────────────────────
+
+describe("parseAnchorRailCapability", () => {
+ it("returns success for a valid rail", () => {
+ expect(parseAnchorRailCapability(validRail).success).toBe(true);
+ });
+
+ it("returns failure for a rail with empty railId", () => {
+ const bad = { ...validRail, railId: "" };
+ expect(parseAnchorRailCapability(bad).success).toBe(false);
+ });
+
+ it("returns failure for a rail with invalid state", () => {
+ const bad = { ...validRail, state: "not-a-state" };
+ expect(parseAnchorRailCapability(bad).success).toBe(false);
+ });
+
+ it("never throws", () => {
+ expect(() => parseAnchorRailCapability(null)).not.toThrow();
+ });
+});
+
+describe("isAnchorRailCapabilityValid", () => {
+ it("returns true for a valid rail", () => {
+ expect(isAnchorRailCapabilityValid(validRail)).toBe(true);
+ });
+
+ it("returns false for an invalid rail", () => {
+ expect(isAnchorRailCapabilityValid({ railId: "X" })).toBe(false);
+ });
+});
+
+// ─── parseAnchorAssetCapability ───────────────────────────────────────────────
+
+describe("parseAnchorAssetCapability", () => {
+ it("returns success for a valid native asset", () => {
+ expect(parseAnchorAssetCapability(validAsset).success).toBe(true);
+ });
+
+ it("returns success for a valid issued asset", () => {
+ const issued = {
+ ...validAsset,
+ code: "USDC",
+ issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
+ };
+ expect(parseAnchorAssetCapability(issued).success).toBe(true);
+ });
+
+ it("returns failure for an asset with an empty code", () => {
+ const bad = { ...validAsset, code: "" };
+ expect(parseAnchorAssetCapability(bad).success).toBe(false);
+ });
+
+ it("never throws", () => {
+ expect(() => parseAnchorAssetCapability(undefined)).not.toThrow();
+ });
+});
+
+describe("isAnchorAssetCapabilityValid", () => {
+ it("returns true for a valid asset", () => {
+ expect(isAnchorAssetCapabilityValid(validAsset)).toBe(true);
+ });
+
+ it("returns false for an empty object", () => {
+ expect(isAnchorAssetCapabilityValid({})).toBe(false);
+ });
+});
diff --git a/packages/config/src/capabilities.ts b/packages/config/src/capabilities.ts
index 7cc7404..19f9443 100644
--- a/packages/config/src/capabilities.ts
+++ b/packages/config/src/capabilities.ts
@@ -70,6 +70,12 @@ export const CONFIG_PACKAGE_CAPABILITIES: PackageCapability = {
state: "implemented",
description: "Merge default settings with explicit configuration overrides and resolve metadata sources.",
},
+ {
+ id: "rail-config",
+ label: "Anchor Rails Configuration",
+ state: "implemented",
+ description: "Default anchor capability matrix with rail definitions, asset configurations, and query helpers for deposit/withdrawal readiness.",
+ },
],
docsHref: "/docs#config",
};
diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts
index 3d57a53..c23c128 100644
--- a/packages/config/src/index.ts
+++ b/packages/config/src/index.ts
@@ -274,3 +274,6 @@ export function resolveConfigSourceMetadata(
// ─── Module capabilities ────────────────────────────────────────────────────
export * from "./capabilities";
+
+// ─── Anchor rails configuration and capability matrix (issue #54) ────────────
+export * from "./railConfig";
diff --git a/packages/config/src/railConfig.ts b/packages/config/src/railConfig.ts
new file mode 100644
index 0000000..b3fb186
--- /dev/null
+++ b/packages/config/src/railConfig.ts
@@ -0,0 +1,222 @@
+/**
+ * Default anchor rails configuration and capability matrix (issue #54).
+ *
+ * Provides the default mock anchor capability matrix used by the web
+ * dashboard's Anchors page. Also exports helper functions for querying
+ * rail and asset capabilities.
+ */
+
+import type {
+ AnchorCapabilityMatrix,
+ AnchorRailCapability,
+ AnchorAssetCapability,
+ AnchorRailCapabilityState,
+} from "@anchorkit/types";
+
+// ─── Default rail definitions ────────────────────────────────────────────────
+
+/** SEPA Credit Transfer (EUR, Europe). */
+const SEPA_RAIL: AnchorRailCapability = {
+ railId: "SEPA",
+ name: "SEPA Credit Transfer",
+ state: "mock",
+ depositSupported: true,
+ withdrawalSupported: true,
+ currencies: ["EUR"],
+ countries: ["DE", "FR", "ES", "IT", "NL", "BE", "AT", "PT", "IE", "FI"],
+ note: "Mock implementation only. No real SEPA transfers are initiated.",
+};
+
+/** ACH (USD, United States). */
+const ACH_RAIL: AnchorRailCapability = {
+ railId: "ACH",
+ name: "ACH Bank Transfer",
+ state: "mock",
+ depositSupported: true,
+ withdrawalSupported: true,
+ currencies: ["USD"],
+ countries: ["US"],
+ note: "Mock implementation only. No real ACH transfers are initiated.",
+};
+
+/** WIRE transfer (multi-currency, global). */
+const WIRE_RAIL: AnchorRailCapability = {
+ railId: "WIRE",
+ name: "International Wire Transfer",
+ state: "experimental",
+ depositSupported: true,
+ withdrawalSupported: false,
+ currencies: ["USD", "EUR", "GBP"],
+ countries: ["US", "GB", "DE", "FR"],
+ note: "Experimental — deposit-only. Withdrawal via wire is not yet supported.",
+};
+
+/** Card payment (disabled in MVP). */
+const CARD_RAIL: AnchorRailCapability = {
+ railId: "CARD",
+ name: "Card Payment",
+ state: "unavailable",
+ depositSupported: false,
+ withdrawalSupported: false,
+ currencies: ["USD", "EUR"],
+ countries: ["US", "DE"],
+ note: "Card rails are not available in this release.",
+};
+
+// ─── Default asset definitions ────────────────────────────────────────────────
+
+/** XLM (native Stellar asset). */
+const XLM_ASSET: AnchorAssetCapability = {
+ code: "XLM",
+ issuer: null,
+ enabled: true,
+ depositEnabled: true,
+ withdrawalEnabled: true,
+ depositMinAmount: "10.0000000",
+ depositMaxAmount: "100000.0000000",
+ withdrawalMinAmount: "10.0000000",
+ withdrawalMaxAmount: "100000.0000000",
+ feeFixed: "1.5000000",
+ feePercent: "0.1",
+};
+
+/** USDC (Circle USD Coin on Stellar testnet). */
+const USDC_ASSET: AnchorAssetCapability = {
+ code: "USDC",
+ issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
+ enabled: true,
+ depositEnabled: true,
+ withdrawalEnabled: true,
+ depositMinAmount: "5.00",
+ depositMaxAmount: "50000.00",
+ withdrawalMinAmount: "5.00",
+ withdrawalMaxAmount: "50000.00",
+ feeFixed: "0.50",
+ feePercent: "0.2",
+};
+
+/** EURC (experimental, deposit-only for now). */
+const EURC_ASSET: AnchorAssetCapability = {
+ code: "EURC",
+ issuer: "GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP",
+ enabled: true,
+ depositEnabled: true,
+ withdrawalEnabled: false,
+ depositMinAmount: "5.00",
+ depositMaxAmount: "25000.00",
+ note: "EURC withdrawal is experimental and currently disabled.",
+};
+
+// ─── Default capability matrix ────────────────────────────────────────────────
+
+/**
+ * Default mock anchor capability matrix.
+ *
+ * Represents the mock anchor used by the AnchorKit web dashboard's Anchors
+ * page. All flows are backed by local state — no real anchor server is called.
+ */
+export const DEFAULT_ANCHOR_CAPABILITY_MATRIX: AnchorCapabilityMatrix = {
+ anchorName: "Mock Anchor (AnchorKit Demo)",
+ overallState: "mock",
+ isMock: true,
+ depositState: "mock",
+ withdrawalState: "mock",
+ rails: [SEPA_RAIL, ACH_RAIL, WIRE_RAIL, CARD_RAIL],
+ assets: [XLM_ASSET, USDC_ASSET, EURC_ASSET],
+ experimentalBehaviours: {
+ wire_deposit: "International wire deposits are available experimentally for USD, EUR, GBP.",
+ eurc_deposit: "EURC deposits are available experimentally. Withdrawals are not yet enabled.",
+ },
+ disabledBehaviours: {
+ card_payments: "Card payment rails are not available in this release.",
+ eurc_withdrawal: "EURC withdrawal is not yet supported.",
+ wire_withdrawal: "International wire withdrawal is not yet supported.",
+ },
+ docsHref: "/docs#anchors",
+};
+
+// ─── Query helpers ────────────────────────────────────────────────────────────
+
+/**
+ * Return only the rails in the matrix that match the given state.
+ */
+export function getRailsByState(
+ matrix: AnchorCapabilityMatrix,
+ state: AnchorRailCapabilityState
+): AnchorRailCapability[] {
+ return matrix.rails.filter((r) => r.state === state);
+}
+
+/**
+ * Return only the assets that have deposit enabled.
+ */
+export function getDepositEnabledAssets(
+ matrix: AnchorCapabilityMatrix
+): AnchorAssetCapability[] {
+ return matrix.assets.filter((a) => a.enabled && a.depositEnabled);
+}
+
+/**
+ * Return only the assets that have withdrawal enabled.
+ */
+export function getWithdrawalEnabledAssets(
+ matrix: AnchorCapabilityMatrix
+): AnchorAssetCapability[] {
+ return matrix.assets.filter((a) => a.enabled && a.withdrawalEnabled);
+}
+
+/**
+ * Find a specific rail by its stable `railId`.
+ * Returns `undefined` if no matching rail is found.
+ */
+export function findRailById(
+ matrix: AnchorCapabilityMatrix,
+ railId: string
+): AnchorRailCapability | undefined {
+ return matrix.rails.find((r) => r.railId === railId);
+}
+
+/**
+ * Find a specific asset by its code (case-insensitive).
+ * Returns `undefined` if no matching asset is found.
+ */
+export function findAssetByCode(
+ matrix: AnchorCapabilityMatrix,
+ code: string
+): AnchorAssetCapability | undefined {
+ return matrix.assets.find((a) => a.code.toUpperCase() === code.toUpperCase());
+}
+
+/**
+ * Returns `true` if the given rail supports deposit flows and is not
+ * `unavailable` or `unsupported`.
+ */
+export function isRailDepositReady(
+ matrix: AnchorCapabilityMatrix,
+ railId: string
+): boolean {
+ const rail = findRailById(matrix, railId);
+ if (!rail) return false;
+ return (
+ rail.depositSupported &&
+ rail.state !== "unavailable" &&
+ rail.state !== "unsupported"
+ );
+}
+
+/**
+ * Returns `true` if the given rail supports withdrawal flows and is not
+ * `unavailable` or `unsupported`.
+ */
+export function isRailWithdrawalReady(
+ matrix: AnchorCapabilityMatrix,
+ railId: string
+): boolean {
+ const rail = findRailById(matrix, railId);
+ if (!rail) return false;
+ return (
+ rail.withdrawalSupported &&
+ rail.state !== "unavailable" &&
+ rail.state !== "unsupported"
+ );
+}
diff --git a/packages/config/test/railConfig.test.ts b/packages/config/test/railConfig.test.ts
new file mode 100644
index 0000000..0d188b4
--- /dev/null
+++ b/packages/config/test/railConfig.test.ts
@@ -0,0 +1,162 @@
+/**
+ * Tests for anchor rail configuration and capability matrix helpers (issue #54).
+ */
+
+import { describe, it, expect } from "vitest";
+import {
+ DEFAULT_ANCHOR_CAPABILITY_MATRIX,
+ getRailsByState,
+ getDepositEnabledAssets,
+ getWithdrawalEnabledAssets,
+ findRailById,
+ findAssetByCode,
+ isRailDepositReady,
+ isRailWithdrawalReady,
+} from "../src/railConfig";
+import { ANCHOR_RAIL_CAPABILITY_STATES } from "@anchorkit/types";
+
+describe("DEFAULT_ANCHOR_CAPABILITY_MATRIX", () => {
+ it("has a non-empty anchorName", () => {
+ expect(DEFAULT_ANCHOR_CAPABILITY_MATRIX.anchorName.trim().length).toBeGreaterThan(0);
+ });
+
+ it("has a valid overallState", () => {
+ expect(ANCHOR_RAIL_CAPABILITY_STATES).toContain(
+ DEFAULT_ANCHOR_CAPABILITY_MATRIX.overallState
+ );
+ });
+
+ it("has at least one rail", () => {
+ expect(DEFAULT_ANCHOR_CAPABILITY_MATRIX.rails.length).toBeGreaterThan(0);
+ });
+
+ it("has at least one asset", () => {
+ expect(DEFAULT_ANCHOR_CAPABILITY_MATRIX.assets.length).toBeGreaterThan(0);
+ });
+
+ it("marks isMock as true", () => {
+ expect(DEFAULT_ANCHOR_CAPABILITY_MATRIX.isMock).toBe(true);
+ });
+
+ it("every rail has a valid state", () => {
+ for (const rail of DEFAULT_ANCHOR_CAPABILITY_MATRIX.rails) {
+ expect(ANCHOR_RAIL_CAPABILITY_STATES).toContain(rail.state);
+ }
+ });
+
+ it("every rail has a non-empty currencies and countries list", () => {
+ for (const rail of DEFAULT_ANCHOR_CAPABILITY_MATRIX.rails) {
+ expect(rail.currencies.length).toBeGreaterThan(0);
+ expect(rail.countries.length).toBeGreaterThan(0);
+ }
+ });
+
+ it("every asset has a non-empty code", () => {
+ for (const asset of DEFAULT_ANCHOR_CAPABILITY_MATRIX.assets) {
+ expect(asset.code.trim().length).toBeGreaterThan(0);
+ }
+ });
+
+ it("has no duplicate rail IDs", () => {
+ const ids = DEFAULT_ANCHOR_CAPABILITY_MATRIX.rails.map((r) => r.railId);
+ expect(new Set(ids).size).toBe(ids.length);
+ });
+
+ it("has no duplicate asset codes", () => {
+ const codes = DEFAULT_ANCHOR_CAPABILITY_MATRIX.assets.map((a) => a.code);
+ expect(new Set(codes).size).toBe(codes.length);
+ });
+});
+
+describe("getRailsByState", () => {
+ it("returns only rails matching the given state", () => {
+ const mocks = getRailsByState(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "mock");
+ expect(mocks.length).toBeGreaterThan(0);
+ for (const r of mocks) {
+ expect(r.state).toBe("mock");
+ }
+ });
+
+ it("returns an empty array when no rails match", () => {
+ const result = getRailsByState(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "testnet-only");
+ expect(result).toEqual([]);
+ });
+});
+
+describe("getDepositEnabledAssets", () => {
+ it("returns only enabled assets with depositEnabled true", () => {
+ const assets = getDepositEnabledAssets(DEFAULT_ANCHOR_CAPABILITY_MATRIX);
+ expect(assets.length).toBeGreaterThan(0);
+ for (const a of assets) {
+ expect(a.enabled).toBe(true);
+ expect(a.depositEnabled).toBe(true);
+ }
+ });
+});
+
+describe("getWithdrawalEnabledAssets", () => {
+ it("returns only enabled assets with withdrawalEnabled true", () => {
+ const assets = getWithdrawalEnabledAssets(DEFAULT_ANCHOR_CAPABILITY_MATRIX);
+ expect(assets.length).toBeGreaterThan(0);
+ for (const a of assets) {
+ expect(a.enabled).toBe(true);
+ expect(a.withdrawalEnabled).toBe(true);
+ }
+ });
+});
+
+describe("findRailById", () => {
+ it("returns the matching rail for a known ID", () => {
+ const rail = findRailById(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "SEPA");
+ expect(rail).toBeDefined();
+ expect(rail?.railId).toBe("SEPA");
+ });
+
+ it("returns undefined for an unknown rail ID", () => {
+ expect(findRailById(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "NONEXISTENT")).toBeUndefined();
+ });
+});
+
+describe("findAssetByCode", () => {
+ it("returns the matching asset for a known code (case-insensitive)", () => {
+ const asset = findAssetByCode(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "usdc");
+ expect(asset).toBeDefined();
+ expect(asset?.code).toBe("USDC");
+ });
+
+ it("returns undefined for an unknown asset code", () => {
+ expect(findAssetByCode(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "BOGUS")).toBeUndefined();
+ });
+});
+
+describe("isRailDepositReady", () => {
+ it("returns true for a mock SEPA rail that supports deposit", () => {
+ expect(isRailDepositReady(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "SEPA")).toBe(true);
+ });
+
+ it("returns false for an unavailable CARD rail", () => {
+ expect(isRailDepositReady(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "CARD")).toBe(false);
+ });
+
+ it("returns false for an unknown rail ID", () => {
+ expect(isRailDepositReady(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "NONEXISTENT")).toBe(false);
+ });
+});
+
+describe("isRailWithdrawalReady", () => {
+ it("returns true for a mock ACH rail that supports withdrawal", () => {
+ expect(isRailWithdrawalReady(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "ACH")).toBe(true);
+ });
+
+ it("returns false for the experimental WIRE rail (withdrawal not supported)", () => {
+ expect(isRailWithdrawalReady(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "WIRE")).toBe(false);
+ });
+
+ it("returns false for an unavailable CARD rail", () => {
+ expect(isRailWithdrawalReady(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "CARD")).toBe(false);
+ });
+
+ it("returns false for an unknown rail ID", () => {
+ expect(isRailWithdrawalReady(DEFAULT_ANCHOR_CAPABILITY_MATRIX, "NONEXISTENT")).toBe(false);
+ });
+});
diff --git a/packages/fixtures/src/capabilities.ts b/packages/fixtures/src/capabilities.ts
index dbd67f9..f0c3907 100644
--- a/packages/fixtures/src/capabilities.ts
+++ b/packages/fixtures/src/capabilities.ts
@@ -52,6 +52,12 @@ export const FIXTURES_PACKAGE_CAPABILITIES: PackageCapability = {
state: "implemented",
description: "Intentionally invalid inputs for negative testing of validators and error handling.",
},
+ {
+ id: "rail-capability-fixtures",
+ label: "Rail Capability Matrix Fixtures",
+ state: "implemented",
+ description: "Pre-built AnchorCapabilityMatrix instances covering valid, all-disabled, and experimental-only configurations for deterministic testing.",
+ },
],
docsHref: "/docs#fixtures",
};
diff --git a/packages/fixtures/src/index.ts b/packages/fixtures/src/index.ts
index 12b6d2d..c60e89f 100644
--- a/packages/fixtures/src/index.ts
+++ b/packages/fixtures/src/index.ts
@@ -17,5 +17,8 @@ export * from "./milestoneUi";
export * from "./diagnostics";
export * from "./invalid";
+// ─── Rail capability matrix fixtures (issue #54) ─────────────────────────────
+export * from "./railCapability";
+
// ─── Package capability metadata ────────────────────────────────────────────
export * from "./capabilities";
diff --git a/packages/fixtures/src/railCapability.ts b/packages/fixtures/src/railCapability.ts
new file mode 100644
index 0000000..211e016
--- /dev/null
+++ b/packages/fixtures/src/railCapability.ts
@@ -0,0 +1,164 @@
+/**
+ * Rail capability matrix fixtures (issue #54).
+ *
+ * Pre-built `AnchorCapabilityMatrix` instances for deterministic testing.
+ * These fixtures cover valid configurations, all-disabled rails, and
+ * experimental-only states.
+ */
+
+import type {
+ AnchorCapabilityMatrix,
+ AnchorRailCapability,
+ AnchorAssetCapability,
+} from "@anchorkit/types";
+
+// ─── Reusable rail stubs ──────────────────────────────────────────────────────
+
+export const mockSepaRail: AnchorRailCapability = {
+ railId: "SEPA",
+ name: "SEPA Credit Transfer",
+ state: "mock",
+ depositSupported: true,
+ withdrawalSupported: true,
+ currencies: ["EUR"],
+ countries: ["DE", "FR", "ES"],
+};
+
+export const mockAchRail: AnchorRailCapability = {
+ railId: "ACH",
+ name: "ACH Bank Transfer",
+ state: "mock",
+ depositSupported: true,
+ withdrawalSupported: true,
+ currencies: ["USD"],
+ countries: ["US"],
+};
+
+export const experimentalWireRail: AnchorRailCapability = {
+ railId: "WIRE",
+ name: "International Wire Transfer",
+ state: "experimental",
+ depositSupported: true,
+ withdrawalSupported: false,
+ currencies: ["USD", "EUR"],
+ countries: ["US", "DE"],
+ note: "Experimental deposit-only wire rail.",
+};
+
+export const unavailableCardRail: AnchorRailCapability = {
+ railId: "CARD",
+ name: "Card Payment",
+ state: "unavailable",
+ depositSupported: false,
+ withdrawalSupported: false,
+ currencies: ["USD"],
+ countries: ["US"],
+ note: "Card rails are not available.",
+};
+
+// ─── Reusable asset stubs ─────────────────────────────────────────────────────
+
+export const mockXlmAsset: AnchorAssetCapability = {
+ code: "XLM",
+ issuer: null,
+ enabled: true,
+ depositEnabled: true,
+ withdrawalEnabled: true,
+ depositMinAmount: "10.0000000",
+ depositMaxAmount: "100000.0000000",
+ withdrawalMinAmount: "10.0000000",
+ withdrawalMaxAmount: "100000.0000000",
+ feeFixed: "1.5000000",
+ feePercent: "0.1",
+};
+
+export const mockUsdcAsset: AnchorAssetCapability = {
+ code: "USDC",
+ issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
+ enabled: true,
+ depositEnabled: true,
+ withdrawalEnabled: true,
+ depositMinAmount: "5.00",
+ depositMaxAmount: "50000.00",
+ withdrawalMinAmount: "5.00",
+ withdrawalMaxAmount: "50000.00",
+ feeFixed: "0.50",
+ feePercent: "0.2",
+};
+
+export const disabledEurcAsset: AnchorAssetCapability = {
+ code: "EURC",
+ issuer: "GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP",
+ enabled: true,
+ depositEnabled: true,
+ withdrawalEnabled: false,
+ depositMinAmount: "5.00",
+ depositMaxAmount: "25000.00",
+ note: "EURC withdrawal is not yet enabled.",
+};
+
+// ─── Full matrix fixtures ─────────────────────────────────────────────────────
+
+/**
+ * A complete, valid mock anchor capability matrix covering all rail states and
+ * typical deposit/withdrawal asset configurations.
+ */
+export const mockAnchorCapabilityMatrix: AnchorCapabilityMatrix = {
+ anchorName: "Mock Anchor (fixture)",
+ overallState: "mock",
+ isMock: true,
+ depositState: "mock",
+ withdrawalState: "mock",
+ rails: [mockSepaRail, mockAchRail, experimentalWireRail, unavailableCardRail],
+ assets: [mockXlmAsset, mockUsdcAsset, disabledEurcAsset],
+ experimentalBehaviours: {
+ wire_deposit: "Wire deposit is experimental.",
+ eurc_deposit: "EURC deposit is experimental.",
+ },
+ disabledBehaviours: {
+ card_payments: "Card rails unavailable.",
+ eurc_withdrawal: "EURC withdrawal not yet supported.",
+ wire_withdrawal: "Wire withdrawal not yet supported.",
+ },
+ docsHref: "/docs#anchors",
+};
+
+/**
+ * A capability matrix where every rail is `unavailable` — used to test
+ * that UI correctly renders a fully-disabled anchor.
+ */
+export const allDisabledRailsMatrix: AnchorCapabilityMatrix = {
+ anchorName: "Disabled Anchor (fixture)",
+ overallState: "unavailable",
+ isMock: true,
+ depositState: "unavailable",
+ withdrawalState: "unavailable",
+ rails: [
+ { ...mockSepaRail, state: "unavailable", depositSupported: false, withdrawalSupported: false },
+ { ...mockAchRail, state: "unavailable", depositSupported: false, withdrawalSupported: false },
+ ],
+ assets: [
+ { ...mockXlmAsset, enabled: false, depositEnabled: false, withdrawalEnabled: false },
+ { ...mockUsdcAsset, enabled: false, depositEnabled: false, withdrawalEnabled: false },
+ ],
+ disabledBehaviours: {
+ all_rails: "All rails are disabled in this fixture.",
+ },
+};
+
+/**
+ * A capability matrix with only experimental rails — useful for testing that
+ * `experimental` state is surfaced correctly in the UI.
+ */
+export const experimentalOnlyMatrix: AnchorCapabilityMatrix = {
+ anchorName: "Experimental Anchor (fixture)",
+ overallState: "experimental",
+ isMock: true,
+ depositState: "experimental",
+ withdrawalState: "unavailable",
+ rails: [experimentalWireRail],
+ assets: [disabledEurcAsset],
+ experimentalBehaviours: {
+ wire_deposit: "Wire deposit is experimental.",
+ },
+};
diff --git a/packages/fixtures/test/railCapability.test.ts b/packages/fixtures/test/railCapability.test.ts
new file mode 100644
index 0000000..b40777d
--- /dev/null
+++ b/packages/fixtures/test/railCapability.test.ts
@@ -0,0 +1,151 @@
+/**
+ * Tests for rail capability matrix fixtures (issue #54).
+ */
+
+import { describe, it, expect } from "vitest";
+import {
+ mockSepaRail,
+ mockAchRail,
+ experimentalWireRail,
+ unavailableCardRail,
+ mockXlmAsset,
+ mockUsdcAsset,
+ disabledEurcAsset,
+ mockAnchorCapabilityMatrix,
+ allDisabledRailsMatrix,
+ experimentalOnlyMatrix,
+} from "../src/railCapability";
+import { ANCHOR_RAIL_CAPABILITY_STATES } from "@anchorkit/types";
+
+// ─── Rail fixtures ────────────────────────────────────────────────────────────
+
+describe("mockSepaRail", () => {
+ it("has railId SEPA and state mock", () => {
+ expect(mockSepaRail.railId).toBe("SEPA");
+ expect(mockSepaRail.state).toBe("mock");
+ });
+
+ it("supports deposit and withdrawal", () => {
+ expect(mockSepaRail.depositSupported).toBe(true);
+ expect(mockSepaRail.withdrawalSupported).toBe(true);
+ });
+});
+
+describe("experimentalWireRail", () => {
+ it("has state experimental", () => {
+ expect(experimentalWireRail.state).toBe("experimental");
+ });
+
+ it("supports deposit but not withdrawal", () => {
+ expect(experimentalWireRail.depositSupported).toBe(true);
+ expect(experimentalWireRail.withdrawalSupported).toBe(false);
+ });
+});
+
+describe("unavailableCardRail", () => {
+ it("has state unavailable", () => {
+ expect(unavailableCardRail.state).toBe("unavailable");
+ });
+
+ it("does not support deposit or withdrawal", () => {
+ expect(unavailableCardRail.depositSupported).toBe(false);
+ expect(unavailableCardRail.withdrawalSupported).toBe(false);
+ });
+});
+
+// ─── Asset fixtures ────────────────────────────────────────────────────────────
+
+describe("mockXlmAsset", () => {
+ it("has code XLM and null issuer", () => {
+ expect(mockXlmAsset.code).toBe("XLM");
+ expect(mockXlmAsset.issuer).toBeNull();
+ });
+
+ it("is enabled for deposit and withdrawal", () => {
+ expect(mockXlmAsset.depositEnabled).toBe(true);
+ expect(mockXlmAsset.withdrawalEnabled).toBe(true);
+ });
+});
+
+describe("disabledEurcAsset", () => {
+ it("has depositEnabled true but withdrawalEnabled false", () => {
+ expect(disabledEurcAsset.depositEnabled).toBe(true);
+ expect(disabledEurcAsset.withdrawalEnabled).toBe(false);
+ });
+});
+
+// ─── Matrix fixtures ─────────────────────────────────────────────────────────
+
+describe("mockAnchorCapabilityMatrix", () => {
+ it("has a non-empty anchor name", () => {
+ expect(mockAnchorCapabilityMatrix.anchorName.trim().length).toBeGreaterThan(0);
+ });
+
+ it("has isMock true", () => {
+ expect(mockAnchorCapabilityMatrix.isMock).toBe(true);
+ });
+
+ it("has 4 rails", () => {
+ expect(mockAnchorCapabilityMatrix.rails).toHaveLength(4);
+ });
+
+ it("has 3 assets", () => {
+ expect(mockAnchorCapabilityMatrix.assets).toHaveLength(3);
+ });
+
+ it("every rail has a valid state", () => {
+ for (const rail of mockAnchorCapabilityMatrix.rails) {
+ expect(ANCHOR_RAIL_CAPABILITY_STATES).toContain(rail.state);
+ }
+ });
+
+ it("has no duplicate rail IDs", () => {
+ const ids = mockAnchorCapabilityMatrix.rails.map((r) => r.railId);
+ expect(new Set(ids).size).toBe(ids.length);
+ });
+
+ it("has experimental and disabled behaviour records", () => {
+ expect(mockAnchorCapabilityMatrix.experimentalBehaviours).toBeDefined();
+ expect(mockAnchorCapabilityMatrix.disabledBehaviours).toBeDefined();
+ expect(
+ Object.keys(mockAnchorCapabilityMatrix.experimentalBehaviours!).length
+ ).toBeGreaterThan(0);
+ expect(
+ Object.keys(mockAnchorCapabilityMatrix.disabledBehaviours!).length
+ ).toBeGreaterThan(0);
+ });
+});
+
+describe("allDisabledRailsMatrix", () => {
+ it("has overallState unavailable", () => {
+ expect(allDisabledRailsMatrix.overallState).toBe("unavailable");
+ });
+
+ it("has all rails with depositSupported false", () => {
+ for (const rail of allDisabledRailsMatrix.rails) {
+ expect(rail.depositSupported).toBe(false);
+ expect(rail.withdrawalSupported).toBe(false);
+ }
+ });
+
+ it("has all assets disabled", () => {
+ for (const asset of allDisabledRailsMatrix.assets) {
+ expect(asset.enabled).toBe(false);
+ }
+ });
+});
+
+describe("experimentalOnlyMatrix", () => {
+ it("has overallState experimental", () => {
+ expect(experimentalOnlyMatrix.overallState).toBe("experimental");
+ });
+
+ it("has exactly one rail", () => {
+ expect(experimentalOnlyMatrix.rails).toHaveLength(1);
+ });
+
+ it("the single rail is the experimental wire rail", () => {
+ expect(experimentalOnlyMatrix.rails[0]!.railId).toBe("WIRE");
+ expect(experimentalOnlyMatrix.rails[0]!.state).toBe("experimental");
+ });
+});
diff --git a/packages/types/src/capabilities.ts b/packages/types/src/capabilities.ts
index 0098b6c..8d4ab56 100644
--- a/packages/types/src/capabilities.ts
+++ b/packages/types/src/capabilities.ts
@@ -40,6 +40,12 @@ export const TYPES_PACKAGE_CAPABILITIES: PackageCapability = {
state: "implemented",
description: "AssetDisplayInfo, AssetDisplayMetadata, and AssetDisplayState types for registry-based asset resolution.",
},
+ {
+ id: "rail-capability-types",
+ label: "Rail Capability Matrix Types",
+ state: "implemented",
+ description: "AnchorRailCapabilityState, AnchorRailCapability, AnchorAssetCapability, and AnchorCapabilityMatrix types for the anchor rails capability model.",
+ },
],
docsHref: "/docs#types",
};
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index 3d67a6b..59d6d9d 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -633,6 +633,9 @@ export * from "./errors";
// ─── Package capability metadata ────────────────────────────────────────────
export * from "./capabilities";
+// ─── Anchor rails capability matrix ─────────────────────────────────────────
+export * from "./railCapability";
+
// ─── Capability states ──────────────────────────────────────────────────────
export type CapabilityState =
| "implemented"
diff --git a/packages/types/src/railCapability.ts b/packages/types/src/railCapability.ts
new file mode 100644
index 0000000..61afc3d
--- /dev/null
+++ b/packages/types/src/railCapability.ts
@@ -0,0 +1,137 @@
+/**
+ * Anchor Rails Capability Matrix types (issue #54).
+ *
+ * These types describe which payment rails, assets, deposit flows, and
+ * withdrawal flows are available, disabled, or experimental for an anchor.
+ * They are designed to be consumed by the web dashboard, test fixtures,
+ * and validators without creating circular dependencies.
+ */
+
+import type { CapabilityState } from "./index";
+
+// ─── Rail capability ─────────────────────────────────────────────────────────
+
+/**
+ * A single rail's capability state.
+ * Extends `CapabilityState` with an `"unsupported"` value specifically for
+ * rails that are structurally known but explicitly not offered by this anchor.
+ */
+export type AnchorRailCapabilityState = CapabilityState | "unsupported";
+
+export const ANCHOR_RAIL_CAPABILITY_STATES: readonly AnchorRailCapabilityState[] = [
+ "implemented",
+ "mock",
+ "testnet-only",
+ "experimental",
+ "unavailable",
+ "unsupported",
+] as const;
+
+/**
+ * Capability descriptor for a single payment rail offered (or not) by an anchor.
+ */
+export interface AnchorRailCapability {
+ /** Stable rail identifier, e.g. "SEPA", "ACH", "WIRE". */
+ railId: string;
+ /** Human-readable display name. */
+ name: string;
+ /** Readiness state for this rail. */
+ state: AnchorRailCapabilityState;
+ /** Whether deposit flows are supported on this rail. */
+ depositSupported: boolean;
+ /** Whether withdrawal flows are supported on this rail. */
+ withdrawalSupported: boolean;
+ /** ISO 4217 currency codes accepted on this rail. */
+ currencies: string[];
+ /** ISO 3166-1 alpha-2 country codes where this rail operates. */
+ countries: string[];
+ /**
+ * Optional note for experimental or disabled rails explaining why the
+ * state is not `implemented`.
+ */
+ note?: string;
+}
+
+// ─── Asset capability ─────────────────────────────────────────────────────────
+
+/**
+ * Capability descriptor for a supported anchor asset.
+ */
+export interface AnchorAssetCapability {
+ /** Asset code (e.g. "USDC", "XLM"). */
+ code: string;
+ /** Issuer public key — `null` for native XLM. */
+ issuer: string | null;
+ /** Whether the asset is active. */
+ enabled: boolean;
+ /** Whether deposit is available for this asset. */
+ depositEnabled: boolean;
+ /** Whether withdrawal is available for this asset. */
+ withdrawalEnabled: boolean;
+ /**
+ * Minimum deposit amount as a decimal string.
+ * Undefined means no minimum is enforced.
+ */
+ depositMinAmount?: string;
+ /**
+ * Maximum deposit amount as a decimal string.
+ * Undefined means no maximum is enforced.
+ */
+ depositMaxAmount?: string;
+ /**
+ * Minimum withdrawal amount as a decimal string.
+ * Undefined means no minimum is enforced.
+ */
+ withdrawalMinAmount?: string;
+ /**
+ * Maximum withdrawal amount as a decimal string.
+ * Undefined means no maximum is enforced.
+ */
+ withdrawalMaxAmount?: string;
+ /** Fixed fee as a decimal string, if any. */
+ feeFixed?: string;
+ /** Percentage fee (0–100), if any. */
+ feePercent?: string;
+ /**
+ * Optional note for disabled or experimental assets.
+ */
+ note?: string;
+}
+
+// ─── Capability matrix ────────────────────────────────────────────────────────
+
+/**
+ * The full capability matrix for an anchor integration.
+ *
+ * Aggregates rail capabilities, asset capabilities, and top-level deposit
+ * and withdrawal readiness into a single queryable structure. The web
+ * dashboard renders this as a capability card on the Anchors page.
+ */
+export interface AnchorCapabilityMatrix {
+ /** Human-readable anchor name (e.g. "Mock Anchor", "Circle USDC Anchor"). */
+ anchorName: string;
+ /** Overall readiness state of the anchor integration. */
+ overallState: AnchorRailCapabilityState;
+ /** Whether this matrix entry represents a mock/demo anchor. */
+ isMock: boolean;
+ /** Top-level deposit flow readiness. */
+ depositState: AnchorRailCapabilityState;
+ /** Top-level withdrawal flow readiness. */
+ withdrawalState: AnchorRailCapabilityState;
+ /** Payment rails supported by this anchor. */
+ rails: AnchorRailCapability[];
+ /** Assets supported by this anchor. */
+ assets: AnchorAssetCapability[];
+ /**
+ * Any experimental behaviours that are not yet fully supported.
+ * Keyed by feature id; value is a human-readable description.
+ */
+ experimentalBehaviours?: Record;
+ /**
+ * Any explicitly disabled behaviours.
+ * Keyed by feature id; value is the reason it is disabled.
+ */
+ disabledBehaviours?: Record;
+ /** Docs link for this anchor's capability page. */
+ docsHref?: string;
+}
diff --git a/packages/types/test/railCapability.test.ts b/packages/types/test/railCapability.test.ts
new file mode 100644
index 0000000..07d9055
--- /dev/null
+++ b/packages/types/test/railCapability.test.ts
@@ -0,0 +1,30 @@
+/**
+ * Tests for anchor rail capability types and constants (issue #54).
+ * Validates `ANCHOR_RAIL_CAPABILITY_STATES` and the exported type contracts.
+ */
+
+import { describe, it, expect } from "vitest";
+import { ANCHOR_RAIL_CAPABILITY_STATES, CAPABILITY_STATES } from "@anchorkit/types";
+
+describe("ANCHOR_RAIL_CAPABILITY_STATES", () => {
+ it("is a non-empty readonly array", () => {
+ expect(Array.isArray(ANCHOR_RAIL_CAPABILITY_STATES)).toBe(true);
+ expect(ANCHOR_RAIL_CAPABILITY_STATES.length).toBeGreaterThan(0);
+ });
+
+ it("contains all base CAPABILITY_STATES values", () => {
+ for (const state of CAPABILITY_STATES) {
+ expect(ANCHOR_RAIL_CAPABILITY_STATES).toContain(state);
+ }
+ });
+
+ it("additionally contains 'unsupported'", () => {
+ expect(ANCHOR_RAIL_CAPABILITY_STATES).toContain("unsupported");
+ });
+
+ it("has no duplicate entries", () => {
+ expect(new Set(ANCHOR_RAIL_CAPABILITY_STATES).size).toBe(
+ ANCHOR_RAIL_CAPABILITY_STATES.length
+ );
+ });
+});
diff --git a/packages/validators/src/capabilities.ts b/packages/validators/src/capabilities.ts
index 622ac3d..f7e32ba 100644
--- a/packages/validators/src/capabilities.ts
+++ b/packages/validators/src/capabilities.ts
@@ -34,6 +34,12 @@ export const VALIDATORS_PACKAGE_CAPABILITIES: PackageCapability = {
state: "implemented",
description: "Uniform ValidationResult type and engine validators that never throw, with mapped error codes.",
},
+ {
+ id: "rail-capability-schemas",
+ label: "Rail Capability Schemas",
+ state: "implemented",
+ description: "Zod schemas for AnchorRailCapability, AnchorAssetCapability, and AnchorCapabilityMatrix.",
+ },
],
docsHref: "/docs#validators",
};
diff --git a/packages/validators/src/index.ts b/packages/validators/src/index.ts
index 0e19257..e3d0cba 100644
--- a/packages/validators/src/index.ts
+++ b/packages/validators/src/index.ts
@@ -6,6 +6,7 @@ export * from "./schemas/anchor";
export * from "./schemas/escrow";
export * from "./schemas/milestoneUi";
export * from "./schemas/receipt";
+export * from "./schemas/railCapability";
// ─── Validation engine (issue #6) ───────────────────────────────────────────
export * from "./validationEngine";
diff --git a/packages/validators/src/schemas/railCapability.ts b/packages/validators/src/schemas/railCapability.ts
new file mode 100644
index 0000000..cfb88d9
--- /dev/null
+++ b/packages/validators/src/schemas/railCapability.ts
@@ -0,0 +1,57 @@
+/**
+ * Zod schemas for anchor rails capability matrix (issue #54).
+ */
+
+import { z } from "zod";
+import { CAPABILITY_STATES, ANCHOR_RAIL_CAPABILITY_STATES } from "@anchorkit/types";
+
+export const AnchorRailCapabilityStateSchema = z.enum(
+ ANCHOR_RAIL_CAPABILITY_STATES as [string, ...string[]]
+);
+
+export const CapabilityStateSchema = z.enum(
+ CAPABILITY_STATES as [string, ...string[]]
+);
+
+export const AnchorRailCapabilitySchema = z.object({
+ railId: z.string().min(1),
+ name: z.string().min(1),
+ state: AnchorRailCapabilityStateSchema,
+ depositSupported: z.boolean(),
+ withdrawalSupported: z.boolean(),
+ currencies: z.array(z.string().min(1)).min(1),
+ countries: z.array(z.string().length(2)).min(1),
+ note: z.string().optional(),
+});
+
+export const AnchorAssetCapabilitySchema = z.object({
+ code: z.string().min(1).max(12),
+ issuer: z.string().nullable(),
+ enabled: z.boolean(),
+ depositEnabled: z.boolean(),
+ withdrawalEnabled: z.boolean(),
+ depositMinAmount: z.string().optional(),
+ depositMaxAmount: z.string().optional(),
+ withdrawalMinAmount: z.string().optional(),
+ withdrawalMaxAmount: z.string().optional(),
+ feeFixed: z.string().optional(),
+ feePercent: z.string().optional(),
+ note: z.string().optional(),
+});
+
+export const AnchorCapabilityMatrixSchema = z.object({
+ anchorName: z.string().min(1),
+ overallState: AnchorRailCapabilityStateSchema,
+ isMock: z.boolean(),
+ depositState: AnchorRailCapabilityStateSchema,
+ withdrawalState: AnchorRailCapabilityStateSchema,
+ rails: z.array(AnchorRailCapabilitySchema),
+ assets: z.array(AnchorAssetCapabilitySchema),
+ experimentalBehaviours: z.record(z.string()).optional(),
+ disabledBehaviours: z.record(z.string()).optional(),
+ docsHref: z.string().optional(),
+});
+
+export type ParsedAnchorRailCapability = z.infer;
+export type ParsedAnchorAssetCapability = z.infer;
+export type ParsedAnchorCapabilityMatrix = z.infer;
diff --git a/packages/validators/test/railCapability.test.ts b/packages/validators/test/railCapability.test.ts
new file mode 100644
index 0000000..6c5f690
--- /dev/null
+++ b/packages/validators/test/railCapability.test.ts
@@ -0,0 +1,187 @@
+/**
+ * Tests for anchor rail capability matrix Zod schemas (issue #54).
+ */
+
+import { describe, it, expect } from "vitest";
+import {
+ AnchorRailCapabilitySchema,
+ AnchorAssetCapabilitySchema,
+ AnchorCapabilityMatrixSchema,
+} from "../src/schemas/railCapability";
+
+// ─── Valid stubs ─────────────────────────────────────────────────────────────
+
+const validRail = {
+ railId: "SEPA",
+ name: "SEPA Credit Transfer",
+ state: "mock",
+ depositSupported: true,
+ withdrawalSupported: true,
+ currencies: ["EUR"],
+ countries: ["DE"],
+};
+
+const validAsset = {
+ code: "USDC",
+ issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
+ enabled: true,
+ depositEnabled: true,
+ withdrawalEnabled: true,
+ feeFixed: "0.50",
+ feePercent: "0.2",
+};
+
+const validNativeAsset = {
+ code: "XLM",
+ issuer: null,
+ enabled: true,
+ depositEnabled: true,
+ withdrawalEnabled: true,
+};
+
+const validMatrix = {
+ anchorName: "Test Anchor",
+ overallState: "mock",
+ isMock: true,
+ depositState: "mock",
+ withdrawalState: "mock",
+ rails: [validRail],
+ assets: [validAsset],
+};
+
+// ─── AnchorRailCapabilitySchema ───────────────────────────────────────────────
+
+describe("AnchorRailCapabilitySchema", () => {
+ it("accepts a valid mock rail", () => {
+ expect(AnchorRailCapabilitySchema.safeParse(validRail).success).toBe(true);
+ });
+
+ it("accepts an unavailable rail", () => {
+ const rail = { ...validRail, state: "unavailable", depositSupported: false, withdrawalSupported: false };
+ expect(AnchorRailCapabilitySchema.safeParse(rail).success).toBe(true);
+ });
+
+ it("accepts an experimental rail with a note", () => {
+ const rail = { ...validRail, state: "experimental", note: "Experimental only." };
+ expect(AnchorRailCapabilitySchema.safeParse(rail).success).toBe(true);
+ });
+
+ it("accepts the 'unsupported' state", () => {
+ const rail = { ...validRail, state: "unsupported" };
+ expect(AnchorRailCapabilitySchema.safeParse(rail).success).toBe(true);
+ });
+
+ it("rejects a rail with an empty railId", () => {
+ const bad = { ...validRail, railId: "" };
+ expect(AnchorRailCapabilitySchema.safeParse(bad).success).toBe(false);
+ });
+
+ it("rejects a rail with an empty currencies array", () => {
+ const bad = { ...validRail, currencies: [] };
+ expect(AnchorRailCapabilitySchema.safeParse(bad).success).toBe(false);
+ });
+
+ it("rejects a rail with an empty countries array", () => {
+ const bad = { ...validRail, countries: [] };
+ expect(AnchorRailCapabilitySchema.safeParse(bad).success).toBe(false);
+ });
+
+ it("rejects a rail with an invalid state", () => {
+ const bad = { ...validRail, state: "unknown-state" };
+ expect(AnchorRailCapabilitySchema.safeParse(bad).success).toBe(false);
+ });
+
+ it("rejects a rail missing required fields", () => {
+ expect(AnchorRailCapabilitySchema.safeParse({ railId: "SEPA" }).success).toBe(false);
+ });
+});
+
+// ─── AnchorAssetCapabilitySchema ──────────────────────────────────────────────
+
+describe("AnchorAssetCapabilitySchema", () => {
+ it("accepts a valid issued asset", () => {
+ expect(AnchorAssetCapabilitySchema.safeParse(validAsset).success).toBe(true);
+ });
+
+ it("accepts a native asset with null issuer", () => {
+ expect(AnchorAssetCapabilitySchema.safeParse(validNativeAsset).success).toBe(true);
+ });
+
+ it("accepts an asset with optional amount bounds and note", () => {
+ const asset = {
+ ...validAsset,
+ depositMinAmount: "5.00",
+ depositMaxAmount: "50000.00",
+ withdrawalMinAmount: "5.00",
+ withdrawalMaxAmount: "50000.00",
+ note: "Some note.",
+ };
+ expect(AnchorAssetCapabilitySchema.safeParse(asset).success).toBe(true);
+ });
+
+ it("rejects an asset with an empty code", () => {
+ const bad = { ...validAsset, code: "" };
+ expect(AnchorAssetCapabilitySchema.safeParse(bad).success).toBe(false);
+ });
+
+ it("rejects an asset with a code longer than 12 chars", () => {
+ const bad = { ...validAsset, code: "VERYLONGCODE1" };
+ expect(AnchorAssetCapabilitySchema.safeParse(bad).success).toBe(false);
+ });
+
+ it("rejects an asset missing required boolean fields", () => {
+ const bad = { code: "XLM", issuer: null };
+ expect(AnchorAssetCapabilitySchema.safeParse(bad).success).toBe(false);
+ });
+});
+
+// ─── AnchorCapabilityMatrixSchema ─────────────────────────────────────────────
+
+describe("AnchorCapabilityMatrixSchema", () => {
+ it("accepts a minimal valid matrix", () => {
+ expect(AnchorCapabilityMatrixSchema.safeParse(validMatrix).success).toBe(true);
+ });
+
+ it("accepts a matrix with experimental and disabled behaviours", () => {
+ const full = {
+ ...validMatrix,
+ experimentalBehaviours: { wire_deposit: "Experimental wire deposit." },
+ disabledBehaviours: { card_payments: "Cards unavailable." },
+ docsHref: "/docs#anchors",
+ };
+ expect(AnchorCapabilityMatrixSchema.safeParse(full).success).toBe(true);
+ });
+
+ it("accepts an empty rails array", () => {
+ const m = { ...validMatrix, rails: [] };
+ expect(AnchorCapabilityMatrixSchema.safeParse(m).success).toBe(true);
+ });
+
+ it("accepts an empty assets array", () => {
+ const m = { ...validMatrix, assets: [] };
+ expect(AnchorCapabilityMatrixSchema.safeParse(m).success).toBe(true);
+ });
+
+ it("rejects a matrix with an empty anchor name", () => {
+ const bad = { ...validMatrix, anchorName: "" };
+ expect(AnchorCapabilityMatrixSchema.safeParse(bad).success).toBe(false);
+ });
+
+ it("rejects a matrix with an invalid overallState", () => {
+ const bad = { ...validMatrix, overallState: "bogus" };
+ expect(AnchorCapabilityMatrixSchema.safeParse(bad).success).toBe(false);
+ });
+
+ it("rejects a matrix with an invalid rail inside rails array", () => {
+ const bad = {
+ ...validMatrix,
+ rails: [{ railId: "", name: "Bad", state: "mock", depositSupported: true, withdrawalSupported: true, currencies: [], countries: [] }],
+ };
+ expect(AnchorCapabilityMatrixSchema.safeParse(bad).success).toBe(false);
+ });
+
+ it("rejects when anchorName is missing", () => {
+ const { anchorName: _, ...bad } = validMatrix;
+ expect(AnchorCapabilityMatrixSchema.safeParse(bad).success).toBe(false);
+ });
+});