diff --git a/src/__tests__/components/TransferAdminModal.test.tsx b/src/__tests__/components/TransferAdminModal.test.tsx
new file mode 100644
index 00000000..ad294ea5
--- /dev/null
+++ b/src/__tests__/components/TransferAdminModal.test.tsx
@@ -0,0 +1,69 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import { StrKey } from "@stellar/stellar-sdk";
+import TransferAdminModal from "@/components/TransferAdminModal";
+
+// The modal reads a few labels from the "Admin" namespace; everything else
+// arrives via props. Mirror the real translations so assertions read naturally.
+jest.mock("next-intl", () => ({
+ useTranslations: () => (key: string) => {
+ const map: Record = {
+ requiredWord: "CONFIRM",
+ invalidAddress: "Invalid address",
+ newAdminAddress: "New Admin Address",
+ transferring: "Transferring…",
+ };
+ return map[key] ?? key;
+ },
+}));
+
+// A guaranteed well-formed Stellar public key (correct prefix, length, checksum).
+const VALID_ADDRESS = StrKey.encodeEd25519PublicKey(Buffer.alloc(32, 7));
+const INVALID_ADDRESS = "GNOTAREALSTELLARADDRESS";
+
+const baseProps = {
+ isOpen: true,
+ isTransferring: false,
+ onClose: jest.fn(),
+ title: "Transfer admin",
+ body: "Confirm the transfer.",
+ confirmLabel: "Type CONFIRM to proceed",
+ typeConfirmPlaceholder: "CONFIRM",
+ cancelLabel: "Cancel",
+ confirmButtonLabel: "Transfer",
+};
+
+describe("TransferAdminModal address validation", () => {
+ it("accepts a valid Stellar public key: no error, submit enabled after typing CONFIRM", () => {
+ const onConfirm = jest.fn();
+ render(
+ ,
+ );
+
+ expect(screen.queryByText("Invalid address")).not.toBeInTheDocument();
+
+ const confirmButton = screen.getByRole("button", { name: "Transfer" });
+ expect(confirmButton).toBeDisabled();
+
+ fireEvent.change(screen.getByPlaceholderText("CONFIRM"), { target: { value: "CONFIRM" } });
+ expect(confirmButton).toBeEnabled();
+
+ fireEvent.click(confirmButton);
+ expect(onConfirm).toHaveBeenCalledTimes(1);
+ });
+
+ it("rejects an invalid address: shows an inline error and keeps submit disabled", () => {
+ const onConfirm = jest.fn();
+ render(
+ ,
+ );
+
+ expect(screen.getByRole("alert")).toHaveTextContent("Invalid address");
+
+ const confirmButton = screen.getByRole("button", { name: "Transfer" });
+ fireEvent.change(screen.getByPlaceholderText("CONFIRM"), { target: { value: "CONFIRM" } });
+ expect(confirmButton).toBeDisabled();
+
+ fireEvent.click(confirmButton);
+ expect(onConfirm).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/__tests__/unit/validators.test.ts b/src/__tests__/unit/validators.test.ts
index 472c9bea..6cea0744 100644
--- a/src/__tests__/unit/validators.test.ts
+++ b/src/__tests__/unit/validators.test.ts
@@ -1,5 +1,7 @@
+import { StrKey } from "@stellar/stellar-sdk";
import {
validateStellarAddress,
+ isValidStellarPublicKey,
validateAmount,
validateFundingGoal,
validateDuration,
@@ -19,6 +21,19 @@ describe("Validators", () => {
expect(() => validateStellarAddress("G" + "A".repeat(55))).not.toThrow();
});
+ it("isValidStellarPublicKey", () => {
+ const valid = StrKey.encodeEd25519PublicKey(Buffer.alloc(32, 7));
+ expect(isValidStellarPublicKey(valid)).toBe(true);
+ // Tolerates surrounding whitespace from copy/paste.
+ expect(isValidStellarPublicKey(` ${valid} `)).toBe(true);
+
+ // Wrong prefix, too short, bad checksum, and empty all fail.
+ expect(isValidStellarPublicKey("")).toBe(false);
+ expect(isValidStellarPublicKey("GABC")).toBe(false);
+ expect(isValidStellarPublicKey("G" + "A".repeat(55))).toBe(false);
+ expect(isValidStellarPublicKey("MNOTAVALIDADDRESS")).toBe(false);
+ });
+
it("validateAmount", () => {
expect(() => validateAmount(0)).toThrow(
new ContractErrorException(ContractError.ContributionMustBePositive),
diff --git a/src/components/TransferAdminModal.tsx b/src/components/TransferAdminModal.tsx
index 5037bd73..b2a24cde 100644
--- a/src/components/TransferAdminModal.tsx
+++ b/src/components/TransferAdminModal.tsx
@@ -2,6 +2,7 @@
import { useEffect, useRef, useState } from "react";
import { useTranslations } from "next-intl";
+import { isValidStellarPublicKey } from "@/utils/validators";
interface TransferAdminModalProps {
newAdminAddress?: string;
@@ -44,7 +45,11 @@ export default function TransferAdminModal({
const [confirmInput, setConfirmInput] = useState("");
const requiredWord = t("requiredWord");
- const canConfirm = confirmInput.trim() === requiredWord;
+ // When an address is supplied, it must be a well-formed Stellar public key
+ // before submit is allowed. This catches invalid input client-side instead of
+ // failing only after a Freighter signature prompt and a wasted transaction.
+ const isAddressValid = !newAdminAddress || isValidStellarPublicKey(newAdminAddress);
+ const canConfirm = confirmInput.trim() === requiredWord && isAddressValid;
// Reset input and focus when modal opens
useEffect(() => {
@@ -114,6 +119,11 @@ export default function TransferAdminModal({
) : null}
+ {newAdminAddress && !isAddressValid ? (
+
+ {t("invalidAddress")}
+
+ ) : null}