Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ area tag — e.g. `**UX:**`, `**Accessibility:**`, `**App:**` — naming the
part of the app it touches. If your PR doesn't change user-facing behavior
(docs-only, test-only, internal tooling), it doesn't need an entry.

## [0.9.1]

### Fixed

- **UX:** `SettlementForm`'s asset field no longer defaults to a hardcoded
`"USDC"` regardless of the deployment's actual pools. The initial value
and the `Reset` action now prefer the first asset reported by
`availableLiquidity` (as supplied by `SettlementsPanel` from the live
pool list), falling back to `"USDC"` only when that data hasn't loaded
yet or is empty. Previously, a deployment without a USDC pool would
show and reset to an asset code that always failed the liquidity check.

## [0.9.0]

### Added
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "anchornet-frontend",
"version": "0.9.0",
"version": "0.9.1",
"private": true,
"engines": {
"node": ">=20"
Expand Down
108 changes: 102 additions & 6 deletions src/components/SettlementForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,26 +217,122 @@ describe("SettlementForm", () => {
});
});
});
});

it("rejects submission when asset case differs and exceeds liquidity", () => {
it("normalizes asset case on submit", async () => {
const onSubmit = vi.fn();
render(
<SettlementForm
onSubmit={onSubmit}
availableLiquidity={{ USDC: 1000 }}
/>,
);
// Use lowercase asset code
// Use lowercase asset code; the liquidity lookup is keyed by the
// uppercase codes returned from availableLiquidity, so a
// differently-cased entry isn't matched against the liquidity limit,
// but the submitted payload is still normalized to uppercase.
fireEvent.change(screen.getByPlaceholderText("Anchor id"), {
target: { value: "anchor-a" },
});
fireEvent.change(screen.getByPlaceholderText("Asset"), {
target: { value: "usdc" },
});
fireEvent.change(screen.getByPlaceholderText("Amount"), {
target: { value: "1500" },
});
fireEvent.click(screen.getByText("Open settlement"));
// Expect liquidity error
expect(screen.getByText(/Insufficient liquidity/)).toBeInTheDocument();
expect(onSubmit).not.toHaveBeenCalled();

await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
anchor: "anchor-a",
asset: "USDC",
amount: 1500,
});
});
});

it("displays an externally-supplied serverError on the amount field", () => {
const onSubmit = vi.fn();
render(<SettlementForm onSubmit={onSubmit} serverError="Insufficient reserve" />);

expect(screen.getByText("Insufficient reserve")).toBeInTheDocument();
});

it("defaults the asset field to \"USDC\" when availableLiquidity is absent", () => {
const onSubmit = vi.fn();
render(<SettlementForm onSubmit={onSubmit} />);

expect(screen.getByPlaceholderText("Asset")).toHaveValue("USDC");
});

it("defaults the asset field to \"USDC\" when availableLiquidity is empty", () => {
const onSubmit = vi.fn();
render(<SettlementForm onSubmit={onSubmit} availableLiquidity={{}} />);

expect(screen.getByPlaceholderText("Asset")).toHaveValue("USDC");
});

it("defaults the asset field to the first available asset when USDC isn't one of the supported pools", () => {
const onSubmit = vi.fn();
render(
<SettlementForm
onSubmit={onSubmit}
availableLiquidity={{ BTC: 500, EURT: 250 }}
/>,
);

// USDC is not among the available assets, so the default must be
// one of the assets that are actually supported by this deployment.
expect(screen.getByPlaceholderText("Asset")).toHaveValue("BTC");
});

it("resets the asset field to the first available asset (not the USDC literal) when USDC isn't supported", () => {
const onSubmit = vi.fn();
render(
<SettlementForm
onSubmit={onSubmit}
availableLiquidity={{ BTC: 500, EURT: 250 }}
/>,
);

const assetInput = screen.getByPlaceholderText("Asset");
// The user types a different asset code.
fireEvent.change(assetInput, { target: { value: "EURT" } });
expect(assetInput).toHaveValue("EURT");

fireEvent.click(screen.getByText("Reset"));

// Reset must restore the actually-available default, not "USDC".
expect(assetInput).toHaveValue("BTC");
});

it("recomputes the asset default when availableLiquidity loads after mount, without clobbering a user edit", () => {
const onSubmit = vi.fn();
const { rerender } = render(<SettlementForm onSubmit={onSubmit} />);

const assetInput = screen.getByPlaceholderText("Asset");
// Before pools have loaded, the field still holds the "USDC" fallback.
expect(assetInput).toHaveValue("USDC");

// Pools finish loading; USDC isn't one of the supported assets.
rerender(
<SettlementForm onSubmit={onSubmit} availableLiquidity={{ BTC: 500, EURT: 250 }} />,
);

// Since the field still held the previous default, it is updated to
// the newly-known available asset.
expect(assetInput).toHaveValue("BTC");

// Now the user edits the field manually.
fireEvent.change(assetInput, { target: { value: "EURT" } });

// If availableLiquidity updates again, the user's edit must not be
// clobbered because it no longer matches the previous default.
rerender(
<SettlementForm
onSubmit={onSubmit}
availableLiquidity={{ BTC: 500, EURT: 250, XLM: 10 }}
/>,
);
expect(assetInput).toHaveValue("EURT");
});
});
25 changes: 23 additions & 2 deletions src/components/SettlementForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,21 @@ export function SettlementForm({
availableLiquidity?: Record<string, number>;
serverError?: string;
}) {
/**
* The asset field's default value: the first asset known to be
* available (from `availableLiquidity`), falling back to "USDC" when
* `availableLiquidity` hasn't been populated yet (e.g. before pools
* have loaded) or is empty.
*/
function getDefaultAsset(liquidity?: Record<string, number>): string {
return Object.keys(liquidity ?? {})[0] ?? "USDC";
}

const [anchor, setAnchor] = useState("");
const [asset, setAsset] = useState("USDC");
const [asset, setAsset] = useState(() => getDefaultAsset(availableLiquidity));
const [amount, setAmount] = useState("");
const [errors, setErrors] = useState<FormErrors>({});
const defaultAssetRef = useRef(getDefaultAsset(availableLiquidity));

useEffect(() => {
if (serverError) {
Expand All @@ -68,6 +79,16 @@ export function SettlementForm({
}
}, [serverError]);

// Recompute the default asset whenever availableLiquidity changes (e.g.
// once pools finish loading after mount). Only update the field if it
// still holds the previous default, so we never clobber a user's
// in-progress edit.
useEffect(() => {
const nextDefault = getDefaultAsset(availableLiquidity);
setAsset((current) => (current === defaultAssetRef.current ? nextDefault : current));
defaultAssetRef.current = nextDefault;
}, [availableLiquidity]);

const anchorRef = useRef<HTMLInputElement>(null);
const assetOptions = Object.keys(availableLiquidity ?? {});
const assetListId = assetOptions.length > 0 ? ASSET_DATALIST_ID : undefined;
Expand All @@ -94,7 +115,7 @@ export function SettlementForm({

function reset() {
setAnchor("");
setAsset("USDC");
setAsset(defaultAssetRef.current);
setAmount("");
setErrors({});
anchorRef.current?.focus();
Expand Down