From a5c9bb0f5c2f0489de512754e02cb689ef505163 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Mon, 19 Oct 2026 05:32:32 -0400 Subject: [PATCH 1/5] chore(ci): repair pre-existing root package.json, build, and validation failures (cherry picked from commit f384a0ff3a83c651d124c9415190f576fcafb509) --- .husky/commit-msg | 2 + contracts/vault/fuzz/Cargo.toml | 1 + docs/GLOSSARY.md | 2 +- .../src/components/NetworkBannerStack.tsx | 8 +- .../components/SessionExpiryWarning.test.tsx | 11 +- .../src/components/VaultDashboard.test.tsx | 16 +-- frontend/src/context/ToastContext.test.tsx | 22 ++++ frontend/src/forms/useForm.ts | 10 +- frontend/src/forms/useZodForm.test.ts | 4 +- frontend/src/hooks/useDelayedLoading.ts | 21 ++-- frontend/src/hooks/useFocusTrap.test.tsx | 6 + frontend/src/hooks/useLoadingState.ts | 4 +- .../src/hooks/useLocalizedFormat.test.tsx | 2 +- frontend/src/lib/optimisticVaultCache.ts | 2 +- frontend/src/lib/vaultHealthApi.ts | 8 +- packages/api-schemas/src/index.d.ts | 6 - packages/api-schemas/src/index.d.ts.map | 1 - packages/api-schemas/src/index.js | 35 ------ packages/api-schemas/src/index.js.map | 1 - packages/api-schemas/src/primitives.d.ts | 24 ---- packages/api-schemas/src/primitives.d.ts.map | 1 - packages/api-schemas/src/primitives.js | 66 ---------- packages/api-schemas/src/primitives.js.map | 1 - packages/api-schemas/src/requests.d.ts | 33 ----- packages/api-schemas/src/requests.d.ts.map | 1 - packages/api-schemas/src/requests.js | 74 ------------ packages/api-schemas/src/requests.js.map | 1 - packages/api-schemas/src/responses.d.ts | 5 - packages/api-schemas/src/responses.d.ts.map | 1 - packages/api-schemas/src/responses.js | 19 --- packages/api-schemas/src/responses.js.map | 1 - packages/api-schemas/src/vault.d.ts | 18 --- packages/api-schemas/src/vault.d.ts.map | 1 - packages/api-schemas/src/vault.js | 47 -------- packages/api-schemas/src/vault.js.map | 1 - packages/api-schemas/src/webhookEvents.d.ts | 57 --------- .../api-schemas/src/webhookEvents.d.ts.map | 1 - packages/api-schemas/src/webhookEvents.js | 114 ------------------ packages/api-schemas/src/webhookEvents.js.map | 1 - scripts/lint-staged-runners.js | 9 +- 40 files changed, 92 insertions(+), 546 deletions(-) create mode 100755 .husky/commit-msg delete mode 100644 packages/api-schemas/src/index.d.ts delete mode 100644 packages/api-schemas/src/index.d.ts.map delete mode 100644 packages/api-schemas/src/index.js delete mode 100644 packages/api-schemas/src/index.js.map delete mode 100644 packages/api-schemas/src/primitives.d.ts delete mode 100644 packages/api-schemas/src/primitives.d.ts.map delete mode 100644 packages/api-schemas/src/primitives.js delete mode 100644 packages/api-schemas/src/primitives.js.map delete mode 100644 packages/api-schemas/src/requests.d.ts delete mode 100644 packages/api-schemas/src/requests.d.ts.map delete mode 100644 packages/api-schemas/src/requests.js delete mode 100644 packages/api-schemas/src/requests.js.map delete mode 100644 packages/api-schemas/src/responses.d.ts delete mode 100644 packages/api-schemas/src/responses.d.ts.map delete mode 100644 packages/api-schemas/src/responses.js delete mode 100644 packages/api-schemas/src/responses.js.map delete mode 100644 packages/api-schemas/src/vault.d.ts delete mode 100644 packages/api-schemas/src/vault.d.ts.map delete mode 100644 packages/api-schemas/src/vault.js delete mode 100644 packages/api-schemas/src/vault.js.map delete mode 100644 packages/api-schemas/src/webhookEvents.d.ts delete mode 100644 packages/api-schemas/src/webhookEvents.d.ts.map delete mode 100644 packages/api-schemas/src/webhookEvents.js delete mode 100644 packages/api-schemas/src/webhookEvents.js.map diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 000000000..a11011270 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +npx --no -- commitlint --edit "$1" diff --git a/contracts/vault/fuzz/Cargo.toml b/contracts/vault/fuzz/Cargo.toml index 2fb7b8c1a..0c60fe10e 100644 --- a/contracts/vault/fuzz/Cargo.toml +++ b/contracts/vault/fuzz/Cargo.toml @@ -3,6 +3,7 @@ name = "vault-fuzz" version = "0.0.0" publish = false edition = "2021" +license = "MIT" [package.metadata] cargo-fuzz = true diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 287dbd915..b64efe915 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -24,7 +24,7 @@ A shared reference for technical and product terminology used across the YieldVa - [Access Controls — Whitelist, RBAC & Admin](#access-controls--whitelist-rbac--admin) - [Protocol Fees, Treasury & Basis Points](#protocol-fees-treasury--basis-points) - [RWA Shipment Tracking & Asset Provenance](#rwa-shipment-tracking--asset-provenance) -- [Smart Contracts & On-Chian Mechanics](#smart-contracts--on-chain-mechanics) +- [Smart Contracts & On-Chain Mechanics](#smart-contracts--on-chain-mechanics) - [Math & Fixed-Point Arithmetic](#math--fixed-point-arithmetic) - [Frontend & UI](#frontend--ui) - [API & Backend](#api--backend) diff --git a/frontend/src/components/NetworkBannerStack.tsx b/frontend/src/components/NetworkBannerStack.tsx index 104f5b19d..90f4132da 100644 --- a/frontend/src/components/NetworkBannerStack.tsx +++ b/frontend/src/components/NetworkBannerStack.tsx @@ -23,7 +23,7 @@ import { useWalletNetwork } from "../hooks/useWalletNetwork"; import { useRetryState } from "../hooks/useRetryState"; import { useTranslation } from "../i18n"; import { queryClient } from "../lib/queryClient"; -import { AlertTriangle, Clock, WifiOff, Wifi, X } from "./icons"; +import { AlertTriangle, Clock, Wifi, X } from "./icons"; import NetworkMismatchGuideModal from "./NetworkMismatchGuideModal"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -98,7 +98,11 @@ function OfflineBannerView({ {lastKnownTvl !== undefined && `TVL: ${lastKnownTvl.toLocaleString()}`} {lastKnownTvl !== undefined && lastKnownBalance !== undefined && " · "} - {lastKnownBalance !== undefined && `Balance: ${lastKnownBalance.toFixed(2)} USDC`} + {lastKnownBalance !== undefined && + `Balance: ${lastKnownBalance.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} USDC`} )} diff --git a/frontend/src/components/SessionExpiryWarning.test.tsx b/frontend/src/components/SessionExpiryWarning.test.tsx index 513e67879..82b1bf66d 100644 --- a/frontend/src/components/SessionExpiryWarning.test.tsx +++ b/frontend/src/components/SessionExpiryWarning.test.tsx @@ -5,9 +5,16 @@ import { AuthProvider } from "../context/AuthContext"; vi.mock("../i18n", () => ({ useTranslation: () => ({ - t: (key: string) => { + t: (key: string, options?: Record) => { + const interpolate = (message: string) => + options + ? message.replace(/\{\{(\w+)\}\}/g, (match, name: string) => + options[name] !== undefined ? String(options[name]) : match, + ) + : message; if (key === "session.warning.title") return "Session Expiring Soon"; - if (key === "session.warning.message") return "Your wallet session will expire in {{minutes}} minutes. Reconnect to continue without interruption."; + if (key === "session.warning.message") + return interpolate("Your wallet session will expire in {{minutes}} minutes. Reconnect to continue without interruption."); if (key === "session.warning.reconnect") return "Reconnect"; if (key === "common.dismiss") return "Dismiss"; return key; diff --git a/frontend/src/components/VaultDashboard.test.tsx b/frontend/src/components/VaultDashboard.test.tsx index 7c7b3982e..213e4e69c 100644 --- a/frontend/src/components/VaultDashboard.test.tsx +++ b/frontend/src/components/VaultDashboard.test.tsx @@ -234,13 +234,13 @@ describe("VaultDashboard", () => { expect(await screen.findByText(/Review Transaction/i)).toBeInTheDocument(); - fireEvent.click(screen.getByRole("button", { name: "Withdraw" })); + fireEvent.click(screen.getByRole("tab", { name: "Withdraw" })); await waitFor(() => { expect(screen.getByTestId("location-search")).toHaveTextContent("tab=withdraw"); expect(screen.getByText(/Amount to withdraw/i)).toBeInTheDocument(); }); - fireEvent.click(screen.getByRole("button", { name: "Deposit" })); + fireEvent.click(screen.getByRole("tab", { name: "Deposit" })); await waitFor(() => { expect(screen.getByTestId("location-search")).toHaveTextContent("tab=deposit"); expect(screen.getByText(/Amount to deposit/i)).toBeInTheDocument(); @@ -353,7 +353,7 @@ describe("VaultDashboard", () => { expect(await screen.findByText(/Review Transaction/i)).toBeInTheDocument(); - fireEvent.click(screen.getByRole("button", { name: "Withdraw" })); + fireEvent.click(screen.getByRole("tab", { name: "Withdraw" })); await waitFor(() => { expect(screen.getByTestId("location-search")).toHaveTextContent("tab=withdraw"); }); @@ -491,7 +491,7 @@ describe("VaultDashboard", () => { fireEvent.change(input, { target: { value: "100" } }); expect(input).toHaveValue(100); - fireEvent.click(screen.getByRole("button", { name: "Withdraw" })); + fireEvent.click(screen.getByRole("tab", { name: "Withdraw" })); await waitFor(() => { expect(screen.getByTestId("location-search")).toHaveTextContent("tab=withdraw"); @@ -569,7 +569,7 @@ describe("VaultDashboard", () => { await screen.findByRole("heading", { name: "Transaction Successful" }); }, 15000); - it("does not offer a retry action for a non-retryable validation failure", async () => { + it("routes a non-retryable validation failure back to the amount step without a retry action", async () => { const validationError = new ValidationError({ message: "Validation failed", userMessage: "Please review the amount and try again.", @@ -584,8 +584,10 @@ describe("VaultDashboard", () => { fireEvent.click(screen.getByRole("button", { name: "Review Transaction" })); fireEvent.click(await screen.findByRole("button", { name: /Confirm deposit/i })); - await screen.findByRole("heading", { name: "Transaction Failed" }); - expect(await screen.findByRole("button", { name: "Start Over" })).toBeInTheDocument(); + // Field-level validation failures send the user back to the amount step + // so the input can be corrected; a blind retry is never offered. + expect(await screen.findByText("Amount exceeds vault cap")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("0.00")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Retry" })).not.toBeInTheDocument(); }, 15000); diff --git a/frontend/src/context/ToastContext.test.tsx b/frontend/src/context/ToastContext.test.tsx index 682ccabd3..c826a299c 100644 --- a/frontend/src/context/ToastContext.test.tsx +++ b/frontend/src/context/ToastContext.test.tsx @@ -54,6 +54,11 @@ describe("ToastProvider", () => { fireEvent.click(screen.getByText(/Show success/i)); + // Sonner defers rendering new toasts by a tick; flush it under fake timers. + act(() => { + vi.advanceTimersByTime(20); + }); + expect(screen.getByText("Saved")).toBeInTheDocument(); expect(container.querySelector("[data-sonner-toaster]")).toBeTruthy(); @@ -61,6 +66,11 @@ describe("ToastProvider", () => { vi.advanceTimersByTime(1500); }); + // Sonner keeps the toast mounted for a 200ms exit animation before unmounting. + act(() => { + vi.advanceTimersByTime(300); + }); + expect(screen.queryByText("Saved")).not.toBeInTheDocument(); }); @@ -72,11 +82,23 @@ describe("ToastProvider", () => { ); fireEvent.click(screen.getByText(/Show error/i)); + + // Sonner defers rendering new toasts by a tick; flush it under fake timers. + act(() => { + vi.advanceTimersByTime(20); + }); + expect(screen.getByText("Failed")).toBeInTheDocument(); const dismiss = screen.getByRole("button", { name: /close|dismiss/i }); fireEvent.click(dismiss); + // Dismissal is applied on the next animation frame, then sonner keeps the + // toast mounted for a 200ms exit animation before unmounting it. + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.queryByText("Failed")).not.toBeInTheDocument(); }); }); diff --git a/frontend/src/forms/useForm.ts b/frontend/src/forms/useForm.ts index bace30592..6ac5aee84 100644 --- a/frontend/src/forms/useForm.ts +++ b/frontend/src/forms/useForm.ts @@ -133,10 +133,12 @@ export function useForm( const valuesToValidate = overrideValues ?? valuesRef.current; const nextErrors = runSchemaValidation(schemaRef.current, valuesToValidate); - let touchedAll: Partial> = {}; - if ("shape" in schemaRef.current && typeof (schemaRef.current as any).shape === "object") { - const shape = (schemaRef.current as any).shape; - Object.keys(shape).forEach((key) => { + const schemaShape = ( + schemaRef.current as unknown as { shape?: Record } + ).shape; + const touchedAll: Partial> = {}; + if (schemaShape && typeof schemaShape === "object") { + Object.keys(schemaShape).forEach((key) => { touchedAll[key as keyof T] = true; }); } else { diff --git a/frontend/src/forms/useZodForm.test.ts b/frontend/src/forms/useZodForm.test.ts index a85e68b7f..b50fdba7e 100644 --- a/frontend/src/forms/useZodForm.test.ts +++ b/frontend/src/forms/useZodForm.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, vi } from "vitest"; import { renderHook, act } from "@testing-library/react"; +import type { FormEvent } from "react"; import { useForm } from "./useForm"; import { settingsFormSchema } from "./schemas/settingsFormSchema"; -import { z } from "zod"; describe("Zod Form Validation & Submission States", () => { const initialValues = { @@ -59,7 +59,7 @@ describe("Zod Form Validation & Submission States", () => { const onSubmitSuccess = vi.fn().mockResolvedValue(undefined); - const fakeEvent = { preventDefault: vi.fn() } as any; + const fakeEvent = { preventDefault: vi.fn() } as unknown as FormEvent; await act(async () => { await result.current.handleSubmit(onSubmitSuccess)(fakeEvent); diff --git a/frontend/src/hooks/useDelayedLoading.ts b/frontend/src/hooks/useDelayedLoading.ts index 002dd598e..211456065 100644 --- a/frontend/src/hooks/useDelayedLoading.ts +++ b/frontend/src/hooks/useDelayedLoading.ts @@ -13,22 +13,21 @@ export function useDelayedLoading(isLoading: boolean, delayMs: number = 100): bo const [delayedLoading, setDelayedLoading] = useState(false); useEffect(() => { - let timeoutId: ReturnType; - - if (isLoading) { - timeoutId = setTimeout(() => { - setDelayedLoading(true); - }, delayMs); - } else { - queueMicrotask(() => { - setDelayedLoading(false); - }); + if (!isLoading) { + return; } + const timeoutId = setTimeout(() => { + setDelayedLoading(true); + }, delayMs); + return () => { clearTimeout(timeoutId); + setDelayedLoading(false); }; }, [isLoading, delayMs]); - return delayedLoading; + // Mask the stale flag while not loading so consumers clear their skeleton + // immediately instead of waiting for a microtask. + return isLoading && delayedLoading; } diff --git a/frontend/src/hooks/useFocusTrap.test.tsx b/frontend/src/hooks/useFocusTrap.test.tsx index f58f46ea0..291dce3a1 100644 --- a/frontend/src/hooks/useFocusTrap.test.tsx +++ b/frontend/src/hooks/useFocusTrap.test.tsx @@ -25,6 +25,12 @@ describe("useFocusTrap", () => { const user = userEvent.setup(); render( undefined} />); + // The trap sets initial focus on the next animation frame; wait for it so + // it cannot steal focus from "Last" while tab() is in flight. + await waitFor(() => { + expect(screen.getByRole("button", { name: "First" })).toHaveFocus(); + }); + screen.getByRole("button", { name: "Last" }).focus(); await user.tab(); expect(screen.getByRole("button", { name: "First" })).toHaveFocus(); diff --git a/frontend/src/hooks/useLoadingState.ts b/frontend/src/hooks/useLoadingState.ts index 0f63365f7..de8280fcb 100644 --- a/frontend/src/hooks/useLoadingState.ts +++ b/frontend/src/hooks/useLoadingState.ts @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from "react"; +import { useState, useCallback } from "react"; export type LoadingStatus = "idle" | "loading" | "success" | "error"; @@ -22,7 +22,7 @@ export interface UseLoadingStateReturn { reset: () => void; } -export function useLoadingState( +export function useLoadingState( options: UseLoadingStateOptions = {} ): UseLoadingStateReturn { const { initialData = null, minLoadingTimeMs = 0, onSuccess, onError } = options; diff --git a/frontend/src/hooks/useLocalizedFormat.test.tsx b/frontend/src/hooks/useLocalizedFormat.test.tsx index 144d7ad79..ff9f97229 100644 --- a/frontend/src/hooks/useLocalizedFormat.test.tsx +++ b/frontend/src/hooks/useLocalizedFormat.test.tsx @@ -4,7 +4,7 @@ import { useLocalizedFormat } from "./useLocalizedFormat"; import { PreferencesProvider } from "../context/PreferencesContext"; import React from "react"; -function createWrapper(locale = "en-US") { +function createWrapper() { return function Wrapper({ children }: { children: React.ReactNode }) { return ( diff --git a/frontend/src/lib/optimisticVaultCache.ts b/frontend/src/lib/optimisticVaultCache.ts index 9bc0b2bb7..cf58c2506 100644 --- a/frontend/src/lib/optimisticVaultCache.ts +++ b/frontend/src/lib/optimisticVaultCache.ts @@ -178,7 +178,7 @@ export function applyOptimisticVaultPatch( queryClient.setQueryData(keys.balanceKey, (current) => typeof current === "number" ? Math.max(current + walletDelta, 0) - : current, + : Math.max(walletDelta, 0), ); queryClient.setQueryData( keys.holdingsKey, diff --git a/frontend/src/lib/vaultHealthApi.ts b/frontend/src/lib/vaultHealthApi.ts index e6da59339..5e701a522 100644 --- a/frontend/src/lib/vaultHealthApi.ts +++ b/frontend/src/lib/vaultHealthApi.ts @@ -30,12 +30,16 @@ export type VaultHealthRecord = z.infer; * Falls back to mock data if the real endpoint is unavailable. */ export async function getVaultHealth(): Promise { + let data: unknown; try { - const data = await apiClient.get("/mock-api/vault-health.json"); - return validate(VaultHealthResponseSchema, data, "VaultHealth"); + data = await apiClient.get("/mock-api/vault-health.json"); } catch { + // Backend unavailable – degrade to an empty health list. return []; } + // Validation failures are programming/API-contract errors and must + // propagate instead of masquerading as "no health data". + return validate(VaultHealthResponseSchema, data, "VaultHealth"); } export const VaultMetricsSchema = z.object({ diff --git a/packages/api-schemas/src/index.d.ts b/packages/api-schemas/src/index.d.ts deleted file mode 100644 index 42176e51c..000000000 --- a/packages/api-schemas/src/index.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { StellarAddressSchema, AmountSchema, AmountInputSchema, ShareCountSchema, AssetCodeSchema, IsoDatestamp, SlippageBpsSchema, } from "./primitives"; -export { DepositRequestSchema, WithdrawalRequestSchema, VaultHistoryQuerySchema, PortfolioQuerySchema, WalletAddressSchema, TransactionQuerySchema, type DepositRequest, type WithdrawalRequest, type VaultHistoryQuery, type PortfolioQuery, type WalletAddressParam, type TransactionQuery, type TransactionQueryInput, } from "./requests"; -export { VaultOperationResponseSchema, type VaultOperationResponse, } from "./responses"; -export { VaultDepositBodySchema, VaultWithdrawalBodySchema, SignedVaultDepositBodySchema, SignedVaultWithdrawalBodySchema, VaultOperationSchema, type VaultDepositBody, type VaultWithdrawalBody, } from "./vault"; -export { WEBHOOK_SCHEMA_VERSION, WebhookEventTypeSchema, WebhookEventPayloadSchemas, WebhookEnvelopeSchema, TransactionDepositCreatedPayloadSchema, TransactionWithdrawalCreatedPayloadSchema, parseWebhookEnvelope, type WebhookEventType, type WebhookEnvelope, type TransactionDepositCreatedPayload, type TransactionWithdrawalCreatedPayload, } from "./webhookEvents"; -//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/api-schemas/src/index.d.ts.map b/packages/api-schemas/src/index.d.ts.map deleted file mode 100644 index ed8da5c23..000000000 --- a/packages/api-schemas/src/index.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,EACvB,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,EACtB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,4BAA4B,EAC5B,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,4BAA4B,EAC5B,+BAA+B,EAC/B,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EACtB,0BAA0B,EAC1B,qBAAqB,EACrB,sCAAsC,EACtC,yCAAyC,EACzC,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,gCAAgC,EACrC,KAAK,mCAAmC,GACzC,MAAM,iBAAiB,CAAC"} \ No newline at end of file diff --git a/packages/api-schemas/src/index.js b/packages/api-schemas/src/index.js deleted file mode 100644 index a0e0e01a6..000000000 --- a/packages/api-schemas/src/index.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.parseWebhookEnvelope = exports.TransactionWithdrawalCreatedPayloadSchema = exports.TransactionDepositCreatedPayloadSchema = exports.WebhookEnvelopeSchema = exports.WebhookEventPayloadSchemas = exports.WebhookEventTypeSchema = exports.WEBHOOK_SCHEMA_VERSION = exports.VaultOperationSchema = exports.SignedVaultWithdrawalBodySchema = exports.SignedVaultDepositBodySchema = exports.VaultWithdrawalBodySchema = exports.VaultDepositBodySchema = exports.VaultOperationResponseSchema = exports.TransactionQuerySchema = exports.WalletAddressSchema = exports.PortfolioQuerySchema = exports.VaultHistoryQuerySchema = exports.WithdrawalRequestSchema = exports.DepositRequestSchema = exports.SlippageBpsSchema = exports.IsoDatestamp = exports.AssetCodeSchema = exports.ShareCountSchema = exports.AmountInputSchema = exports.AmountSchema = exports.StellarAddressSchema = void 0; -var primitives_1 = require("./primitives"); -Object.defineProperty(exports, "StellarAddressSchema", { enumerable: true, get: function () { return primitives_1.StellarAddressSchema; } }); -Object.defineProperty(exports, "AmountSchema", { enumerable: true, get: function () { return primitives_1.AmountSchema; } }); -Object.defineProperty(exports, "AmountInputSchema", { enumerable: true, get: function () { return primitives_1.AmountInputSchema; } }); -Object.defineProperty(exports, "ShareCountSchema", { enumerable: true, get: function () { return primitives_1.ShareCountSchema; } }); -Object.defineProperty(exports, "AssetCodeSchema", { enumerable: true, get: function () { return primitives_1.AssetCodeSchema; } }); -Object.defineProperty(exports, "IsoDatestamp", { enumerable: true, get: function () { return primitives_1.IsoDatestamp; } }); -Object.defineProperty(exports, "SlippageBpsSchema", { enumerable: true, get: function () { return primitives_1.SlippageBpsSchema; } }); -var requests_1 = require("./requests"); -Object.defineProperty(exports, "DepositRequestSchema", { enumerable: true, get: function () { return requests_1.DepositRequestSchema; } }); -Object.defineProperty(exports, "WithdrawalRequestSchema", { enumerable: true, get: function () { return requests_1.WithdrawalRequestSchema; } }); -Object.defineProperty(exports, "VaultHistoryQuerySchema", { enumerable: true, get: function () { return requests_1.VaultHistoryQuerySchema; } }); -Object.defineProperty(exports, "PortfolioQuerySchema", { enumerable: true, get: function () { return requests_1.PortfolioQuerySchema; } }); -Object.defineProperty(exports, "WalletAddressSchema", { enumerable: true, get: function () { return requests_1.WalletAddressSchema; } }); -Object.defineProperty(exports, "TransactionQuerySchema", { enumerable: true, get: function () { return requests_1.TransactionQuerySchema; } }); -var responses_1 = require("./responses"); -Object.defineProperty(exports, "VaultOperationResponseSchema", { enumerable: true, get: function () { return responses_1.VaultOperationResponseSchema; } }); -var vault_1 = require("./vault"); -Object.defineProperty(exports, "VaultDepositBodySchema", { enumerable: true, get: function () { return vault_1.VaultDepositBodySchema; } }); -Object.defineProperty(exports, "VaultWithdrawalBodySchema", { enumerable: true, get: function () { return vault_1.VaultWithdrawalBodySchema; } }); -Object.defineProperty(exports, "SignedVaultDepositBodySchema", { enumerable: true, get: function () { return vault_1.SignedVaultDepositBodySchema; } }); -Object.defineProperty(exports, "SignedVaultWithdrawalBodySchema", { enumerable: true, get: function () { return vault_1.SignedVaultWithdrawalBodySchema; } }); -Object.defineProperty(exports, "VaultOperationSchema", { enumerable: true, get: function () { return vault_1.VaultOperationSchema; } }); -var webhookEvents_1 = require("./webhookEvents"); -Object.defineProperty(exports, "WEBHOOK_SCHEMA_VERSION", { enumerable: true, get: function () { return webhookEvents_1.WEBHOOK_SCHEMA_VERSION; } }); -Object.defineProperty(exports, "WebhookEventTypeSchema", { enumerable: true, get: function () { return webhookEvents_1.WebhookEventTypeSchema; } }); -Object.defineProperty(exports, "WebhookEventPayloadSchemas", { enumerable: true, get: function () { return webhookEvents_1.WebhookEventPayloadSchemas; } }); -Object.defineProperty(exports, "WebhookEnvelopeSchema", { enumerable: true, get: function () { return webhookEvents_1.WebhookEnvelopeSchema; } }); -Object.defineProperty(exports, "TransactionDepositCreatedPayloadSchema", { enumerable: true, get: function () { return webhookEvents_1.TransactionDepositCreatedPayloadSchema; } }); -Object.defineProperty(exports, "TransactionWithdrawalCreatedPayloadSchema", { enumerable: true, get: function () { return webhookEvents_1.TransactionWithdrawalCreatedPayloadSchema; } }); -Object.defineProperty(exports, "parseWebhookEnvelope", { enumerable: true, get: function () { return webhookEvents_1.parseWebhookEnvelope; } }); -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/api-schemas/src/index.js.map b/packages/api-schemas/src/index.js.map deleted file mode 100644 index 8528491aa..000000000 --- a/packages/api-schemas/src/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;AAAA,2CAQsB;AAPpB,kHAAA,oBAAoB,OAAA;AACpB,0GAAA,YAAY,OAAA;AACZ,+GAAA,iBAAiB,OAAA;AACjB,8GAAA,gBAAgB,OAAA;AAChB,6GAAA,eAAe,OAAA;AACf,0GAAA,YAAY,OAAA;AACZ,+GAAA,iBAAiB,OAAA;AAGnB,uCAcoB;AAblB,gHAAA,oBAAoB,OAAA;AACpB,mHAAA,uBAAuB,OAAA;AACvB,mHAAA,uBAAuB,OAAA;AACvB,gHAAA,oBAAoB,OAAA;AACpB,+GAAA,mBAAmB,OAAA;AACnB,kHAAA,sBAAsB,OAAA;AAUxB,yCAGqB;AAFnB,yHAAA,4BAA4B,OAAA;AAI9B,iCAQiB;AAPf,+GAAA,sBAAsB,OAAA;AACtB,kHAAA,yBAAyB,OAAA;AACzB,qHAAA,4BAA4B,OAAA;AAC5B,wHAAA,+BAA+B,OAAA;AAC/B,6GAAA,oBAAoB,OAAA;AAKtB,iDAYyB;AAXvB,uHAAA,sBAAsB,OAAA;AACtB,uHAAA,sBAAsB,OAAA;AACtB,2HAAA,0BAA0B,OAAA;AAC1B,sHAAA,qBAAqB,OAAA;AACrB,uIAAA,sCAAsC,OAAA;AACtC,0IAAA,yCAAyC,OAAA;AACzC,qHAAA,oBAAoB,OAAA"} \ No newline at end of file diff --git a/packages/api-schemas/src/primitives.d.ts b/packages/api-schemas/src/primitives.d.ts deleted file mode 100644 index 792dd373c..000000000 --- a/packages/api-schemas/src/primitives.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Stellar / Soroban public key: G... base-32 address, 56 characters. - * Validates format only — not an on-chain account existence check. - */ -export declare const StellarAddressSchema: any; -/** - * Positive decimal amount represented as a string (preserves precision). - * Allows up to 7 decimal places to match Stellar's stroop precision. - */ -export declare const AmountSchema: any; -/** - * API boundary amount: accepts canonical string amounts or legacy numeric JSON. - * Normalizes to a string so frontend and backend share one wire format. - */ -export declare const AmountInputSchema: any; -/** Positive integer share count (UI / portfolio display). */ -export declare const ShareCountSchema: any; -/** Supported asset codes. Extend as new assets are on-boarded. */ -export declare const AssetCodeSchema: any; -/** ISO 8601 date string (YYYY-MM-DD). */ -export declare const IsoDatestamp: any; -/** Optional slippage tolerance in basis points (0–500). */ -export declare const SlippageBpsSchema: any; -//# sourceMappingURL=primitives.d.ts.map \ No newline at end of file diff --git a/packages/api-schemas/src/primitives.d.ts.map b/packages/api-schemas/src/primitives.d.ts.map deleted file mode 100644 index 1f35f9d93..000000000 --- a/packages/api-schemas/src/primitives.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["primitives.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,eAAO,MAAM,oBAAoB,KAM7B,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,YAAY,KASrB,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,iBAAiB,KAQ8C,CAAC;AAE7E,6DAA6D;AAC7D,eAAO,MAAM,gBAAgB,KAIqC,CAAC;AAEnE,kEAAkE;AAClE,eAAO,MAAM,eAAe,KAE1B,CAAC;AAEH,yCAAyC;AACzC,eAAO,MAAM,YAAY,KAIrB,CAAC;AAEL,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,KAKjB,CAAC"} \ No newline at end of file diff --git a/packages/api-schemas/src/primitives.js b/packages/api-schemas/src/primitives.js deleted file mode 100644 index 103a39eaf..000000000 --- a/packages/api-schemas/src/primitives.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SlippageBpsSchema = exports.IsoDatestamp = exports.AssetCodeSchema = exports.ShareCountSchema = exports.AmountInputSchema = exports.AmountSchema = exports.StellarAddressSchema = void 0; -const zod_1 = require("zod"); -/** - * Stellar / Soroban public key: G... base-32 address, 56 characters. - * Validates format only — not an on-chain account existence check. - */ -exports.StellarAddressSchema = zod_1.z - .string() - .trim() - .min(1, { message: "Wallet address is required" }) - .regex(/^G[A-Z2-7]{55}$/, { - message: "Must be a valid Stellar public key (starts with G, 56 chars)", -}); -/** - * Positive decimal amount represented as a string (preserves precision). - * Allows up to 7 decimal places to match Stellar's stroop precision. - */ -exports.AmountSchema = zod_1.z - .string() - .trim() - .min(1, { message: "Amount is required" }) - .regex(/^\d+(\.\d{1,7})?$/, { - message: "Amount must be a positive number with up to 7 decimal places", -}) - .refine((value) => parseFloat(value) > 0, { - message: "Amount must be greater than zero", -}); -/** - * API boundary amount: accepts canonical string amounts or legacy numeric JSON. - * Normalizes to a string so frontend and backend share one wire format. - */ -exports.AmountInputSchema = zod_1.z - .union([ - exports.AmountSchema, - zod_1.z - .number({ error: "Amount is required" }) - .positive("Amount must be greater than zero") - .finite("Amount must be a finite number"), -]) - .transform((value) => (typeof value === "number" ? String(value) : value)); -/** Positive integer share count (UI / portfolio display). */ -exports.ShareCountSchema = zod_1.z - .number({ error: "Share count is required" }) - .int("Share count must be a whole number") - .positive("Share count must be greater than zero") - .max(1000000000, "Share count exceeds maximum allowed value"); -/** Supported asset codes. Extend as new assets are on-boarded. */ -exports.AssetCodeSchema = zod_1.z.enum(["XLM", "USDC", "yUSDC", "RWA"], { - error: "Asset must be one of: XLM, USDC, yUSDC, RWA", -}); -/** ISO 8601 date string (YYYY-MM-DD). */ -exports.IsoDatestamp = zod_1.z - .string() - .regex(/^\d{4}-\d{2}-\d{2}$/, { - message: "Date must be in YYYY-MM-DD format", -}); -/** Optional slippage tolerance in basis points (0–500). */ -exports.SlippageBpsSchema = zod_1.z - .number() - .int("Slippage must be a whole number of basis points") - .min(0, "Slippage cannot be negative") - .max(500, "Slippage tolerance may not exceed 500 bps (5%)") - .optional(); -//# sourceMappingURL=primitives.js.map \ No newline at end of file diff --git a/packages/api-schemas/src/primitives.js.map b/packages/api-schemas/src/primitives.js.map deleted file mode 100644 index 3f1fc9ad3..000000000 --- a/packages/api-schemas/src/primitives.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"primitives.js","sourceRoot":"","sources":["primitives.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAExB;;;GAGG;AACU,QAAA,oBAAoB,GAAG,OAAC;KAClC,MAAM,EAAE;KACR,IAAI,EAAE;KACN,GAAG,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,4BAA4B,EAAE,CAAC;KACjD,KAAK,CAAC,iBAAiB,EAAE;IACxB,OAAO,EAAE,8DAA8D;CACxE,CAAC,CAAC;AAEL;;;GAGG;AACU,QAAA,YAAY,GAAG,OAAC;KAC1B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,GAAG,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,oBAAoB,EAAE,CAAC;KACzC,KAAK,CAAC,mBAAmB,EAAE;IAC1B,OAAO,EAAE,8DAA8D;CACxE,CAAC;KACD,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;IACxC,OAAO,EAAE,kCAAkC;CAC5C,CAAC,CAAC;AAEL;;;GAGG;AACU,QAAA,iBAAiB,GAAG,OAAC;KAC/B,KAAK,CAAC;IACL,oBAAY;IACZ,OAAC;SACE,MAAM,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;SACvC,QAAQ,CAAC,kCAAkC,CAAC;SAC5C,MAAM,CAAC,gCAAgC,CAAC;CAC5C,CAAC;KACD,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAE7E,6DAA6D;AAChD,QAAA,gBAAgB,GAAG,OAAC;KAC9B,MAAM,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC;KAC5C,GAAG,CAAC,oCAAoC,CAAC;KACzC,QAAQ,CAAC,uCAAuC,CAAC;KACjD,GAAG,CAAC,UAAa,EAAE,2CAA2C,CAAC,CAAC;AAEnE,kEAAkE;AACrD,QAAA,eAAe,GAAG,OAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAU,EAAE;IAC9E,KAAK,EAAE,6CAA6C;CACrD,CAAC,CAAC;AAEH,yCAAyC;AAC5B,QAAA,YAAY,GAAG,OAAC;KAC1B,MAAM,EAAE;KACR,KAAK,CAAC,qBAAqB,EAAE;IAC5B,OAAO,EAAE,mCAAmC;CAC7C,CAAC,CAAC;AAEL,2DAA2D;AAC9C,QAAA,iBAAiB,GAAG,OAAC;KAC/B,MAAM,EAAE;KACR,GAAG,CAAC,iDAAiD,CAAC;KACtD,GAAG,CAAC,CAAC,EAAE,6BAA6B,CAAC;KACrC,GAAG,CAAC,GAAG,EAAE,gDAAgD,CAAC;KAC1D,QAAQ,EAAE,CAAC"} \ No newline at end of file diff --git a/packages/api-schemas/src/requests.d.ts b/packages/api-schemas/src/requests.d.ts deleted file mode 100644 index d677e82fe..000000000 --- a/packages/api-schemas/src/requests.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { z } from "zod"; -/** - * Payload sent when a user deposits assets into a vault. - */ -export declare const DepositRequestSchema: any; -export type DepositRequest = z.infer; -/** - * Payload sent when a user redeems vault shares for underlying assets. - */ -export declare const WithdrawalRequestSchema: any; -export type WithdrawalRequest = z.infer; -/** - * Query-string parameters for the vault performance history endpoint. - */ -export declare const VaultHistoryQuerySchema: any; -export type VaultHistoryQuery = z.infer; -/** - * Query-string parameters for the portfolio holdings endpoint. - */ -export declare const PortfolioQuerySchema: any; -export type PortfolioQuery = z.infer; -/** - * Single-param schema used when an endpoint only needs the caller's address. - */ -export declare const WalletAddressSchema: any; -export type WalletAddressParam = z.infer; -/** - * Query-string parameters for the transaction history endpoint. - */ -export declare const TransactionQuerySchema: any; -export type TransactionQuery = z.infer; -export type TransactionQueryInput = z.input; -//# sourceMappingURL=requests.d.ts.map \ No newline at end of file diff --git a/packages/api-schemas/src/requests.d.ts.map b/packages/api-schemas/src/requests.d.ts.map deleted file mode 100644 index 03071ce36..000000000 --- a/packages/api-schemas/src/requests.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"requests.d.ts","sourceRoot":"","sources":["requests.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASxB;;GAEG;AACH,eAAO,MAAM,oBAAoB,KAM/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;GAEG;AACH,eAAO,MAAM,uBAAuB,KAMlC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;GAEG;AACH,eAAO,MAAM,uBAAuB,KAmBjC,CAAC;AAEJ,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;GAEG;AACH,eAAO,MAAM,oBAAoB,KAG/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;GAEG;AACH,eAAO,MAAM,mBAAmB,KAE9B,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAErE;;GAEG;AACH,eAAO,MAAM,sBAAsB,KAWjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACtE,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"} \ No newline at end of file diff --git a/packages/api-schemas/src/requests.js b/packages/api-schemas/src/requests.js deleted file mode 100644 index c1b1fd76f..000000000 --- a/packages/api-schemas/src/requests.js +++ /dev/null @@ -1,74 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.TransactionQuerySchema = exports.WalletAddressSchema = exports.PortfolioQuerySchema = exports.VaultHistoryQuerySchema = exports.WithdrawalRequestSchema = exports.DepositRequestSchema = void 0; -const zod_1 = require("zod"); -const primitives_1 = require("./primitives"); -/** - * Payload sent when a user deposits assets into a vault. - */ -exports.DepositRequestSchema = zod_1.z.object({ - walletAddress: primitives_1.StellarAddressSchema, - amount: primitives_1.AmountSchema, - asset: primitives_1.AssetCodeSchema, - slippageBps: primitives_1.SlippageBpsSchema, - referralCode: zod_1.z.string().optional(), -}); -/** - * Payload sent when a user redeems vault shares for underlying assets. - */ -exports.WithdrawalRequestSchema = zod_1.z.object({ - walletAddress: primitives_1.StellarAddressSchema, - amount: primitives_1.AmountSchema, - asset: primitives_1.AssetCodeSchema, - destinationAddress: primitives_1.StellarAddressSchema.optional(), - slippageBps: primitives_1.SlippageBpsSchema, -}); -/** - * Query-string parameters for the vault performance history endpoint. - */ -exports.VaultHistoryQuerySchema = zod_1.z - .object({ - from: primitives_1.IsoDatestamp.optional(), - to: primitives_1.IsoDatestamp.optional(), - limit: zod_1.z - .number() - .int("Limit must be a whole number") - .min(1, "Limit must be at least 1") - .max(365, "Limit may not exceed 365 data points") - .optional(), -}) - .refine((query) => { - if (query.from && query.to) { - return query.from <= query.to; - } - return true; -}, { message: '"from" date must not be later than "to" date', path: ["from"] }); -/** - * Query-string parameters for the portfolio holdings endpoint. - */ -exports.PortfolioQuerySchema = zod_1.z.object({ - walletAddress: primitives_1.StellarAddressSchema, - status: zod_1.z.enum(["active", "pending", "all"]).optional().default("all"), -}); -/** - * Single-param schema used when an endpoint only needs the caller's address. - */ -exports.WalletAddressSchema = zod_1.z.object({ - walletAddress: primitives_1.StellarAddressSchema, -}); -/** - * Query-string parameters for the transaction history endpoint. - */ -exports.TransactionQuerySchema = zod_1.z.object({ - walletAddress: primitives_1.StellarAddressSchema, - limit: zod_1.z - .number() - .int("Limit must be a whole number") - .min(1, "Limit must be at least 1") - .max(200, "Limit may not exceed 200 records") - .optional() - .default(50), - order: zod_1.z.enum(["asc", "desc"]).optional().default("desc"), - type: zod_1.z.enum(["deposit", "withdrawal", "all"]).optional().default("all"), -}); -//# sourceMappingURL=requests.js.map \ No newline at end of file diff --git a/packages/api-schemas/src/requests.js.map b/packages/api-schemas/src/requests.js.map deleted file mode 100644 index 561385baa..000000000 --- a/packages/api-schemas/src/requests.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"requests.js","sourceRoot":"","sources":["requests.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,6CAMsB;AAEtB;;GAEG;AACU,QAAA,oBAAoB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC3C,aAAa,EAAE,iCAAoB;IACnC,MAAM,EAAE,yBAAY;IACpB,KAAK,EAAE,4BAAe;IACtB,WAAW,EAAE,8BAAiB;IAC9B,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACpC,CAAC,CAAC;AAIH;;GAEG;AACU,QAAA,uBAAuB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC9C,aAAa,EAAE,iCAAoB;IACnC,MAAM,EAAE,yBAAY;IACpB,KAAK,EAAE,4BAAe;IACtB,kBAAkB,EAAE,iCAAoB,CAAC,QAAQ,EAAE;IACnD,WAAW,EAAE,8BAAiB;CAC/B,CAAC,CAAC;AAIH;;GAEG;AACU,QAAA,uBAAuB,GAAG,OAAC;KACrC,MAAM,CAAC;IACN,IAAI,EAAE,yBAAY,CAAC,QAAQ,EAAE;IAC7B,EAAE,EAAE,yBAAY,CAAC,QAAQ,EAAE;IAC3B,KAAK,EAAE,OAAC;SACL,MAAM,EAAE;SACR,GAAG,CAAC,8BAA8B,CAAC;SACnC,GAAG,CAAC,CAAC,EAAE,0BAA0B,CAAC;SAClC,GAAG,CAAC,GAAG,EAAE,sCAAsC,CAAC;SAChD,QAAQ,EAAE;CACd,CAAC;KACD,MAAM,CACL,CAAC,KAAK,EAAE,EAAE;IACR,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,EAAE,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;IAChC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,EACD,EAAE,OAAO,EAAE,8CAA8C,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,CAC5E,CAAC;AAIJ;;GAEG;AACU,QAAA,oBAAoB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC3C,aAAa,EAAE,iCAAoB;IACnC,MAAM,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CACvE,CAAC,CAAC;AAIH;;GAEG;AACU,QAAA,mBAAmB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC1C,aAAa,EAAE,iCAAoB;CACpC,CAAC,CAAC;AAIH;;GAEG;AACU,QAAA,sBAAsB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC7C,aAAa,EAAE,iCAAoB;IACnC,KAAK,EAAE,OAAC;SACL,MAAM,EAAE;SACR,GAAG,CAAC,8BAA8B,CAAC;SACnC,GAAG,CAAC,CAAC,EAAE,0BAA0B,CAAC;SAClC,GAAG,CAAC,GAAG,EAAE,kCAAkC,CAAC;SAC5C,QAAQ,EAAE;SACV,OAAO,CAAC,EAAE,CAAC;IACd,KAAK,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;IACzD,IAAI,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CACzE,CAAC,CAAC"} \ No newline at end of file diff --git a/packages/api-schemas/src/responses.d.ts b/packages/api-schemas/src/responses.d.ts deleted file mode 100644 index aa98cb417..000000000 --- a/packages/api-schemas/src/responses.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod"; -/** Successful vault deposit / withdrawal response body. */ -export declare const VaultOperationResponseSchema: any; -export type VaultOperationResponse = z.infer; -//# sourceMappingURL=responses.d.ts.map \ No newline at end of file diff --git a/packages/api-schemas/src/responses.d.ts.map b/packages/api-schemas/src/responses.d.ts.map deleted file mode 100644 index a93951044..000000000 --- a/packages/api-schemas/src/responses.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["responses.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,2DAA2D;AAC3D,eAAO,MAAM,4BAA4B,KAW9B,CAAC;AAEZ,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC"} \ No newline at end of file diff --git a/packages/api-schemas/src/responses.js b/packages/api-schemas/src/responses.js deleted file mode 100644 index 404c60184..000000000 --- a/packages/api-schemas/src/responses.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.VaultOperationResponseSchema = void 0; -const zod_1 = require("zod"); -const primitives_1 = require("./primitives"); -/** Successful vault deposit / withdrawal response body. */ -exports.VaultOperationResponseSchema = zod_1.z - .object({ - id: zod_1.z.string(), - type: zod_1.z.enum(["deposit", "withdrawal"]), - amount: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]), - asset: primitives_1.AssetCodeSchema, - walletAddress: primitives_1.StellarAddressSchema, - transactionHash: zod_1.z.string(), - status: zod_1.z.string(), - timestamp: zod_1.z.string(), -}) - .strict(); -//# sourceMappingURL=responses.js.map \ No newline at end of file diff --git a/packages/api-schemas/src/responses.js.map b/packages/api-schemas/src/responses.js.map deleted file mode 100644 index 68641a5a6..000000000 --- a/packages/api-schemas/src/responses.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"responses.js","sourceRoot":"","sources":["responses.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,6CAAqE;AAErE,2DAA2D;AAC9C,QAAA,4BAA4B,GAAG,OAAC;KAC1C,MAAM,CAAC;IACN,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE;IACd,IAAI,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IACvC,MAAM,EAAE,OAAC,CAAC,KAAK,CAAC,CAAC,OAAC,CAAC,MAAM,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,KAAK,EAAE,4BAAe;IACtB,aAAa,EAAE,iCAAoB;IACnC,eAAe,EAAE,OAAC,CAAC,MAAM,EAAE;IAC3B,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE;IAClB,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE;CACtB,CAAC;KACD,MAAM,EAAE,CAAC"} \ No newline at end of file diff --git a/packages/api-schemas/src/vault.d.ts b/packages/api-schemas/src/vault.d.ts deleted file mode 100644 index 8dda4ee61..000000000 --- a/packages/api-schemas/src/vault.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { z } from "zod"; -/** - * POST /api/v1/vault/deposits request body (shared with frontend client). - * Accepts string or numeric amounts at the JSON boundary and normalizes to string. - */ -export declare const VaultDepositBodySchema: any; -export type VaultDepositBody = z.infer; -/** - * POST /api/v1/vault/withdrawals request body (shared with frontend client). - */ -export declare const VaultWithdrawalBodySchema: any; -export type VaultWithdrawalBody = z.infer; -/** Vault write body when wallet nonce enforcement is strict. */ -export declare const SignedVaultDepositBodySchema: any; -export declare const SignedVaultWithdrawalBodySchema: any; -/** @deprecated Use VaultDepositBodySchema or VaultWithdrawalBodySchema */ -export declare const VaultOperationSchema: any; -//# sourceMappingURL=vault.d.ts.map \ No newline at end of file diff --git a/packages/api-schemas/src/vault.d.ts.map b/packages/api-schemas/src/vault.d.ts.map deleted file mode 100644 index a2e1933df..000000000 --- a/packages/api-schemas/src/vault.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"vault.d.ts","sourceRoot":"","sources":["vault.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAgBxB;;;GAGG;AACH,eAAO,MAAM,sBAAsB,KAGxB,CAAC;AAEZ,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;GAEG;AACH,eAAO,MAAM,yBAAyB,KAG3B,CAAC;AAEZ,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,gEAAgE;AAChE,eAAO,MAAM,4BAA4B,KAK9B,CAAC;AAEZ,eAAO,MAAM,+BAA+B,KAKjC,CAAC;AAEZ,0EAA0E;AAC1E,eAAO,MAAM,oBAAoB,KAAyB,CAAC"} \ No newline at end of file diff --git a/packages/api-schemas/src/vault.js b/packages/api-schemas/src/vault.js deleted file mode 100644 index 4e2bbd9fa..000000000 --- a/packages/api-schemas/src/vault.js +++ /dev/null @@ -1,47 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.VaultOperationSchema = exports.SignedVaultWithdrawalBodySchema = exports.SignedVaultDepositBodySchema = exports.VaultWithdrawalBodySchema = exports.VaultDepositBodySchema = void 0; -const zod_1 = require("zod"); -const primitives_1 = require("./primitives"); -const requests_1 = require("./requests"); -const signedActionFields = { - nonce: zod_1.z.string().min(16).max(128), - signature: zod_1.z.string().min(32).max(512), -}; -const vaultOperationExtras = { - email: zod_1.z.string().email().optional(), - referralCode: zod_1.z.string().max(64).optional(), - nonce: signedActionFields.nonce.optional(), - signature: signedActionFields.signature.optional(), -}; -/** - * POST /api/v1/vault/deposits request body (shared with frontend client). - * Accepts string or numeric amounts at the JSON boundary and normalizes to string. - */ -exports.VaultDepositBodySchema = requests_1.DepositRequestSchema.extend({ - amount: primitives_1.AmountInputSchema, - ...vaultOperationExtras, -}).strict(); -/** - * POST /api/v1/vault/withdrawals request body (shared with frontend client). - */ -exports.VaultWithdrawalBodySchema = requests_1.WithdrawalRequestSchema.extend({ - amount: primitives_1.AmountInputSchema, - ...vaultOperationExtras, -}).strict(); -/** Vault write body when wallet nonce enforcement is strict. */ -exports.SignedVaultDepositBodySchema = requests_1.DepositRequestSchema.extend({ - amount: primitives_1.AmountInputSchema, - email: zod_1.z.string().email().optional(), - referralCode: zod_1.z.string().max(64).optional(), - ...signedActionFields, -}).strict(); -exports.SignedVaultWithdrawalBodySchema = requests_1.WithdrawalRequestSchema.extend({ - amount: primitives_1.AmountInputSchema, - email: zod_1.z.string().email().optional(), - referralCode: zod_1.z.string().max(64).optional(), - ...signedActionFields, -}).strict(); -/** @deprecated Use VaultDepositBodySchema or VaultWithdrawalBodySchema */ -exports.VaultOperationSchema = exports.VaultDepositBodySchema; -//# sourceMappingURL=vault.js.map \ No newline at end of file diff --git a/packages/api-schemas/src/vault.js.map b/packages/api-schemas/src/vault.js.map deleted file mode 100644 index 0888a095c..000000000 --- a/packages/api-schemas/src/vault.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"vault.js","sourceRoot":"","sources":["vault.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,6CAAiD;AACjD,yCAA2E;AAE3E,MAAM,kBAAkB,GAAG;IACzB,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAClC,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;CACvC,CAAC;AAEF,MAAM,oBAAoB,GAAG;IAC3B,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IACpC,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC3C,KAAK,EAAE,kBAAkB,CAAC,KAAK,CAAC,QAAQ,EAAE;IAC1C,SAAS,EAAE,kBAAkB,CAAC,SAAS,CAAC,QAAQ,EAAE;CACnD,CAAC;AAEF;;;GAGG;AACU,QAAA,sBAAsB,GAAG,+BAAoB,CAAC,MAAM,CAAC;IAChE,MAAM,EAAE,8BAAiB;IACzB,GAAG,oBAAoB;CACxB,CAAC,CAAC,MAAM,EAAE,CAAC;AAIZ;;GAEG;AACU,QAAA,yBAAyB,GAAG,kCAAuB,CAAC,MAAM,CAAC;IACtE,MAAM,EAAE,8BAAiB;IACzB,GAAG,oBAAoB;CACxB,CAAC,CAAC,MAAM,EAAE,CAAC;AAIZ,gEAAgE;AACnD,QAAA,4BAA4B,GAAG,+BAAoB,CAAC,MAAM,CAAC;IACtE,MAAM,EAAE,8BAAiB;IACzB,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IACpC,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC3C,GAAG,kBAAkB;CACtB,CAAC,CAAC,MAAM,EAAE,CAAC;AAEC,QAAA,+BAA+B,GAAG,kCAAuB,CAAC,MAAM,CAAC;IAC5E,MAAM,EAAE,8BAAiB;IACzB,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IACpC,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC3C,GAAG,kBAAkB;CACtB,CAAC,CAAC,MAAM,EAAE,CAAC;AAEZ,0EAA0E;AAC7D,QAAA,oBAAoB,GAAG,8BAAsB,CAAC"} \ No newline at end of file diff --git a/packages/api-schemas/src/webhookEvents.d.ts b/packages/api-schemas/src/webhookEvents.d.ts deleted file mode 100644 index 11155373a..000000000 --- a/packages/api-schemas/src/webhookEvents.d.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { z } from "zod"; -/** - * Every event type YieldVault can deliver to a registered webhook endpoint. - * - * This is the *outbound webhook* catalog — the events an off-chain HTTP - * consumer receives from the backend's delivery service - * (`backend/src/webhookDelivery.ts`). It is intentionally narrower than the - * on-chain Soroban contract event catalog documented in - * `docs/WEBHOOK_INTEGRATION.md`: the vault contract emits many distinct - * ledger events (admin rotation, emergency actions, fee changes, strategy - * bookkeeping, etc.), while webhooks surface the operational deposit, - * withdrawal, and strategy-change events consumers need for off-chain - * automation. - * - * Keep this list in sync with `TransactionEventType` in - * `backend/src/webhookDelivery.ts` — that file is the source of truth for - * what the server actually emits. - */ -export declare const WebhookEventTypeSchema: any; -export type WebhookEventType = z.infer; -/** - * Monotonically increasing schema version for the outbound webhook envelope. - * Mirrors `WEBHOOK_SCHEMA_VERSION` in `backend/src/webhookDelivery.ts`. - * Consumers should gate on `schemaVersion` for forward-compatibility rather - * than assuming the envelope shape is fixed. - */ -export declare const WEBHOOK_SCHEMA_VERSION = 1; -/** Payload for `transaction.deposit.created`. */ -export declare const TransactionDepositCreatedPayloadSchema: any; -export type TransactionDepositCreatedPayload = z.infer; -/** Payload for `transaction.withdrawal.created`. */ -export declare const TransactionWithdrawalCreatedPayloadSchema: any; -export type TransactionWithdrawalCreatedPayload = z.infer; -/** Maps each event type to its payload schema. Used to validate by discriminant. */ -export declare const WebhookEventPayloadSchemas: { - readonly "transaction.deposit.created": any; - readonly "transaction.withdrawal.created": any; - readonly "vault.deposit.created": any; - readonly "vault.withdrawal.created": any; - readonly "vault.strategy.changed": any; -}; -/** - * The full outbound envelope written to the wire and stored in - * dead-letter records. Matches `WebhookEnvelope` in - * `backend/src/webhookDelivery.ts`. - */ -export declare const WebhookEnvelopeSchema: any; -export type WebhookEnvelope = z.infer; -/** - * Parses and validates a raw webhook delivery body against the envelope - * schema, then re-validates `payload` against the schema specific to its - * `eventType`. Prefer this over `WebhookEnvelopeSchema.parse` directly so - * that payload drift for a specific event type is caught even if the - * generic envelope shape still matches. - */ -export declare function parseWebhookEnvelope(data: unknown): WebhookEnvelope; -//# sourceMappingURL=webhookEvents.d.ts.map diff --git a/packages/api-schemas/src/webhookEvents.d.ts.map b/packages/api-schemas/src/webhookEvents.d.ts.map deleted file mode 100644 index 77bad9ebf..000000000 --- a/packages/api-schemas/src/webhookEvents.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"webhookEvents.d.ts","sourceRoot":"","sources":["webhookEvents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,sBAAsB,KAGjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAoBxC,iDAAiD;AACjD,eAAO,MAAM,sCAAsC,KAChB,CAAC;AACpC,MAAM,MAAM,gCAAgC,GAAG,CAAC,CAAC,KAAK,CACpD,OAAO,sCAAsC,CAC9C,CAAC;AAEF,oDAAoD;AACpD,eAAO,MAAM,yCAAyC,KACnB,CAAC;AACpC,MAAM,MAAM,mCAAmC,GAAG,CAAC,CAAC,KAAK,CACvD,OAAO,yCAAyC,CACjD,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,0BAA0B;;;CAGoB,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,KAOvB,CAAC;AAEZ,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,eAAe,CAKnE"} \ No newline at end of file diff --git a/packages/api-schemas/src/webhookEvents.js b/packages/api-schemas/src/webhookEvents.js deleted file mode 100644 index 32d0c209c..000000000 --- a/packages/api-schemas/src/webhookEvents.js +++ /dev/null @@ -1,114 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.parseWebhookEnvelope = exports.WebhookEnvelopeSchema = exports.WebhookEventPayloadSchemas = exports.TransactionWithdrawalCreatedPayloadSchema = exports.TransactionDepositCreatedPayloadSchema = exports.WEBHOOK_SCHEMA_VERSION = exports.WebhookEventTypeSchema = void 0; -const zod_1 = require("zod"); -const primitives_1 = require("./primitives"); -/** - * Every event type YieldVault can deliver to a registered webhook endpoint. - * - * This is the *outbound webhook* catalog — the events an off-chain HTTP - * consumer receives from the backend's delivery service - * (`backend/src/webhookDelivery.ts`). It is intentionally narrower than the - * on-chain Soroban contract event catalog documented in - * `docs/WEBHOOK_INTEGRATION.md`: the vault contract emits many distinct - * ledger events (admin rotation, emergency actions, fee changes, strategy - * bookkeeping, etc.), while webhooks surface the operational deposit, - * withdrawal, and strategy-change events consumers need for off-chain - * automation. - * - * Keep this list in sync with `TransactionEventType` in - * `backend/src/webhookDelivery.ts` — that file is the source of truth for - * what the server actually emits. - */ -exports.WebhookEventTypeSchema = zod_1.z.enum([ - "transaction.deposit.created", - "transaction.withdrawal.created", - "vault.deposit.created", - "vault.withdrawal.created", - "vault.strategy.changed", -]); -/** - * Monotonically increasing schema version for the outbound webhook envelope. - * Mirrors `WEBHOOK_SCHEMA_VERSION` in `backend/src/webhookDelivery.ts`. - * Consumers should gate on `schemaVersion` for forward-compatibility rather - * than assuming the envelope shape is fixed. - */ -exports.WEBHOOK_SCHEMA_VERSION = 1; -/** - * Payload shared by every current transaction event. Both - * `transaction.deposit.created` and `transaction.withdrawal.created` use - * this same shape today; they are kept as separate schemas below so each - * event type can diverge independently as new fields are added. - */ -const BaseTransactionEventPayloadSchema = zod_1.z - .object({ - transactionId: zod_1.z.string().min(1), - amount: zod_1.z.string().min(1), - asset: primitives_1.AssetCodeSchema, - walletAddress: primitives_1.StellarAddressSchema, - transactionHash: zod_1.z.string().min(1), - status: zod_1.z.string().min(1), - timestamp: zod_1.z.iso.datetime(), -}) - .strict(); -const VaultEventPayloadSchema = BaseTransactionEventPayloadSchema.extend({ - vaultId: zod_1.z.string().min(1), -}); -const VaultStrategyChangedPayloadSchema = zod_1.z - .object({ - transactionId: zod_1.z.string().min(1), - amount: zod_1.z.string().min(1), - asset: primitives_1.AssetCodeSchema, - walletAddress: zod_1.z.string().min(1), - transactionHash: zod_1.z.string().min(1), - status: zod_1.z.string().min(1), - timestamp: zod_1.z.iso.datetime(), - vaultId: zod_1.z.string().min(1), - strategyId: zod_1.z.string().min(1), - previousStrategyId: zod_1.z.string().min(1).optional(), -}) - .strict(); -/** Payload for `transaction.deposit.created`. */ -exports.TransactionDepositCreatedPayloadSchema = BaseTransactionEventPayloadSchema; -/** Payload for `transaction.withdrawal.created`. */ -exports.TransactionWithdrawalCreatedPayloadSchema = BaseTransactionEventPayloadSchema; -/** Maps each event type to its payload schema. Used to validate by discriminant. */ -exports.WebhookEventPayloadSchemas = { - "transaction.deposit.created": exports.TransactionDepositCreatedPayloadSchema, - "transaction.withdrawal.created": exports.TransactionWithdrawalCreatedPayloadSchema, - "vault.deposit.created": VaultEventPayloadSchema, - "vault.withdrawal.created": VaultEventPayloadSchema, - "vault.strategy.changed": VaultStrategyChangedPayloadSchema, -}; -/** - * The full outbound envelope written to the wire and stored in - * dead-letter records. Matches `WebhookEnvelope` in - * `backend/src/webhookDelivery.ts`. - */ -exports.WebhookEnvelopeSchema = zod_1.z - .object({ - schemaVersion: zod_1.z.number().int().positive(), - eventType: exports.WebhookEventTypeSchema, - sentAt: zod_1.z.iso.datetime(), - payload: zod_1.z.union([ - BaseTransactionEventPayloadSchema, - VaultEventPayloadSchema, - VaultStrategyChangedPayloadSchema, - ]), -}) - .strict(); -/** - * Parses and validates a raw webhook delivery body against the envelope - * schema, then re-validates `payload` against the schema specific to its - * `eventType`. Prefer this over `WebhookEnvelopeSchema.parse` directly so - * that payload drift for a specific event type is caught even if the - * generic envelope shape still matches. - */ -function parseWebhookEnvelope(data) { - const envelope = exports.WebhookEnvelopeSchema.parse(data); - const payloadSchema = exports.WebhookEventPayloadSchemas[envelope.eventType]; - payloadSchema.parse(envelope.payload); - return envelope; -} -exports.parseWebhookEnvelope = parseWebhookEnvelope; -//# sourceMappingURL=webhookEvents.js.map diff --git a/packages/api-schemas/src/webhookEvents.js.map b/packages/api-schemas/src/webhookEvents.js.map deleted file mode 100644 index b5bdff984..000000000 --- a/packages/api-schemas/src/webhookEvents.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"webhookEvents.js","sourceRoot":"","sources":["webhookEvents.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,6CAAqE;AAErE;;;;;;;;;;;;;;;;;GAiBG;AACU,QAAA,sBAAsB,GAAG,OAAC,CAAC,IAAI,CAAC;IAC3C,6BAA6B;IAC7B,gCAAgC;CACjC,CAAC,CAAC;AAIH;;;;;GAKG;AACU,QAAA,sBAAsB,GAAG,CAAC,CAAC;AAExC;;;;;GAKG;AACH,MAAM,iCAAiC,GAAG,OAAC;KACxC,MAAM,CAAC;IACN,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAChC,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,KAAK,EAAE,4BAAe;IACtB,aAAa,EAAE,iCAAoB;IACnC,eAAe,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAClC,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,SAAS,EAAE,OAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;CAC5B,CAAC;KACD,MAAM,EAAE,CAAC;AAEZ,iDAAiD;AACpC,QAAA,sCAAsC,GACjD,iCAAiC,CAAC;AAKpC,oDAAoD;AACvC,QAAA,yCAAyC,GACpD,iCAAiC,CAAC;AAKpC,oFAAoF;AACvE,QAAA,0BAA0B,GAAG;IACxC,6BAA6B,EAAE,8CAAsC;IACrE,gCAAgC,EAAE,iDAAyC;CAClB,CAAC;AAE5D;;;;GAIG;AACU,QAAA,qBAAqB,GAAG,OAAC;KACnC,MAAM,CAAC;IACN,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IAC1C,SAAS,EAAE,8BAAsB;IACjC,MAAM,EAAE,OAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;IACxB,OAAO,EAAE,iCAAiC;CAC3C,CAAC;KACD,MAAM,EAAE,CAAC;AAIZ;;;;;;GAMG;AACH,SAAgB,oBAAoB,CAAC,IAAa;IAChD,MAAM,QAAQ,GAAG,6BAAqB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,aAAa,GAAG,kCAA0B,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IACrE,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACtC,OAAO,QAAQ,CAAC;AAClB,CAAC;AALD,oDAKC"} \ No newline at end of file diff --git a/scripts/lint-staged-runners.js b/scripts/lint-staged-runners.js index 688ec3577..a5ecfdcf2 100644 --- a/scripts/lint-staged-runners.js +++ b/scripts/lint-staged-runners.js @@ -115,7 +115,14 @@ function runEslint(files) { continue; } - const result = spawnSync(process.execPath, [launcher, '--fix', ...groups[pkg]], { + // lint-staged hands over repo-relative paths, but ESLint runs with the + // package directory as its cwd — strip the `/` prefix so the pattern + // resolves (otherwise ESLint reports "No files matching the pattern"). + const packageFiles = groups[pkg].map((file) => + file.startsWith(`${pkg}/`) ? file.slice(pkg.length + 1) : file, + ); + + const result = spawnSync(process.execPath, [launcher, '--fix', ...packageFiles], { cwd: path.join(ROOT, pkg), stdio: 'inherit', timeout: ESLINT_TIMEOUT_MS, From 279a25ae95fb305a2f7ef322be4fa744ea3900b7 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Mon, 19 Oct 2026 05:38:26 -0400 Subject: [PATCH 2/5] chore(ci): regenerate root package-lock.json so npm ci succeeds in ci (cherry picked from commit 4763ad4d6bf2060b41073f4bd051304559459a08) --- package-lock.json | 95 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 69f5517dd..214e2e521 100644 --- a/package-lock.json +++ b/package-lock.json @@ -468,7 +468,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -513,18 +512,43 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1237,6 +1261,40 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", @@ -1362,6 +1420,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1379,6 +1438,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1396,6 +1456,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1413,6 +1474,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1430,6 +1492,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1447,6 +1510,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1464,6 +1528,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1481,6 +1546,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1498,6 +1564,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1515,6 +1582,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1532,6 +1600,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1549,6 +1618,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1566,6 +1636,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1583,6 +1654,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1600,6 +1672,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1617,6 +1690,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1634,6 +1708,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1651,6 +1726,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1668,6 +1744,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -1685,6 +1762,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -2206,7 +2284,6 @@ "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -2803,7 +2880,6 @@ "version": "29.1.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", @@ -4004,7 +4080,6 @@ "version": "4.22.4", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.28.0" }, @@ -4067,7 +4142,8 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/util-extend": { "version": "1.0.3", @@ -4104,7 +4180,6 @@ "version": "8.1.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", From 337cbeb52fc6bc79ae161bec186e6ecbe3a98588 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Mon, 19 Oct 2026 06:03:37 -0400 Subject: [PATCH 3/5] fix(frontend): add admin route to prefetch imports (cherry picked from commit 1c5e8e46164d59b86afbb5b4201662a9a8b2f517) --- frontend/src/lib/routePrefetch.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/lib/routePrefetch.ts b/frontend/src/lib/routePrefetch.ts index 8c5c98636..dcc508038 100644 --- a/frontend/src/lib/routePrefetch.ts +++ b/frontend/src/lib/routePrefetch.ts @@ -8,6 +8,7 @@ export const routeImports = { "/compare": () => import("../pages/VaultComparison"), "/vault-health": () => import("../pages/VaultHealthDashboard"), "/settings": () => import("../pages/Settings"), + "/admin": () => import("../pages/Admin"), "/ui-kit": () => import("../pages/UIPreview"), } as const; @@ -39,6 +40,7 @@ export const HEAVY_ROUTES: readonly PrefetchableRoute[] = [ export const PREFETCHABLE_ROUTES: readonly PrefetchableRoute[] = [ ...PRIMARY_ROUTES, ...HEAVY_ROUTES, + "/admin", "/ui-kit", ]; From 1db3e80561e326ce95278fac4287465d2dda0347 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Mon, 19 Oct 2026 06:25:11 -0400 Subject: [PATCH 4/5] chore(ci): align settings form enum validation with the installed zod v4 API (cherry picked from commit dec5463c1e423415dafd1b1c5918ff96678baa4d) --- frontend/src/forms/schemas/settingsFormSchema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/forms/schemas/settingsFormSchema.ts b/frontend/src/forms/schemas/settingsFormSchema.ts index ca9184f15..ba4df6d51 100644 --- a/frontend/src/forms/schemas/settingsFormSchema.ts +++ b/frontend/src/forms/schemas/settingsFormSchema.ts @@ -13,7 +13,7 @@ export const settingsFormSchema = z.object({ message: "Slippage tolerance must be between 0.1% and 5.0%.", }), currency: z.enum(["USD", "EUR", "GBP", "XLM"], { - errorMap: () => ({ message: "Please select a supported currency." }), + error: () => ({ message: "Please select a supported currency." }), }), }); From 3566d9eeba39907faae6e19253e91410caf95d33 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Mon, 19 Oct 2026 07:27:15 -0400 Subject: [PATCH 5/5] fix(frontend): centralise Stellar endpoints in the network config Network passphrase, Horizon, explorer and price-API URLs were read or hard-coded in a dozen places, bypassing `config/network.ts`. - `config/network.ts` now owns `horizonUrl`, `explorerUrl`, `xlmPriceApiUrl`, per-network `horizonBaseUrls` / `stellarExplorerBaseUrls` and `getHorizonUrlForRpcUrl()`, each with a built-in default that can be overridden through `VITE_HORIZON_URL`, `VITE_STELLAR_EXPLORER_URL` and `VITE_XLM_PRICE_API_URL` - drop the direct `VITE_STELLAR_NETWORK_PASSPHRASE ?? ""` reads in `NetworkSwitchNotification`, `TransactionStatus` and `TransactionStatusModal`; they now derive the mode from `networkConfig.isTestnet` - route every Horizon call (`transactionApi`, `useTransactionTimeline`, `TransactionStatusModal`, `TransactionReceipt`, `stellarAccount`) through `networkConfig.horizonUrl` - `getStellarExplorerUrl()` builds links from `stellarExplorerBaseUrls`; `TransactionTimeline` uses it instead of inlining the URL and no longer renders a dead link for an invalid hash - move the CoinGecko price URL and the referral share endpoints into config modules - keep `ALLOWED_EXTERNAL_DOMAINS` in sync with the configured explorer and Horizon hosts so overrides do not break `sanitizeExternalLink` - document the new variables and their defaults in `.env.example` and `docs/ENV_VARIABLE_MATRIX.md` Closes 1626 --- ENVIRONMENT_SETUP_GUIDE.md | 2 +- docs/ENV_VARIABLE_MATRIX.md | 6 +- frontend/.env.example | 19 +++++ .../components/NetworkSwitchNotification.tsx | 11 ++- frontend/src/components/ShareModal.tsx | 7 +- frontend/src/components/TransactionStatus.tsx | 14 ++-- .../src/components/TransactionStatusModal.tsx | 20 +++--- .../components/TransactionTimeline.test.tsx | 16 ++++- .../src/components/TransactionTimeline.tsx | 16 ++++- frontend/src/config/externalLinks.ts | 9 +++ frontend/src/config/network.test.ts | 71 +++++++++++++++++++ frontend/src/config/network.ts | 71 +++++++++++++++++-- frontend/src/hooks/useTransactionTimeline.ts | 4 +- frontend/src/lib/security.ts | 41 +++++++---- frontend/src/lib/stellarAccount.ts | 20 ++---- frontend/src/lib/transactionApi.ts | 5 +- frontend/src/lib/vaultApi.ts | 2 +- .../src/pages/TransactionHistory.test.tsx | 10 ++- frontend/src/pages/TransactionReceipt.tsx | 18 ++--- 19 files changed, 274 insertions(+), 88 deletions(-) create mode 100644 frontend/src/config/externalLinks.ts create mode 100644 frontend/src/config/network.test.ts diff --git a/ENVIRONMENT_SETUP_GUIDE.md b/ENVIRONMENT_SETUP_GUIDE.md index e87e14449..f45777f89 100644 --- a/ENVIRONMENT_SETUP_GUIDE.md +++ b/ENVIRONMENT_SETUP_GUIDE.md @@ -142,7 +142,7 @@ That document covers 39 service-area tables with every variable's default, requi | [Backend — Rate Limiting](./docs/ENV_VARIABLE_MATRIX.md#8-backend--rate-limiting) | 16 | | [Backend — Webhook Delivery](./docs/ENV_VARIABLE_MATRIX.md#16-backend--webhook-delivery) | 11 | | [Backend — Withdrawal Recovery](./docs/ENV_VARIABLE_MATRIX.md#30-backend--withdrawal-partial-failure-recovery) | 8 | -| [Frontend — Stellar / Soroban](./docs/ENV_VARIABLE_MATRIX.md#33-frontend--stellar--soroban-vite-vite_-prefix) | 6 | +| [Frontend — Stellar / Soroban](./docs/ENV_VARIABLE_MATRIX.md#33-frontend--stellar--soroban-vite-vite_-prefix) | 8 | | [Frontend — Sentry Monitoring](./docs/ENV_VARIABLE_MATRIX.md#36-frontend--sentry-error-monitoring) | 7 | | [Environment-by-Environment Summary](./docs/ENV_VARIABLE_MATRIX.md#environment-by-environment-summary) | 16 | | [Minimum Required Sets](./docs/ENV_VARIABLE_MATRIX.md#minimum-required-sets) | — | diff --git a/docs/ENV_VARIABLE_MATRIX.md b/docs/ENV_VARIABLE_MATRIX.md index f601e2be4..0dfac0982 100644 --- a/docs/ENV_VARIABLE_MATRIX.md +++ b/docs/ENV_VARIABLE_MATRIX.md @@ -426,10 +426,12 @@ These are consumed by `scripts/fund-testnet-account.js` only — not by the runn | Variable | Default | Req. | Description | Source | |---|---|---|---|---| | `VITE_SOROBAN_RPC_URL` | `https://soroban-testnet.stellar.org` | ✅ always | Soroban RPC endpoint for frontend | `frontend/src/config/network.ts`, `lib/stellarAccount.ts` | -| `VITE_STELLAR_NETWORK_PASSPHRASE` | `Test SDF Network ; September 2015` | ✅ always | Network passphrase for transaction building | `frontend/src/config/network.ts`, `components/TransactionStatus.tsx` | +| `VITE_STELLAR_NETWORK_PASSPHRASE` | `Test SDF Network ; September 2015` | ✅ always | Network passphrase for transaction building | `frontend/src/config/network.ts` | | `VITE_VAULT_CONTRACT_ID` | _(empty)_ | ✅ always | Vault contract ID for client-side reads | `frontend/src/config/network.ts` | | `VITE_USDC_ISSUER` | _(empty)_ | ⬜ optional | USDC issuer public key | `frontend/src/lib/stellarAccount.ts` | -| `VITE_HORIZON_URL` | testnet horizon | ⬜ optional | Horizon URL for account data | `frontend/src/lib/stellarAccount.ts` | +| `VITE_HORIZON_URL` | testnet horizon (`https://horizon-testnet.stellar.org`); mainnet default `https://horizon.stellar.org` | ⬜ optional | Horizon endpoint for account and transaction data | `frontend/src/config/network.ts` | +| `VITE_STELLAR_EXPLORER_URL` | testnet `https://stellar.expert/explorer/testnet`; mainnet default `https://stellar.expert/explorer/public` | ⬜ optional | Stellar Expert base URL for transaction/ledger links | `frontend/src/config/network.ts`, `lib/security.ts` | +| `VITE_XLM_PRICE_API_URL` | `https://api.coingecko.com/api/v3/simple/price?ids=stellar&vs_currencies=usd` | ⬜ optional | XLM/USD reference-rate API | `frontend/src/config/network.ts`, `lib/vaultApi.ts` | | `VITE_E2E_STUB_BALANCES` | _(empty)_ | ⬜ optional | Enable stub balances for E2E tests | `frontend/src/lib/vaultApi.ts`, `hooks/useTokenAllowance.ts`, `hooks/useBalanceData.ts` | --- diff --git a/frontend/.env.example b/frontend/.env.example index a9b4710ad..ffc0de455 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -6,6 +6,25 @@ VITE_STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015 VITE_VAULT_CONTRACT_ID= VITE_FF_ANALYTICS_PAGE=true +# Optional endpoint overrides. Every variable below has a built-in default that +# is picked from VITE_STELLAR_NETWORK_PASSPHRASE, so leaving them blank keeps +# the public testnet/mainnet endpoints: +# +# VITE_HORIZON_URL Horizon endpoint used for account and transaction +# lookups. +# default: https://horizon-testnet.stellar.org (testnet) +# https://horizon.stellar.org (mainnet) +# VITE_STELLAR_EXPLORER_URL Stellar Expert base URL used for transaction and +# ledger links (see frontend/src/lib/security.ts). +# default: https://stellar.expert/explorer/testnet (testnet) +# https://stellar.expert/explorer/public (mainnet) +# VITE_XLM_PRICE_API_URL XLM/USD reference-rate API used for fiat estimates. +# default: https://api.coingecko.com/api/v3/simple/price?ids=stellar&vs_currencies=usd +# +#VITE_HORIZON_URL=https://horizon-testnet.stellar.org +#VITE_STELLAR_EXPLORER_URL=https://stellar.expert/explorer/testnet +#VITE_XLM_PRICE_API_URL=https://api.coingecko.com/api/v3/simple/price?ids=stellar&vs_currencies=usd + # Comma-separated wallet addresses granted the "admin" role in the frontend # nav/route guards (see frontend/src/lib/roles.ts). Optional – leave blank to # disable the admin console for all wallets. Not a security boundary: this diff --git a/frontend/src/components/NetworkSwitchNotification.tsx b/frontend/src/components/NetworkSwitchNotification.tsx index cc7afae80..6d9b17e11 100644 --- a/frontend/src/components/NetworkSwitchNotification.tsx +++ b/frontend/src/components/NetworkSwitchNotification.tsx @@ -7,11 +7,6 @@ interface NetworkSwitchNotificationProps { onSwitchNetwork?: (network: "testnet" | "mainnet") => Promise; } -function detectExpectedNetwork(): "testnet" | "mainnet" { - const passphrase = import.meta.env.VITE_STELLAR_NETWORK_PASSPHRASE ?? ""; - return passphrase.includes("Public") ? "mainnet" : "testnet"; -} - const NetworkSwitchNotification: React.FC = ({ walletAddress, onSwitchNetwork, @@ -56,10 +51,12 @@ const NetworkSwitchNotification: React.FC = ({ if (!isMismatch || isDismissed) return null; const walletNet = walletNetwork || "Unknown"; - const appNet = expectedNetwork || detectExpectedNetwork(); + // `expectedNetwork` comes from `networkConfig` inside `useWalletNetwork`. + const appNet = expectedNetwork; + const isAppTestnet = appNet.toLowerCase() === "testnet"; const getManualSteps = () => { - if (appNet === "testnet") { + if (isAppTestnet) { return [ "Open your Stellar wallet extension", 'Navigate to network settings or preferences', diff --git a/frontend/src/components/ShareModal.tsx b/frontend/src/components/ShareModal.tsx index 79663468a..a0da8ab5e 100644 --- a/frontend/src/components/ShareModal.tsx +++ b/frontend/src/components/ShareModal.tsx @@ -3,6 +3,7 @@ import { Share2, Copy, Check, MessageCircle, Twitter, Facebook } from "./icons"; import Modal from "./Modal"; import { copyTextToClipboard } from "../lib/clipboard"; import { useToast } from "../context/ToastContext"; +import { socialShareBaseUrls } from "../config/externalLinks"; interface ShareModalProps { isOpen: boolean; @@ -41,7 +42,7 @@ const ShareModal: React.FC = ({ { name: "Twitter", icon: Twitter, - url: `https://twitter.com/intent/tweet?text=${encodeURIComponent( + url: `${socialShareBaseUrls.twitter}?text=${encodeURIComponent( `Join me on YieldVault and earn stable yields on real-world assets! Use my referral code: ${referralCode}\n\n${referralLink}` )}`, color: "#1DA1F2", @@ -49,13 +50,13 @@ const ShareModal: React.FC = ({ { name: "Facebook", icon: Facebook, - url: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(referralLink)}`, + url: `${socialShareBaseUrls.facebook}?u=${encodeURIComponent(referralLink)}`, color: "#4267B2", }, { name: "Telegram", icon: MessageCircle, - url: `https://t.me/share/url?url=${encodeURIComponent(referralLink)}&text=${encodeURIComponent( + url: `${socialShareBaseUrls.telegram}?url=${encodeURIComponent(referralLink)}&text=${encodeURIComponent( `Join me on YieldVault and earn stable yields! Use my referral code: ${referralCode}` )}`, color: "#0088CC", diff --git a/frontend/src/components/TransactionStatus.tsx b/frontend/src/components/TransactionStatus.tsx index 1beaae637..1186a8062 100644 --- a/frontend/src/components/TransactionStatus.tsx +++ b/frontend/src/components/TransactionStatus.tsx @@ -1,6 +1,7 @@ import React from "react"; import { AlertCircle, Check, Loader2 } from "./icons"; import { getStellarExplorerUrl, sanitizeExternalLink } from "../lib/security"; +import { networkConfig } from "../config/network"; export type ActionStatusState = "idle" | "pending" | "success" | "failure"; @@ -16,14 +17,6 @@ interface TransactionStatusProps { status: ActionStatus; } -function resolveNetworkMode(): "testnet" | "mainnet" { - const networkPassphrase = - import.meta.env.VITE_STELLAR_NETWORK_PASSPHRASE ?? ""; - return networkPassphrase.toLowerCase().includes("public") - ? "mainnet" - : "testnet"; -} - const STATE_STYLES: Record< Exclude, { border: string; icon: React.ReactNode } @@ -50,7 +43,10 @@ const TransactionStatus: React.FC = ({ status }) => { const stateStyles = STATE_STYLES[status.state]; const explorerUrl = status.txHash ? sanitizeExternalLink( - getStellarExplorerUrl(status.txHash, resolveNetworkMode()), + getStellarExplorerUrl( + status.txHash, + networkConfig.isTestnet ? "testnet" : "mainnet", + ), ) : null; diff --git a/frontend/src/components/TransactionStatusModal.tsx b/frontend/src/components/TransactionStatusModal.tsx index c67f03058..7d78b1663 100644 --- a/frontend/src/components/TransactionStatusModal.tsx +++ b/frontend/src/components/TransactionStatusModal.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState, useCallback } from "react"; import { createPortal } from "react-dom"; import { Loader2, Check, AlertCircle, Copy, X } from "./icons"; import { getStellarExplorerUrl, sanitizeExternalLink } from "../lib/security"; +import { networkConfig } from "../config/network"; export type TransactionStatusState = "submitting" | "confirming" | "success" | "failure"; @@ -17,16 +18,6 @@ interface TransactionStatusModalProps { mockMode?: boolean; } -const HORIZON_BASE_URL = "https://horizon-testnet.stellar.org"; - -function resolveNetworkMode(): "testnet" | "mainnet" { - const networkPassphrase = - import.meta.env.VITE_STELLAR_NETWORK_PASSPHRASE ?? ""; - return networkPassphrase.toLowerCase().includes("public") - ? "mainnet" - : "testnet"; -} - const TransactionStatusModal: React.FC = ({ isOpen, onClose, @@ -105,7 +96,7 @@ const TransactionStatusModal: React.FC = ({ // Check transaction status on Stellar Horizon const checkTxStatus = useCallback(async (hash: string): Promise => { - const url = `${HORIZON_BASE_URL}/transactions/${hash}`; + const url = `${networkConfig.horizonUrl}/transactions/${hash}`; try { const response = await fetch(url); if (response.ok) { @@ -223,7 +214,12 @@ const TransactionStatusModal: React.FC = ({ }; const explorerUrl = txHash - ? sanitizeExternalLink(getStellarExplorerUrl(txHash, resolveNetworkMode())) + ? sanitizeExternalLink( + getStellarExplorerUrl( + txHash, + networkConfig.isTestnet ? "testnet" : "mainnet", + ), + ) : null; return createPortal( diff --git a/frontend/src/components/TransactionTimeline.test.tsx b/frontend/src/components/TransactionTimeline.test.tsx index 90bfa5682..216d18dd6 100644 --- a/frontend/src/components/TransactionTimeline.test.tsx +++ b/frontend/src/components/TransactionTimeline.test.tsx @@ -1,6 +1,10 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import TransactionTimeline from "./TransactionTimeline"; +import { getStellarExplorerUrl } from "../lib/security"; + +/** 64-char hex hash that passes `isValidTransactionHash`. */ +const VALID_TX_HASH = "a1b2c3d4".repeat(8); describe("TransactionTimeline", () => { it("renders aria label", () => { @@ -25,7 +29,7 @@ describe("TransactionTimeline", () => { render( , ); // Two "Finalized" labels: one in the steps list (completed) and one in the final row @@ -33,11 +37,19 @@ describe("TransactionTimeline", () => { expect(finalizedLabels.length).toBeGreaterThanOrEqual(1); const explorerLink = screen.getByRole("link", { name: /View on Stellar Explorer/i }); - expect(explorerLink).toHaveAttribute("href", expect.stringContaining("abc123")); + expect(explorerLink).toHaveAttribute( + "href", + getStellarExplorerUrl(VALID_TX_HASH, "testnet"), + ); expect(explorerLink).toHaveAttribute("target", "_blank"); expect(explorerLink).toHaveAttribute("rel", "noopener noreferrer"); }); + it("does not render an explorer link for an invalid transaction hash", () => { + render(); + expect(screen.queryByRole("link")).not.toBeInTheDocument(); + }); + it("does not render explorer link when finalized without txHash", () => { render(); expect(screen.queryByRole("link")).not.toBeInTheDocument(); diff --git a/frontend/src/components/TransactionTimeline.tsx b/frontend/src/components/TransactionTimeline.tsx index 021cbb8f9..7efebd614 100644 --- a/frontend/src/components/TransactionTimeline.tsx +++ b/frontend/src/components/TransactionTimeline.tsx @@ -1,6 +1,8 @@ import React from "react"; import { Check, AlertCircle, Loader2 } from "./icons"; import { useTranslation } from "../i18n"; +import { networkConfig } from "../config/network"; +import { getStellarExplorerUrl } from "../lib/security"; export type TxTimelineStatus = "preparing" | "signing" | "submitting" | "pending" | "confirming" | "finalized" | "failed"; @@ -80,6 +82,16 @@ const TransactionTimeline: React.FC = ({ ? STEPS // show all steps, the failed one will be marked failed based on order : STEPS; + // `getStellarExplorerUrl` returns "#" for hashes that fail validation; never + // render a dead link in that case. + const explorerUrl = txHash + ? getStellarExplorerUrl( + txHash, + networkConfig.isTestnet ? "testnet" : "mainnet", + ) + : null; + const hasExplorerLink = Boolean(explorerUrl && explorerUrl !== "#"); + return (
= ({ ? errorMessage : t(status === "finalized" ? "txTimeline.steps.finalized.desc" : "txTimeline.steps.failed.desc")}

- {status === "finalized" && txHash && ( + {status === "finalized" && hasExplorerLink && ( { + vi.unstubAllEnvs(); + vi.resetModules(); +}); + +describe("networkConfig", () => { + it("falls back to the public testnet endpoints and defaults", async () => { + const { networkConfig, horizonBaseUrls, stellarExplorerBaseUrls } = + await import("./network"); + + expect(networkConfig.isTestnet).toBe(true); + expect(networkConfig.rpcUrl).toBe("https://soroban-testnet.stellar.org"); + expect(networkConfig.horizonUrl).toBe(horizonBaseUrls.testnet); + expect(networkConfig.horizonUrl).toBe("https://horizon-testnet.stellar.org"); + expect(networkConfig.explorerUrl).toBe(stellarExplorerBaseUrls.testnet); + expect(networkConfig.explorerUrl).toBe( + "https://stellar.expert/explorer/testnet", + ); + expect(networkConfig.xlmPriceApiUrl).toBe( + "https://api.coingecko.com/api/v3/simple/price?ids=stellar&vs_currencies=usd", + ); + }); + + it("derives mainnet endpoints from the mainnet passphrase", async () => { + vi.stubEnv( + "VITE_STELLAR_NETWORK_PASSPHRASE", + "Public Global Stellar Network ; September 2015", + ); + vi.resetModules(); + + const { networkConfig } = await import("./network"); + + expect(networkConfig.isTestnet).toBe(false); + expect(networkConfig.horizonUrl).toBe("https://horizon.stellar.org"); + expect(networkConfig.explorerUrl).toBe( + "https://stellar.expert/explorer/public", + ); + }); + + it("lets VITE_ variables override the endpoint defaults", async () => { + vi.stubEnv("VITE_HORIZON_URL", "https://horizon.example.test"); + vi.stubEnv("VITE_STELLAR_EXPLORER_URL", "https://explorer.example.test"); + vi.stubEnv("VITE_XLM_PRICE_API_URL", "https://price.example.test/xlm"); + vi.resetModules(); + + const { networkConfig } = await import("./network"); + + expect(networkConfig.horizonUrl).toBe("https://horizon.example.test"); + expect(networkConfig.explorerUrl).toBe("https://explorer.example.test"); + expect(networkConfig.xlmPriceApiUrl).toBe("https://price.example.test/xlm"); + }); + + it("maps a Soroban RPC URL onto the matching Horizon endpoint", async () => { + const { getHorizonUrlForRpcUrl, horizonBaseUrls } = await import( + "./network" + ); + + expect(getHorizonUrlForRpcUrl("https://soroban-testnet.stellar.org")).toBe( + horizonBaseUrls.testnet, + ); + expect(getHorizonUrlForRpcUrl("https://soroban-mainnet.stellar.org")).toBe( + horizonBaseUrls.mainnet, + ); + // Unknown hosts fall back to the configured network. + expect(getHorizonUrlForRpcUrl("https://rpc.example.test")).toBe( + horizonBaseUrls.testnet, + ); + }); +}); diff --git a/frontend/src/config/network.ts b/frontend/src/config/network.ts index 0eff496c7..82e3e7d06 100644 --- a/frontend/src/config/network.ts +++ b/frontend/src/config/network.ts @@ -1,21 +1,82 @@ const PUBLIC_SOROBAN_RPC = "https://soroban-testnet.stellar.org"; const TESTNET_PASSPHRASE = "Test SDF Network ; September 2015"; +/** Soroban RPC host fragments, used to map an RPC URL onto a network. */ +const TESTNET_SOROBAN_RPC_HOST = "soroban-testnet.stellar.org"; +const MAINNET_SOROBAN_RPC_HOST = "soroban-mainnet.stellar.org"; + +/** + * Public defaults for the testnet/mainnet endpoints. Every one of them can be + * overridden through the matching `VITE_` variable (see `.env.example`). + */ +const TESTNET_HORIZON_URL = "https://horizon-testnet.stellar.org"; +const MAINNET_HORIZON_URL = "https://horizon.stellar.org"; +const TESTNET_EXPLORER_URL = "https://stellar.expert/explorer/testnet"; +const MAINNET_EXPLORER_URL = "https://stellar.expert/explorer/public"; +const XLM_PRICE_API_URL = + "https://api.coingecko.com/api/v3/simple/price?ids=stellar&vs_currencies=usd"; + +export type StellarNetwork = "testnet" | "mainnet"; + +const networkPassphrase = + import.meta.env.VITE_STELLAR_NETWORK_PASSPHRASE || TESTNET_PASSPHRASE; + +/** + * Horizon endpoints per network. `VITE_HORIZON_URL` replaces the default of the + * network the build is configured for (it is also the value returned by + * `networkConfig.horizonUrl`). + */ +export const horizonBaseUrls: Record = { + testnet: import.meta.env.VITE_HORIZON_URL || TESTNET_HORIZON_URL, + mainnet: import.meta.env.VITE_HORIZON_URL || MAINNET_HORIZON_URL, +}; + +/** + * Stellar Expert base URLs per network. `VITE_STELLAR_EXPLORER_URL` replaces the + * default of the network the build is configured for. + */ +export const stellarExplorerBaseUrls: Record = { + testnet: import.meta.env.VITE_STELLAR_EXPLORER_URL || TESTNET_EXPLORER_URL, + mainnet: import.meta.env.VITE_STELLAR_EXPLORER_URL || MAINNET_EXPLORER_URL, +}; + export interface NetworkConfig { rpcUrl: string; networkPassphrase: string; contractId: string; isTestnet: boolean; + /** Horizon endpoint for the configured network. */ + horizonUrl: string; + /** Stellar Expert base URL for the configured network. */ + explorerUrl: string; + /** Price API endpoint used for the XLM/USD reference rate. */ + xlmPriceApiUrl: string; } +const isTestnet = networkPassphrase === TESTNET_PASSPHRASE; + export const networkConfig: NetworkConfig = { rpcUrl: import.meta.env.VITE_SOROBAN_RPC_URL || PUBLIC_SOROBAN_RPC, - networkPassphrase: - import.meta.env.VITE_STELLAR_NETWORK_PASSPHRASE || TESTNET_PASSPHRASE, + networkPassphrase, contractId: import.meta.env.VITE_VAULT_CONTRACT_ID || "", - isTestnet: - (import.meta.env.VITE_STELLAR_NETWORK_PASSPHRASE || TESTNET_PASSPHRASE) === - TESTNET_PASSPHRASE, + isTestnet, + horizonUrl: horizonBaseUrls[isTestnet ? "testnet" : "mainnet"], + explorerUrl: stellarExplorerBaseUrls[isTestnet ? "testnet" : "mainnet"], + xlmPriceApiUrl: import.meta.env.VITE_XLM_PRICE_API_URL || XLM_PRICE_API_URL, }; +/** + * Resolve the Horizon endpoint that matches a Soroban RPC URL. Unknown hosts + * fall back to the Horizon endpoint of the configured network. + */ +export function getHorizonUrlForRpcUrl(rpcUrl: string): string { + if (rpcUrl.includes(TESTNET_SOROBAN_RPC_HOST)) { + return horizonBaseUrls.testnet; + } + if (rpcUrl.includes(MAINNET_SOROBAN_RPC_HOST)) { + return horizonBaseUrls.mainnet; + } + return networkConfig.horizonUrl; +} + export const hasCustomRpcConfig = Boolean(import.meta.env.VITE_SOROBAN_RPC_URL); diff --git a/frontend/src/hooks/useTransactionTimeline.ts b/frontend/src/hooks/useTransactionTimeline.ts index bfe041858..3aef48ac8 100644 --- a/frontend/src/hooks/useTransactionTimeline.ts +++ b/frontend/src/hooks/useTransactionTimeline.ts @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import type { TxTimelineStatus } from "../components/TransactionTimeline"; +import { networkConfig } from "../config/network"; -const HORIZON_BASE = "https://horizon-testnet.stellar.org"; const POLL_INTERVAL_MS = 3000; const MAX_POLL_ATTEMPTS = 40; // ~2 minutes @@ -26,7 +26,7 @@ interface UseTransactionTimelineResult { } async function fetchTxStatus(hash: string): Promise<"finalized" | "failed" | "pending"> { - const res = await fetch(`${HORIZON_BASE}/transactions/${hash}`); + const res = await fetch(`${networkConfig.horizonUrl}/transactions/${hash}`); if (res.status === 404) return "pending"; if (!res.ok) throw new Error(`Horizon error: ${res.status}`); const data = (await res.json()) as HorizonTxResponse; diff --git a/frontend/src/lib/security.ts b/frontend/src/lib/security.ts index ff7575daf..ee0d132f0 100644 --- a/frontend/src/lib/security.ts +++ b/frontend/src/lib/security.ts @@ -1,6 +1,7 @@ /** * Security utilities for XSS prevention and input validation */ +import { networkConfig, stellarExplorerBaseUrls } from "../config/network"; /** * Validates Stellar transaction hash format @@ -49,7 +50,9 @@ export function sanitizeExternalUrl( } /** - * Sanitizes a transaction hash and constructs a safe Stellar Explorer URL + * Sanitizes a transaction hash and constructs a safe Stellar Explorer URL. + * Explorer base URLs come from the network config so a self-hosted explorer + * only needs a single `VITE_STELLAR_EXPLORER_URL` override. */ export function getStellarExplorerUrl( transactionHash: string, @@ -58,12 +61,8 @@ export function getStellarExplorerUrl( if (!isValidTransactionHash(transactionHash)) { return '#'; } - - const baseUrl = network === 'mainnet' - ? 'https://stellar.expert/explorer/public' - : 'https://stellar.expert/explorer/testnet'; - - return `${baseUrl}/tx/${transactionHash}`; + + return `${stellarExplorerBaseUrls[network]}/tx/${transactionHash}`; } /** @@ -100,14 +99,30 @@ export function truncateSafe(text: string, maxLength: number): string { return text.slice(0, maxLength) + '...'; } +function hostnameOf(url: string): string | null { + try { + return new URL(url).hostname; + } catch { + return null; + } +} + /** - * Allowed domains for external links + * Allowed domains for external links. The configured Horizon and explorer hosts + * are included so overriding them through `VITE_` variables does not silently + * break outbound links (both defaults are already listed). */ -export const ALLOWED_EXTERNAL_DOMAINS = [ - 'stellar.expert', - 'horizon.stellar.org', - 'horizon-testnet.stellar.org', - 'stellar.org', +export const ALLOWED_EXTERNAL_DOMAINS: string[] = [ + ...new Set( + [ + 'stellar.expert', + 'horizon.stellar.org', + 'horizon-testnet.stellar.org', + 'stellar.org', + hostnameOf(networkConfig.explorerUrl), + hostnameOf(networkConfig.horizonUrl), + ].filter((domain): domain is string => domain !== null), + ), ]; /** diff --git a/frontend/src/lib/stellarAccount.ts b/frontend/src/lib/stellarAccount.ts index 9f4b59fcc..b6b76a12e 100644 --- a/frontend/src/lib/stellarAccount.ts +++ b/frontend/src/lib/stellarAccount.ts @@ -1,10 +1,6 @@ import { getAddress, isAllowed } from "@stellar/freighter-api"; import { Horizon } from "@stellar/stellar-sdk"; - -const TESTNET_SOROBAN_RPC = "soroban-testnet.stellar.org"; -const MAINNET_SOROBAN_RPC = "soroban-mainnet.stellar.org"; -const TESTNET_HORIZON = "https://horizon-testnet.stellar.org"; -const MAINNET_HORIZON = "https://horizon.stellar.org"; +import { getHorizonUrlForRpcUrl, networkConfig } from "../config/network"; const USDC_CODE = "USDC"; const USDC_ISSUER = import.meta.env.VITE_USDC_ISSUER; @@ -17,12 +13,6 @@ function isCreditBalance( return balance.asset_type === "credit_alphanum4" || balance.asset_type === "credit_alphanum12"; } -function toHorizonUrl(rpcUrl: string): string { - if (rpcUrl.includes(TESTNET_SOROBAN_RPC)) return TESTNET_HORIZON; - if (rpcUrl.includes(MAINNET_SOROBAN_RPC)) return MAINNET_HORIZON; - return import.meta.env.VITE_HORIZON_URL || TESTNET_HORIZON; -} - export async function discoverConnectedAddress(): Promise { try { const allowed = await isAllowed(); @@ -58,9 +48,9 @@ export async function discoverConnectedAddressWithRetry( export async function fetchUsdcBalance( walletAddress: string, - rpcUrl = import.meta.env.VITE_SOROBAN_RPC_URL || `https://${TESTNET_SOROBAN_RPC}`, + rpcUrl = networkConfig.rpcUrl, ): Promise { - const horizonUrl = toHorizonUrl(rpcUrl); + const horizonUrl = getHorizonUrlForRpcUrl(rpcUrl); const ServerFactory = Horizon.Server as unknown as { new (url: string): { accounts: () => { accountId: (id: string) => { call: () => Promise<{ balances: Array }> } }; @@ -90,9 +80,9 @@ export async function fetchUsdcBalance( export async function fetchXlmBalance( walletAddress: string, - rpcUrl = import.meta.env.VITE_SOROBAN_RPC_URL || `https://${TESTNET_SOROBAN_RPC}`, + rpcUrl = networkConfig.rpcUrl, ): Promise { - const horizonUrl = toHorizonUrl(rpcUrl); + const horizonUrl = getHorizonUrlForRpcUrl(rpcUrl); const server = new Horizon.Server(horizonUrl); const account = await server.accounts().accountId(walletAddress).call(); diff --git a/frontend/src/lib/transactionApi.ts b/frontend/src/lib/transactionApi.ts index 2a8398ee6..dba03ec3c 100644 --- a/frontend/src/lib/transactionApi.ts +++ b/frontend/src/lib/transactionApi.ts @@ -1,5 +1,6 @@ import { validate, TransactionQuerySchema } from "./api"; import { formatNumber } from "./formatters"; +import { networkConfig } from "../config/network"; import type { TransactionQueryInput } from "./api/schemas"; export type TxType = "deposit" | "withdrawal" | "transfer" | "trade"; @@ -60,13 +61,11 @@ export function normalizeOperation( }; } -const HORIZON_BASE_URL = "https://horizon-testnet.stellar.org"; - export async function getTransactions( params: TransactionQueryInput, ): Promise { const query = validate(TransactionQuerySchema, params, "TransactionQuery"); - const url = `${HORIZON_BASE_URL}/accounts/${query.walletAddress}/operations?limit=${query.limit}&order=${query.order}`; + const url = `${networkConfig.horizonUrl}/accounts/${query.walletAddress}/operations?limit=${query.limit}&order=${query.order}`; const response = await fetch(url); if (!response.ok) { diff --git a/frontend/src/lib/vaultApi.ts b/frontend/src/lib/vaultApi.ts index ec0532d37..ec603a464 100644 --- a/frontend/src/lib/vaultApi.ts +++ b/frontend/src/lib/vaultApi.ts @@ -272,7 +272,7 @@ export async function submitWithdrawal( export async function getXlmPrice(): Promise { try { - const response = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=stellar&vs_currencies=usd"); + const response = await fetch(networkConfig.xlmPriceApiUrl); const data = await response.json(); return data.stellar.usd; } catch (error) { diff --git a/frontend/src/pages/TransactionHistory.test.tsx b/frontend/src/pages/TransactionHistory.test.tsx index c6ca5a065..7be886046 100644 --- a/frontend/src/pages/TransactionHistory.test.tsx +++ b/frontend/src/pages/TransactionHistory.test.tsx @@ -29,9 +29,13 @@ const mockNetworkConfig = vi.hoisted(() => ({ contractId: "", })); -vi.mock("../config/network", () => ({ - networkConfig: mockNetworkConfig, -})); +vi.mock("../config/network", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + networkConfig: mockNetworkConfig, + }; +}); // Mock the transactionApi module vi.mock("../lib/transactionApi", async (importOriginal) => { diff --git a/frontend/src/pages/TransactionReceipt.tsx b/frontend/src/pages/TransactionReceipt.tsx index 17b6938ae..389c633f2 100644 --- a/frontend/src/pages/TransactionReceipt.tsx +++ b/frontend/src/pages/TransactionReceipt.tsx @@ -2,9 +2,8 @@ import { useEffect, useState, useCallback } from "react"; import { useParams, Link } from "react-router-dom"; import EmptyState from "../components/ui/EmptyState"; import { usePageHeadingFocus } from "../hooks/usePageHeadingFocus"; - -const HORIZON_BASE = "https://horizon-testnet.stellar.org"; -const EXPLORER_BASE = "https://stellar.expert/explorer/testnet/tx"; +import { getStellarExplorerUrl } from "../lib/security"; +import { networkConfig } from "../config/network"; interface TxDetails { hash: string; @@ -55,8 +54,8 @@ export default function TransactionReceipt() { try { const [txRes, opsRes] = await Promise.all([ - fetch(`${HORIZON_BASE}/transactions/${txHash}`), - fetch(`${HORIZON_BASE}/transactions/${txHash}/operations`), + fetch(`${networkConfig.horizonUrl}/transactions/${txHash}`), + fetch(`${networkConfig.horizonUrl}/transactions/${txHash}/operations`), ]); if (!txRes.ok) { @@ -93,8 +92,11 @@ export default function TransactionReceipt() { status: "completed", confirmations: txData.ledger ? Math.max(1, txData.ledger) : 1, ledger: txData.ledger, - explorerUrl: `${EXPLORER_BASE}/${txHash}`, - horizonUrl: `${HORIZON_BASE}/transactions/${txHash}`, + explorerUrl: getStellarExplorerUrl( + txHash, + networkConfig.isTestnet ? "testnet" : "mainnet", + ), + horizonUrl: `${networkConfig.horizonUrl}/transactions/${txHash}`, }); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load transaction"); @@ -244,7 +246,7 @@ export default function TransactionReceipt() {
Ledger