From f01d80e1d9a87da520c2868bb5cfe3aca102f45d Mon Sep 17 00:00:00 2001 From: dtopenclaw <265700638+dtopenclaw@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:44:32 +0000 Subject: [PATCH] fix: preserve purchase validation errors --- src/app/api/purchase/route.ts | 22 ++++++++++++++++------ src/hooks/api/use_local_api.tsx | 7 +++++-- src/hooks/use_application.tsx | 19 ++++++++++++++++++- src/types/errors.ts | 19 +++++++++++++++++++ src/utils/error.ts | 10 ++++++++++ 5 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/app/api/purchase/route.ts b/src/app/api/purchase/route.ts index 1039494..e76470e 100644 --- a/src/app/api/purchase/route.ts +++ b/src/app/api/purchase/route.ts @@ -4,7 +4,7 @@ import { nanoid } from "nanoid"; import { FlightLib__factory, FlightOracle__factory, FlightProduct__factory, FlightUSD__factory } from "../../../contracts/flight"; import { IPolicyService__factory } from "../../../contracts/gif"; import { IBundleService__factory, IPoolService__factory } from "../../../contracts/gif/factories/pool"; -import { AirportBlacklistedError, AirportNotWhitelistedError, TransactionFailedException } from "../../../types/errors"; +import { AirportBlacklistedError, AirportNotWhitelistedError, FlightNotFoundError, InconsistentFlightDataError, TransactionFailedException } from "../../../types/errors"; import { Airport } from "../../../types/flightstats/airport"; import { ApplicationData, PermitData, PurchaseRequest } from "../../../types/purchase_request"; import { LOGGER } from "../../../utils/logger_backend"; @@ -54,6 +54,16 @@ export async function POST(request: Request) { error: "AIRPORT_NOT_WHITELISTED", message: err.message, }, { status: 400 }); + } else if (err instanceof FlightNotFoundError) { + return Response.json({ + error: "NO_FLIGHT_FOUND", + message: err.message, + }, { status: 404 }); + } else if (err instanceof InconsistentFlightDataError) { + return Response.json({ + error: "INCONSISTENT_DATA", + message: err.message, + }, { status: 400 }); } else { // @ts-expect-error balance error if (err.message === "BALANCE_ERROR") { @@ -141,7 +151,7 @@ async function validateFlightPlan(reqId: string, application: ApplicationData) { } catch (err) { // @ts-expect-error error has field message LOGGER.error(err.message); - throw new Error(`[${reqId}] Flight not found`); + throw new FlightNotFoundError(`[${reqId}] Flight not found`); } } @@ -157,13 +167,13 @@ async function validateStatistics(reqId: string, application: ApplicationData) { const fsResponse = await fetch(url); if (!fsResponse.ok) { - throw new Error(`[${reqId}] Flight not found on flightstats api`); + throw new FlightNotFoundError(`[${reqId}] Flight not found on flightstats api`); } const fsData = await fsResponse.json(); if (fsData.ratings === undefined || fsData.ratings.length === 0) { - throw new Error(`[${reqId}] Flight ratings not found`); + throw new FlightNotFoundError(`[${reqId}] Flight ratings not found`); } const rating = fsData.ratings[0] as Rating; @@ -172,12 +182,12 @@ async function validateStatistics(reqId: string, application: ApplicationData) { // compare stats vs application statistics if (stats.length !== application.statistics.length) { - throw new Error(`[${reqId}] Statistics length mismatch`); + throw new InconsistentFlightDataError(`[${reqId}] Statistics length mismatch`); } LOGGER.debug(`stats: ${JSON.stringify(stats)}, application statistics: ${JSON.stringify(application.statistics)}`); if (!stats.every((value, index) => value === application.statistics[index])) { - throw new Error(`[${reqId}] Statistics mismatch`); + throw new InconsistentFlightDataError(`[${reqId}] Statistics mismatch`); } } diff --git a/src/hooks/api/use_local_api.tsx b/src/hooks/api/use_local_api.tsx index 054b70f..2d572ce 100644 --- a/src/hooks/api/use_local_api.tsx +++ b/src/hooks/api/use_local_api.tsx @@ -1,5 +1,6 @@ +import { PurchaseErrorCode } from "../../types/errors"; import { ApplicationData, PermitData } from "../../types/purchase_request"; -import { PurchaseFailedError, PurchaseNotPossibleError } from "../../utils/error"; +import { PurchaseFailedError, PurchaseNotPossibleError, PurchaseValidationError } from "../../utils/error"; // @ts-expect-error BigInt is not defined in the global scope BigInt.prototype.toJSON = function () { @@ -29,8 +30,10 @@ export function useLocalApi() { throw new PurchaseFailedError(result.transaction, result.decodedError); } else if (result.error === "BALANCE_ERROR") { throw new PurchaseNotPossibleError(); + } else if (Object.values(PurchaseErrorCode).includes(result.error as PurchaseErrorCode)) { + throw new PurchaseValidationError(result.error as PurchaseErrorCode); } else { - throw new Error(`Error sending purchase protection request: ${result.statusText}`); + throw new Error(result.message || `Error sending purchase protection request: ${res.statusText}`); } } diff --git a/src/hooks/use_application.tsx b/src/hooks/use_application.tsx index 5fe98a5..1acbe8a 100644 --- a/src/hooks/use_application.tsx +++ b/src/hooks/use_application.tsx @@ -8,7 +8,8 @@ import { resetPurchase, setExecuting, setPolicy, setSigning } from "../redux/sli import { RootState } from "../redux/store"; import { Erc20PermitSignature } from "../types/erc20permitsignature"; import { ApplicationData, PermitData } from "../types/purchase_request"; -import { PurchaseFailedError, PurchaseNotPossibleError } from "../utils/error"; +import { PurchaseErrorCode } from "../types/errors"; +import { PurchaseFailedError, PurchaseNotPossibleError, PurchaseValidationError } from "../utils/error"; import { useLocalApi } from "./api/use_local_api"; import { useERC20Contract } from "./onchain/use_erc20_contract"; import { useFlightDelayProductContract } from "./onchain/use_flightdelay_product"; @@ -167,6 +168,22 @@ export default function useApplication() { dispatch(setError({ message: `${t("error.purchase_failed")} (${err.decodedError?.reason || "unknown error"})`, level: "error" })); } else if (err instanceof PurchaseNotPossibleError) { dispatch(setError({ message: t("error.purchase_currently_not_possible"), level: "error" })); + } else if (err instanceof PurchaseValidationError) { + switch (err.code) { + case PurchaseErrorCode.NO_FLIGHT_FOUND: + dispatch(setError({ message: t("error.no_flight_found"), level: "error" })); + break; + case PurchaseErrorCode.INCONSISTENT_DATA: + dispatch(setError({ message: t("error.inconsistent_data"), level: "error" })); + break; + case PurchaseErrorCode.AIRPORT_BLACKLISTED: + case PurchaseErrorCode.AIRPORT_NOT_WHITELISTED: + dispatch(setError({ message: t("error.purchase_currently_not_possible"), level: "error" })); + break; + default: + dispatch(setError({ message: t("error.unknown_error"), level: "error" })); + break; + } } else { // @ts-expect-error code is custom field for metamask error if (err.code !== undefined) { diff --git a/src/types/errors.ts b/src/types/errors.ts index 97beb73..6a084f1 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -9,6 +9,25 @@ export enum Reason { NOT_ENOUGH_CAPACITY, } +export enum PurchaseErrorCode { + NO_FLIGHT_FOUND = "NO_FLIGHT_FOUND", + INCONSISTENT_DATA = "INCONSISTENT_DATA", + AIRPORT_BLACKLISTED = "AIRPORT_BLACKLISTED", + AIRPORT_NOT_WHITELISTED = "AIRPORT_NOT_WHITELISTED", +} + +export class FlightNotFoundError extends Error { + constructor(message = 'Flight not found') { + super(message); + } +} + +export class InconsistentFlightDataError extends Error { + constructor(message = 'Inconsistent flight data') { + super(message); + } +} + /** * Exception thrown when a transaction fails. Contains the transaction receipt in field `transaction`. */ diff --git a/src/utils/error.ts b/src/utils/error.ts index d3c609c..dab87e9 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -1,4 +1,5 @@ import { DecodedError } from "ethers-decode-error"; +import { PurchaseErrorCode } from "../types/errors"; export function ensureError(value: unknown): Error { if (value instanceof Error) return value; @@ -51,6 +52,15 @@ export class PurchaseNotPossibleError extends Error { } } +export class PurchaseValidationError extends Error { + code: PurchaseErrorCode; + + constructor(code: PurchaseErrorCode) { + super(`Purchase validation failed: ${code}`); + this.code = code; + } +} + export class CapacityError extends Error { constructor() { super("Capacity error");