From 603ebf245aa4637df402abe1b1a273f0d2dd0088 Mon Sep 17 00:00:00 2001 From: JONAH-6 Date: Sat, 30 May 2026 08:38:44 -0700 Subject: [PATCH 1/4] ops: add relayer cargo clippy check to verify.sh Resolves #474 --- scripts/verify.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/verify.sh b/scripts/verify.sh index 0f2e3785..2e0770f3 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -17,6 +17,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SMARTCONTRACT_DIR="${SCRIPT_DIR}/../smartcontract" +RELAYER_DIR="${SCRIPT_DIR}/../relayer" # --------------------------------------------------------------------------- # Static analysis: run cargo clippy on the smart contract @@ -33,6 +34,18 @@ fi (cd "${SMARTCONTRACT_DIR}" && cargo clippy -- -D warnings) echo " PASS cargo clippy passed" echo "" + +# --------------------------------------------------------------------------- +# Static analysis: run cargo clippy on the relayer +# --------------------------------------------------------------------------- +echo "============================================" +echo "Running cargo clippy on relayer" +echo "============================================" + +(cd "${RELAYER_DIR}" && cargo clippy -- -D warnings) +echo " PASS cargo clippy passed (relayer)" +echo "" + CONFIG_FILE="${SCRIPT_DIR}/config/testnet.env" CONTRACT_ID="" From ea91688e6b67bce3b5010611459e1672ae2986d4 Mon Sep 17 00:00:00 2001 From: JONAH-6 Date: Sat, 30 May 2026 08:44:35 -0700 Subject: [PATCH 2/4] backend: add warning field for unsupported fee-estimate chains Resolves #443 --- backend/app/services/fee_estimation.py | 8 ++++++++ backend/tests/test_fee_estimation.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/backend/app/services/fee_estimation.py b/backend/app/services/fee_estimation.py index 1d1ab5db..43342c85 100644 --- a/backend/app/services/fee_estimation.py +++ b/backend/app/services/fee_estimation.py @@ -2,10 +2,13 @@ from __future__ import annotations +import logging from dataclasses import dataclass from datetime import datetime, timezone from typing import Optional +logger = logging.getLogger(__name__) + @dataclass class FeeComponent: @@ -22,6 +25,7 @@ class FeeEstimate: asset: str components: list[FeeComponent] estimated_at: str + warning: Optional[str] = None def to_dict(self) -> dict: return { @@ -38,6 +42,7 @@ def to_dict(self) -> dict: for c in self.components ], "estimated_at": self.estimated_at, + "warning": self.warning, } @@ -123,12 +128,15 @@ def estimate_chain_fee(self, chain: str) -> FeeEstimate: chain_lower = chain.lower() config = CHAIN_FEE_CONFIG.get(chain_lower) if not config: + msg = f"Chain '{chain_lower}' is not supported for fee estimation." + logger.warning(msg) return FeeEstimate( chain=chain_lower, total_fee=0.0, asset="UNKNOWN", components=[], estimated_at=datetime.now(timezone.utc).isoformat(), + warning=msg, ) network_fee = FeeComponent( diff --git a/backend/tests/test_fee_estimation.py b/backend/tests/test_fee_estimation.py index 7e6db9f2..e7b638a0 100644 --- a/backend/tests/test_fee_estimation.py +++ b/backend/tests/test_fee_estimation.py @@ -41,6 +41,8 @@ def test_estimate_unknown_chain(self, service): assert estimate.chain == "unknown" assert estimate.total_fee == 0.0 assert len(estimate.components) == 0 + assert estimate.warning is not None + assert "not supported" in estimate.warning def test_fee_components_structure(self, service): estimate = service.estimate_chain_fee("stellar") From 523ae8cebdb7b0c2e3cc353141b75c2e9d8a85ea Mon Sep 17 00:00:00 2001 From: JONAH-6 Date: Sat, 30 May 2026 08:45:25 -0700 Subject: [PATCH 3/4] backend: cap price history per asset at MAX_HISTORY_PER_ASSET Resolves #441 --- backend/app/services/price_oracle.py | 9 +++++++++ backend/tests/test_price_oracle.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/backend/app/services/price_oracle.py b/backend/app/services/price_oracle.py index 3a248586..ff184085 100644 --- a/backend/app/services/price_oracle.py +++ b/backend/app/services/price_oracle.py @@ -26,6 +26,8 @@ PRICE_CACHE_TTL = 60 # Maximum acceptable deviation between sources (10%) MAX_PRICE_DEVIATION = 0.10 +# Maximum price history entries retained per asset +MAX_HISTORY_PER_ASSET = 500 @dataclass @@ -260,5 +262,12 @@ def _record_history(self, price_data: PriceData) -> None: timestamp=price_data.timestamp, ) ) + # Per-asset cap: remove oldest entries for this asset when over limit + asset_indices = [i for i, e in enumerate(self._history) if e.asset == price_data.asset] + if len(asset_indices) > MAX_HISTORY_PER_ASSET: + to_remove = len(asset_indices) - MAX_HISTORY_PER_ASSET + for i in reversed(asset_indices[:to_remove]): + self._history.pop(i) + # Global cap if len(self._history) > 5000: self._history = self._history[-5000:] diff --git a/backend/tests/test_price_oracle.py b/backend/tests/test_price_oracle.py index 8d29af69..dc07a311 100644 --- a/backend/tests/test_price_oracle.py +++ b/backend/tests/test_price_oracle.py @@ -118,6 +118,23 @@ async def test_history_filter_by_asset(self, service): history = service.get_price_history(asset="XLM") assert all(r["asset"] == "XLM" for r in history) + @pytest.mark.anyio + async def test_per_asset_history_is_bounded(self, service): + from app.services.price_oracle import MAX_HISTORY_PER_ASSET, PriceHistoryEntry + + for _ in range(MAX_HISTORY_PER_ASSET + 10): + service._history.append( + PriceHistoryEntry( + asset="XLM", price_usd=0.15, source="test", timestamp="t" + ) + ) + + with patch.object(service, "_fetch_coingecko", return_value=0.15): + await service.get_price("XLM") + + xlm_count = sum(1 for e in service._history if e.asset == "XLM") + assert xlm_count <= MAX_HISTORY_PER_ASSET + class TestAlerts: @pytest.mark.anyio From b9c5155e97f4a92f6834d010fb37150cf82b9cd8 Mon Sep 17 00:00:00 2001 From: JONAH-6 Date: Sat, 30 May 2026 08:45:52 -0700 Subject: [PATCH 4/4] test(e2e): add Playwright smoke test for happy-path swap form Resolves #427 --- frontend/e2e/swap-smoke.spec.ts | 56 +++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 frontend/e2e/swap-smoke.spec.ts diff --git a/frontend/e2e/swap-smoke.spec.ts b/frontend/e2e/swap-smoke.spec.ts new file mode 100644 index 00000000..207ee0cf --- /dev/null +++ b/frontend/e2e/swap-smoke.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from "@playwright/test"; + +/** + * Happy-path smoke test for the swap form at /swap. + * No real wallet required — verifies UI state without wallet dependency. + * Issue #427 + */ + +const SWAP_PAGE = "/swap"; + +test.describe("Swap form — happy-path smoke (no wallet)", () => { + test.beforeEach(async ({ page }) => { + await page.goto(SWAP_PAGE); + }); + + test("page renders form heading", async ({ page }) => { + await expect(page.getByText("Create Swap")).toBeVisible(); + }); + + test("From and To chain-asset selectors are visible", async ({ page }) => { + await expect(page.getByText("From").first()).toBeVisible(); + await expect(page.getByText("To").first()).toBeVisible(); + }); + + test("default route shows stellar XLM as source and bitcoin BTC as destination", async ({ + page, + }) => { + // ChainAssetSelector renders the selected asset symbol as button text + await expect(page.getByRole("button", { name: /XLM/i }).first()).toBeVisible(); + await expect(page.getByRole("button", { name: /BTC/i }).first()).toBeVisible(); + }); + + test("amount input accepts a numeric value", async ({ page }) => { + const amountInput = page.locator('input[type="number"]').first(); + await expect(amountInput).toBeVisible(); + await amountInput.fill("10"); + await expect(amountInput).toHaveValue("10"); + }); + + test("submit button is disabled when no wallet is connected", async ({ page }) => { + const submit = page.getByRole("button", { name: /Connect Wallet to Swap/i }); + await expect(submit).toBeVisible(); + await expect(submit).toBeDisabled(); + }); + + test("wallet-connect hint is visible when no wallet connected", async ({ page }) => { + await expect(page.getByText("Connect a wallet to continue.")).toBeVisible(); + }); + + test("submit remains disabled after entering a valid amount with no wallet", async ({ page }) => { + await page.locator('input[type="number"]').first().fill("5"); + // isConnected is always false in a no-wallet test — canSubmit stays false + const submit = page.getByRole("button", { name: /Connect Wallet to Swap/i }); + await expect(submit).toBeDisabled(); + }); +});