-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
438 additions
and
166 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,282 @@ | ||
import type { VercelResponse } from "@vercel/node"; | ||
import { AxiosError } from "axios"; | ||
import { StructError } from "superstruct"; | ||
import { relayFeeCalculator, typeguards } from "@across-protocol/sdk"; | ||
import { ethers } from "ethers"; | ||
|
||
type AcrossApiErrorCodeKey = keyof typeof AcrossErrorCode; | ||
|
||
export const HttpErrorToStatusCode = { | ||
BAD_REQUEST: 400, | ||
UNAUTHORIZED: 401, | ||
FORBIDDEN: 403, | ||
NOT_FOUND: 404, | ||
METHOD_NOT_ALLOWED: 405, | ||
REQUEST_TIMEOUT: 408, | ||
CONFLICT: 409, | ||
TOO_MANY_REQUESTS: 429, | ||
INTERNAL_SERVER_ERROR: 500, | ||
BAD_GATEWAY: 502, | ||
SERVICE_UNAVAILABLE: 503, | ||
GATEWAY_TIMEOUT: 504, | ||
} as const; | ||
|
||
export const AcrossErrorCode = { | ||
// Status: 40X | ||
INVALID_PARAM: "INVALID_PARAM", | ||
MISSING_PARAM: "MISSING_PARAM", | ||
SIMULATION_ERROR: "SIMULATION_ERROR", | ||
AMOUNT_TOO_LOW: "AMOUNT_TOO_LOW", | ||
AMOUNT_TOO_HIGH: "AMOUNT_TOO_HIGH", | ||
ROUTE_NOT_ENABLED: "ROUTE_NOT_ENABLED", | ||
|
||
// Status: 50X | ||
UPSTREAM_RPC_ERROR: "UPSTREAM_RPC_ERROR", | ||
UPSTREAM_HTTP_ERROR: "UPSTREAM_HTTP_ERROR", | ||
} as const; | ||
|
||
export class AcrossApiError extends Error { | ||
code?: AcrossApiErrorCodeKey; | ||
status: number; | ||
message: string; | ||
param?: string; | ||
|
||
constructor( | ||
args: { | ||
code?: AcrossApiErrorCodeKey; | ||
status: number; | ||
message: string; | ||
param?: string; | ||
}, | ||
opts?: ErrorOptions | ||
) { | ||
super(args.message, opts); | ||
this.code = args.code; | ||
this.status = args.status; | ||
this.message = args.message; | ||
this.param = args.param; | ||
} | ||
|
||
toJSON() { | ||
return { | ||
type: "AcrossApiError", | ||
code: this.code, | ||
status: this.status, | ||
message: this.message, | ||
param: this.param, | ||
}; | ||
} | ||
} | ||
|
||
export class InputError extends AcrossApiError { | ||
constructor( | ||
args: { | ||
message: string; | ||
code: AcrossApiErrorCodeKey; | ||
param?: string; | ||
}, | ||
opts?: ErrorOptions | ||
) { | ||
super( | ||
{ | ||
...args, | ||
status: HttpErrorToStatusCode.BAD_REQUEST, | ||
}, | ||
opts | ||
); | ||
} | ||
} | ||
|
||
export class InvalidParamError extends InputError { | ||
constructor(args: { message: string; param?: string }) { | ||
super({ | ||
message: args.message, | ||
code: AcrossErrorCode.INVALID_PARAM, | ||
param: args.param, | ||
}); | ||
} | ||
} | ||
|
||
export class MissingParamError extends InputError { | ||
constructor(args: { message: string; param?: string }) { | ||
super({ | ||
message: args.message, | ||
code: AcrossErrorCode.MISSING_PARAM, | ||
param: args.param, | ||
}); | ||
} | ||
} | ||
|
||
export class SimulationError extends InputError { | ||
constructor(args: { message: string }, opts?: ErrorOptions) { | ||
super( | ||
{ | ||
message: args.message, | ||
code: AcrossErrorCode.SIMULATION_ERROR, | ||
}, | ||
opts | ||
); | ||
} | ||
} | ||
|
||
export class RouteNotEnabledError extends InputError { | ||
constructor(args: { message: string }, opts?: ErrorOptions) { | ||
super( | ||
{ | ||
message: args.message, | ||
code: AcrossErrorCode.ROUTE_NOT_ENABLED, | ||
}, | ||
opts | ||
); | ||
} | ||
} | ||
|
||
export class AmountTooLowError extends InputError { | ||
constructor(args: { message: string }, opts?: ErrorOptions) { | ||
super( | ||
{ | ||
message: args.message, | ||
code: AcrossErrorCode.AMOUNT_TOO_LOW, | ||
}, | ||
opts | ||
); | ||
} | ||
} | ||
|
||
export class AmountTooHighError extends InputError { | ||
constructor(args: { message: string }, opts?: ErrorOptions) { | ||
super( | ||
{ | ||
message: args.message, | ||
code: AcrossErrorCode.AMOUNT_TOO_HIGH, | ||
}, | ||
opts | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Handles the recurring case of error handling | ||
* @param endpoint A string numeric to indicate to the logging utility where this error occurs | ||
* @param response A VercelResponse object that is used to interract with the returning reponse | ||
* @param logger A logging utility to write to a cloud logging provider | ||
* @param error The error that will be returned to the user | ||
* @returns The `response` input with a status/send sent. Note: using this object again will cause an exception | ||
*/ | ||
export function handleErrorCondition( | ||
endpoint: string, | ||
response: VercelResponse, | ||
logger: relayFeeCalculator.Logger, | ||
error: unknown | ||
): VercelResponse { | ||
let acrossApiError: AcrossApiError; | ||
|
||
// Handle superstruct validation errors | ||
if (error instanceof StructError) { | ||
const { type, path } = error; | ||
// Sanitize the error message that will be sent to client | ||
const message = `Invalid parameter at path '${path}'. Expected type '${type}'`; | ||
acrossApiError = new InputError({ | ||
message, | ||
code: AcrossErrorCode.INVALID_PARAM, | ||
param: path.join("."), | ||
}); | ||
} | ||
// Handle axios errors | ||
else if (error instanceof AxiosError) { | ||
const { response } = error; | ||
|
||
// If upstream error is an AcrossApiError, we just return it | ||
if (response?.data?.type === "AcrossApiError") { | ||
acrossApiError = new AcrossApiError( | ||
{ | ||
message: response.data.message, | ||
status: response.data.status, | ||
code: response.data.code, | ||
param: response.data.param, | ||
}, | ||
{ cause: error } | ||
); | ||
} else { | ||
const message = `Upstream http request to ${error.request?.url} failed with ${error.status} ${error.message}`; | ||
acrossApiError = new AcrossApiError( | ||
{ | ||
message, | ||
status: HttpErrorToStatusCode.BAD_GATEWAY, | ||
code: AcrossErrorCode.UPSTREAM_HTTP_ERROR, | ||
}, | ||
{ cause: error } | ||
); | ||
} | ||
} | ||
// Handle ethers errors | ||
else if (typeguards.isEthersError(error)) { | ||
acrossApiError = resolveEthersError(error); | ||
} | ||
// Handle other errors | ||
else if (error instanceof Error) { | ||
acrossApiError = new AcrossApiError( | ||
{ | ||
message: error.message, | ||
status: HttpErrorToStatusCode.INTERNAL_SERVER_ERROR, | ||
}, | ||
{ cause: error } | ||
); | ||
} else { | ||
acrossApiError = error as AcrossApiError; | ||
} | ||
|
||
const logLevel = acrossApiError.status >= 500 ? "error" : "warn"; | ||
logger[logLevel]({ | ||
at: endpoint, | ||
message: `${acrossApiError.code}: ${acrossApiError.message}`, | ||
}); | ||
|
||
return response.status(acrossApiError.status).json(acrossApiError); | ||
} | ||
|
||
export function resolveEthersError(err: unknown) { | ||
if (!typeguards.isEthersError(err)) { | ||
return new AcrossApiError( | ||
{ | ||
message: err instanceof Error ? err.message : "Unknown error", | ||
status: HttpErrorToStatusCode.INTERNAL_SERVER_ERROR, | ||
code: AcrossErrorCode.UPSTREAM_RPC_ERROR, | ||
}, | ||
{ cause: err } | ||
); | ||
} | ||
|
||
const { reason, code } = err; | ||
const method = "method" in err ? (err.method as string) : undefined; | ||
|
||
// Simulation errors | ||
if ( | ||
method === "estimateGas" && | ||
(code === ethers.utils.Logger.errors.UNPREDICTABLE_GAS_LIMIT || | ||
code === ethers.utils.Logger.errors.CALL_EXCEPTION) | ||
) { | ||
const rpcErrorMessageMatch = reason | ||
.replace(/\\/g, "") | ||
.match(/"message":"((?:[^"\\]|\\.)*)"/); | ||
const rpcErrorMessage = rpcErrorMessageMatch | ||
? rpcErrorMessageMatch[1] | ||
: reason; | ||
|
||
return new SimulationError( | ||
{ | ||
message: rpcErrorMessage, | ||
}, | ||
{ cause: err } | ||
); | ||
} | ||
|
||
return new AcrossApiError( | ||
{ | ||
message: `${err.reason}: ${err.code} - ${err.error}`, | ||
status: HttpErrorToStatusCode.INTERNAL_SERVER_ERROR, | ||
code: AcrossErrorCode.UPSTREAM_RPC_ERROR, | ||
}, | ||
{ cause: err } | ||
); | ||
} |
Oops, something went wrong.