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}