diff --git a/components/Claim/index.tsx b/components/Claim/index.tsx index 0b6795d7..da0b0dbf 100644 --- a/components/Claim/index.tsx +++ b/components/Claim/index.tsx @@ -225,11 +225,17 @@ const Claim = () => { fees: migrationParams.fees.toString(), }), }); + + if (!res.ok) { + const error = await res.json().catch(() => null); + throw new Error(error?.error || "Failed to generate proof"); + } + const proof = await res.json(); setProof(proof); } catch (e) { - console.log(e); + console.error(e); throw new Error((e as Error)?.message); } }} diff --git a/lib/api/errors.ts b/lib/api/errors.ts new file mode 100644 index 00000000..90c86ea0 --- /dev/null +++ b/lib/api/errors.ts @@ -0,0 +1,64 @@ +import { NextApiResponse } from "next"; + +import { ApiError, ErrorCode } from "./types/api-error"; + +export const apiError = ( + res: NextApiResponse, + status: number, + code: ErrorCode, + error: string, + details?: string +) => { + console.error(`[API Error] ${code}: ${error}`, details ?? ""); + const response = { error, code, details } as ApiError; + res.status(status).json(response); + return response; +}; + +export const badRequest = ( + res: NextApiResponse, + message: string, + details?: string +) => apiError(res, 400, "VALIDATION_ERROR", message, details); + +export const notFound = ( + res: NextApiResponse, + message: string, + details?: string +) => apiError(res, 404, "NOT_FOUND", message, details); + +export const internalError = (res: NextApiResponse, err?: unknown) => + apiError( + res, + 500, + "INTERNAL_ERROR", + "Internal server error", + err instanceof Error ? err.message : undefined + ); + +export const externalApiError = ( + res: NextApiResponse, + service: string, + details?: string +) => + apiError( + res, + 502, + "EXTERNAL_API_ERROR", + `Failed to fetch from ${service}`, + details + ); + +export const methodNotAllowed = ( + res: NextApiResponse, + method: string, + allowed: string[] +) => { + res.setHeader("Allow", allowed); + return apiError( + res, + 405, + "METHOD_NOT_ALLOWED", + `Method ${method} Not Allowed` + ); +}; diff --git a/lib/api/types/api-error.ts b/lib/api/types/api-error.ts new file mode 100644 index 00000000..7db19b98 --- /dev/null +++ b/lib/api/types/api-error.ts @@ -0,0 +1,12 @@ +export interface ApiError { + error: string; + code: ErrorCode; + details?: string; +} + +export type ErrorCode = + | "INTERNAL_ERROR" + | "VALIDATION_ERROR" + | "NOT_FOUND" + | "EXTERNAL_API_ERROR" + | "METHOD_NOT_ALLOWED"; diff --git a/lib/axios.ts b/lib/axios.ts index a35e91d8..a5355e2d 100644 --- a/lib/axios.ts +++ b/lib/axios.ts @@ -6,4 +6,14 @@ export const axiosClient = defaultAxios.create({ }); export const fetcher = (url: string) => - axiosClient.get(url).then((res) => res.data); + axiosClient + .get(url) + .then((res) => res.data) + .catch((err) => { + const apiError = err.response?.data; + if (apiError?.code) { + const errorMessage = apiError.error ?? "An unknown error occurred"; + throw new Error(`${apiError.code}: ${errorMessage}`); + } + throw err; + }); diff --git a/pages/api/account-balance/[address].tsx b/pages/api/account-balance/[address].tsx index beb44556..0eab65b5 100644 --- a/pages/api/account-balance/[address].tsx +++ b/pages/api/account-balance/[address].tsx @@ -4,6 +4,7 @@ import { getBondingManagerAddress, getLivepeerTokenAddress, } from "@lib/api/contracts"; +import { badRequest, internalError, methodNotAllowed } from "@lib/api/errors"; import { AccountBalance } from "@lib/api/types/get-account-balance"; import { l2PublicClient } from "@lib/chains"; import { NextApiRequest, NextApiResponse } from "next"; @@ -47,15 +48,13 @@ const handler = async ( return res.status(200).json(accountBalance); } else { - return res.status(500).end("Invalid ID"); + return badRequest(res, "Invalid address format"); } } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/changefeed.tsx b/pages/api/changefeed.tsx index 04d5247a..55e1ebe7 100644 --- a/pages/api/changefeed.tsx +++ b/pages/api/changefeed.tsx @@ -1,4 +1,9 @@ import { getCacheControlHeader } from "@lib/api"; +import { + externalApiError, + internalError, + methodNotAllowed, +} from "@lib/api/errors"; import { fetchWithRetry } from "@lib/fetchWithRetry"; import type { NextApiRequest, NextApiResponse } from "next"; @@ -25,27 +30,41 @@ const query = ` `; const changefeed = async (_req: NextApiRequest, res: NextApiResponse) => { - const response = await fetchWithRetry( - "https://changefeed.app/graphql", - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${process.env.CHANGEFEED_ACCESS_TOKEN!}`, - }, - body: JSON.stringify({ query }), - }, - { - retryOnMethods: ["POST"], - } - ); + try { + const method = _req.method; + + if (method === "GET") { + const response = await fetchWithRetry( + "https://changefeed.app/graphql", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.CHANGEFEED_ACCESS_TOKEN!}`, + }, + body: JSON.stringify({ query }), + }, + { + retryOnMethods: ["POST"], + } + ); + + if (!response.ok) { + return externalApiError(res, "changefeed.app"); + } + + res.setHeader("Cache-Control", getCacheControlHeader("hour")); - res.setHeader("Cache-Control", getCacheControlHeader("hour")); + const { + data: { projectBySlugs }, + } = await response.json(); + return res.status(200).json(projectBySlugs); + } - const { - data: { projectBySlugs }, - } = await response.json(); - res.json(projectBySlugs); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); + } catch (err) { + return internalError(res, err); + } }; export default changefeed; diff --git a/pages/api/contracts.tsx b/pages/api/contracts.tsx index 3ae1dbf3..0422e594 100644 --- a/pages/api/contracts.tsx +++ b/pages/api/contracts.tsx @@ -5,6 +5,7 @@ import { getLivepeerGovernorAddress, getTreasuryAddress, } from "@lib/api/contracts"; +import { internalError, methodNotAllowed } from "@lib/api/errors"; import { ContractInfo } from "@lib/api/types/get-contract-info"; import { CHAIN_INFO, DEFAULT_CHAIN_ID } from "@lib/chains"; import { NextApiRequest, NextApiResponse } from "next"; @@ -126,11 +127,9 @@ const handler = async ( return res.status(200).json(contractsInfo); } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/current-round.tsx b/pages/api/current-round.tsx index 0a367da0..d0a64b58 100644 --- a/pages/api/current-round.tsx +++ b/pages/api/current-round.tsx @@ -1,4 +1,5 @@ import { getCacheControlHeader, getCurrentRound } from "@lib/api"; +import { internalError, methodNotAllowed } from "@lib/api/errors"; import { CurrentRoundInfo } from "@lib/api/types/get-current-round"; import { l1PublicClient } from "@lib/chains"; import { NextApiRequest, NextApiResponse } from "next"; @@ -19,11 +20,11 @@ const handler = async ( const currentRound = protocol?.currentRound; if (!currentRound) { - return res.status(500).end("No current round found"); + throw new Error("No current round found"); } if (!_meta?.block) { - return res.status(500).end("No block number found"); + throw new Error("No block number found"); } const { id, startBlock, initialized } = currentRound; @@ -42,11 +43,9 @@ const handler = async ( return res.status(200).json(roundInfo); } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/ens-data/[address].tsx b/pages/api/ens-data/[address].tsx index 1422cbcf..7d4cb178 100644 --- a/pages/api/ens-data/[address].tsx +++ b/pages/api/ens-data/[address].tsx @@ -1,5 +1,6 @@ import { getCacheControlHeader } from "@lib/api"; import { getEnsForAddress } from "@lib/api/ens"; +import { badRequest, internalError, methodNotAllowed } from "@lib/api/errors"; import { EnsIdentity } from "@lib/api/types/get-ens"; import { NextApiRequest, NextApiResponse } from "next"; import { Address, isAddress } from "viem"; @@ -30,15 +31,13 @@ const handler = async ( return res.status(200).json(ens); } else { - return res.status(500).end("Invalid ID"); + return badRequest(res, "Invalid address format"); } } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/ens-data/image/[name].tsx b/pages/api/ens-data/image/[name].tsx index c77ab713..d2be5f8b 100644 --- a/pages/api/ens-data/image/[name].tsx +++ b/pages/api/ens-data/image/[name].tsx @@ -1,4 +1,10 @@ import { getCacheControlHeader } from "@lib/api"; +import { + badRequest, + internalError, + methodNotAllowed, + notFound, +} from "@lib/api/errors"; import { l1PublicClient } from "@lib/chains"; import { parseArweaveTxId, parseCid } from "livepeer/utils"; import { NextApiRequest, NextApiResponse } from "next"; @@ -47,18 +53,16 @@ const handler = async ( return res.end(Buffer.from(arrayBuffer)); } catch (e) { console.error(e); - return res.status(404).end("Invalid name"); + return notFound(res, "ENS avatar not found"); } } else { - return res.status(500).end("Invalid name"); + return badRequest(res, "Invalid ENS name"); } } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/ens-data/index.tsx b/pages/api/ens-data/index.tsx index 73f911ac..51ae19c8 100644 --- a/pages/api/ens-data/index.tsx +++ b/pages/api/ens-data/index.tsx @@ -1,5 +1,6 @@ import { getCacheControlHeader } from "@lib/api"; import { getEnsForAddress } from "@lib/api/ens"; +import { internalError, methodNotAllowed } from "@lib/api/errors"; import { EnsIdentity } from "@lib/api/types/get-ens"; import { CHAIN_INFO, DEFAULT_CHAIN_ID } from "@lib/chains"; import { fetchWithRetry } from "@lib/fetchWithRetry"; @@ -67,11 +68,9 @@ const handler = async ( return res.status(200).json(ensAddresses); } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/generateProof.tsx b/pages/api/generateProof.tsx index 38615aba..70221aeb 100644 --- a/pages/api/generateProof.tsx +++ b/pages/api/generateProof.tsx @@ -1,3 +1,4 @@ +import { badRequest, internalError, methodNotAllowed } from "@lib/api/errors"; import { DEFAULT_CHAIN_ID } from "@lib/chains"; import { EarningsTree } from "@lib/earningsTree"; import { utils } from "ethers"; @@ -8,23 +9,42 @@ import delegatorClaimSnapshot from "../../data/delegatorClaimSnapshot.json"; import delegatorClaimSnapshotRinkeby from "../../data/delegatorClaimSnapshotRinkeby.json"; const generateProof = async (_req: NextApiRequest, res: NextApiResponse) => { - const { account, delegate, stake, fees } = _req.body; - // generate the merkle tree from JSON - const tree = EarningsTree.fromJSON( - DEFAULT_CHAIN_ID === arbitrum.id - ? JSON.stringify(delegatorClaimSnapshot) - : JSON.stringify(delegatorClaimSnapshotRinkeby) - ); - - // generate the proof - const leaf = utils.solidityPack( - ["address", "address", "uint256", "uint256"], - [account, delegate, stake, fees] - ); - - const proof = tree.getHexProof(leaf); - - res.json(proof); + try { + const method = _req.method; + + if (method === "POST") { + const { account, delegate, stake, fees } = _req.body; + + if (!account || !delegate || stake === undefined || fees === undefined) { + return badRequest( + res, + "Missing required parameters", + "account, delegate, stake, and fees are required" + ); + } + + // generate the merkle tree from JSON + const tree = EarningsTree.fromJSON( + DEFAULT_CHAIN_ID === arbitrum.id + ? JSON.stringify(delegatorClaimSnapshot) + : JSON.stringify(delegatorClaimSnapshotRinkeby) + ); + + // generate the proof + const leaf = utils.solidityPack( + ["address", "address", "uint256", "uint256"], + [account, delegate, stake, fees] + ); + + const proof = tree.getHexProof(leaf); + + return res.status(200).json(proof); + } + + return methodNotAllowed(res, method ?? "unknown", ["POST"]); + } catch (err) { + return internalError(res, err); + } }; export default generateProof; diff --git a/pages/api/l1-delegator/[address].tsx b/pages/api/l1-delegator/[address].tsx index 60e3d41a..d9a38666 100644 --- a/pages/api/l1-delegator/[address].tsx +++ b/pages/api/l1-delegator/[address].tsx @@ -2,6 +2,7 @@ import { getCacheControlHeader } from "@lib/api"; import { bondingManager } from "@lib/api/abis/main/BondingManager"; import { controller } from "@lib/api/abis/main/Controller"; import { roundsManager } from "@lib/api/abis/main/RoundsManager"; +import { badRequest, internalError, methodNotAllowed } from "@lib/api/errors"; import { L1Delegator, UnbondingLock } from "@lib/api/types/get-l1-delegator"; import { CHAIN_INFO, L1_CHAIN_ID, l1PublicClient } from "@lib/chains"; import { EMPTY_ADDRESS } from "@lib/utils"; @@ -113,15 +114,13 @@ const handler = async ( return res.status(200).json(l1Delegator); } else { - return res.status(500).end("Invalid ID"); + return badRequest(res, "Invalid address format"); } } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/pending-stake/[address].tsx b/pages/api/pending-stake/[address].tsx index 28ee1080..e5da3bef 100644 --- a/pages/api/pending-stake/[address].tsx +++ b/pages/api/pending-stake/[address].tsx @@ -1,6 +1,7 @@ import { getCacheControlHeader, getCurrentRound } from "@lib/api"; import { bondingManager } from "@lib/api/abis/main/BondingManager"; import { getBondingManagerAddress } from "@lib/api/contracts"; +import { badRequest, internalError, methodNotAllowed } from "@lib/api/errors"; import { PendingFeesAndStake } from "@lib/api/types/get-pending-stake"; import { l2PublicClient } from "@lib/chains"; import { NextApiRequest, NextApiResponse } from "next"; @@ -27,7 +28,7 @@ const handler = async ( const currentRoundString = protocol?.currentRound?.id; if (!currentRoundString) { - return res.status(500).end("No current round found"); + throw new Error("No current round found"); } const currentRound = BigInt(currentRoundString); @@ -56,15 +57,13 @@ const handler = async ( return res.status(200).json(roundInfo); } else { - return res.status(500).end("Invalid ID"); + return badRequest(res, "Invalid address format"); } } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/pipelines/index.tsx b/pages/api/pipelines/index.tsx index 852ee6ac..8bc2abf8 100644 --- a/pages/api/pipelines/index.tsx +++ b/pages/api/pipelines/index.tsx @@ -1,4 +1,5 @@ import { getCacheControlHeader } from "@lib/api"; +import { internalError, methodNotAllowed } from "@lib/api/errors"; import { AvailablePipelines } from "@lib/api/types/get-available-pipelines"; import { fetchWithRetry } from "@lib/fetchWithRetry"; import { NextApiRequest, NextApiResponse } from "next"; @@ -26,11 +27,9 @@ const handler = async ( return res.status(200).json(availablePipelines); } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); - } catch (e) { - console.error(e); - return res.status(500).json(null); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); + } catch (err) { + return internalError(res, err); } }; diff --git a/pages/api/regions/index.ts b/pages/api/regions/index.ts index 1522a1d8..1d94fe4d 100644 --- a/pages/api/regions/index.ts +++ b/pages/api/regions/index.ts @@ -1,4 +1,5 @@ import { getCacheControlHeader } from "@lib/api"; +import { internalError, methodNotAllowed } from "@lib/api/errors"; import { Region, Regions } from "@lib/api/types/get-regions"; import { fetchWithRetry } from "@lib/fetchWithRetry"; import { NextApiRequest, NextApiResponse } from "next"; @@ -62,11 +63,9 @@ const handler = async ( return res.status(200).json(mergedRegions); } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/score/[address].tsx b/pages/api/score/[address].tsx index 30001a65..572e5144 100644 --- a/pages/api/score/[address].tsx +++ b/pages/api/score/[address].tsx @@ -1,4 +1,10 @@ import { getCacheControlHeader } from "@lib/api"; +import { + badRequest, + externalApiError, + internalError, + methodNotAllowed, +} from "@lib/api/errors"; import { PerformanceMetrics, RegionalValues, @@ -79,7 +85,7 @@ const handler = async ( topScoreResponse.status, errorText ); - return res.status(500).end("Failed to fetch top AI score"); + return externalApiError(res, "AI metrics server"); } if (!metricsResponse.ok) { @@ -89,7 +95,7 @@ const handler = async ( metricsResponse.status, errorText ); - return res.status(500).end("Failed to fetch metrics"); + return externalApiError(res, "metrics server"); } if (!priceResponse.ok) { @@ -99,7 +105,7 @@ const handler = async ( priceResponse.status, errorText ); - return res.status(500).end("Failed to fetch transcoders with price"); + return externalApiError(res, "pricing server"); } const topAIScore: ScoreResponse = await topScoreResponse.json(); @@ -163,15 +169,13 @@ const handler = async ( return res.status(200).json(combined); } else { - return res.status(500).end("Invalid ID"); + return badRequest(res, "Invalid address format"); } } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/score/index.tsx b/pages/api/score/index.tsx index 69a57898..d17ecca6 100644 --- a/pages/api/score/index.tsx +++ b/pages/api/score/index.tsx @@ -1,4 +1,5 @@ import { getCacheControlHeader } from "@lib/api"; +import { internalError, methodNotAllowed } from "@lib/api/errors"; import { AllPerformanceMetrics, RegionalValues, @@ -105,11 +106,9 @@ const handler = async ( return res.status(200).json(combined); } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/totalTokenSupply.tsx b/pages/api/totalTokenSupply.tsx index 23a19d04..6dfb143c 100644 --- a/pages/api/totalTokenSupply.tsx +++ b/pages/api/totalTokenSupply.tsx @@ -1,37 +1,56 @@ import { getCacheControlHeader } from "@lib/api"; +import { + externalApiError, + internalError, + methodNotAllowed, +} from "@lib/api/errors"; import { CHAIN_INFO, DEFAULT_CHAIN_ID } from "@lib/chains"; import { fetchWithRetry } from "@lib/fetchWithRetry"; import type { NextApiRequest, NextApiResponse } from "next"; const totalTokenSupply = async (_req: NextApiRequest, res: NextApiResponse) => { - const response = await fetchWithRetry( - CHAIN_INFO[DEFAULT_CHAIN_ID].subgraph, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: ` - query { - protocol(id: "0") { - totalSupply - } - } - `, - }), - }, - { - retryOnMethods: ["POST"], - } - ); + try { + const method = _req.method; + + if (method === "GET") { + const response = await fetchWithRetry( + CHAIN_INFO[DEFAULT_CHAIN_ID].subgraph, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: ` + query { + protocol(id: "0") { + totalSupply + } + } + `, + }), + }, + { + retryOnMethods: ["POST"], + } + ); + + if (!response.ok) { + return externalApiError(res, "subgraph"); + } - res.setHeader("Cache-Control", getCacheControlHeader("day")); + res.setHeader("Cache-Control", getCacheControlHeader("day")); + + const { + data: { protocol }, + } = await response.json(); + return res.status(200).json(Number(protocol.totalSupply)); + } - const { - data: { protocol }, - } = await response.json(); - res.json(Number(protocol.totalSupply)); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); + } catch (err) { + return internalError(res, err); + } }; export default totalTokenSupply; diff --git a/pages/api/treasury/proposal/[proposalId]/state.tsx b/pages/api/treasury/proposal/[proposalId]/state.tsx index ec326f2f..819e627f 100644 --- a/pages/api/treasury/proposal/[proposalId]/state.tsx +++ b/pages/api/treasury/proposal/[proposalId]/state.tsx @@ -5,6 +5,7 @@ import { getBondingVotesAddress, getLivepeerGovernorAddress, } from "@lib/api/contracts"; +import { badRequest, internalError, methodNotAllowed } from "@lib/api/errors"; import { ProposalState } from "@lib/api/types/get-treasury-proposal"; import { l2PublicClient } from "@lib/chains"; import { NextApiRequest, NextApiResponse } from "next"; @@ -27,14 +28,13 @@ const handler = async ( try { const { method } = req; if (method !== "GET") { - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } res.setHeader("Cache-Control", getCacheControlHeader("second")); const proposalId = req.query.proposalId?.toString(); if (!proposalId) { - throw new Error("Missing proposalId"); + return badRequest(res, "Missing proposalId"); } const livepeerGovernorAddress = await getLivepeerGovernorAddress(); @@ -116,7 +116,7 @@ const handler = async ( }); } catch (err) { console.error("state api error", err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/treasury/proposal/[proposalId]/votes/[address].tsx b/pages/api/treasury/proposal/[proposalId]/votes/[address].tsx index 64919c6f..dac26555 100644 --- a/pages/api/treasury/proposal/[proposalId]/votes/[address].tsx +++ b/pages/api/treasury/proposal/[proposalId]/votes/[address].tsx @@ -5,6 +5,7 @@ import { getBondingVotesAddress, getLivepeerGovernorAddress, } from "@lib/api/contracts"; +import { badRequest, internalError, methodNotAllowed } from "@lib/api/errors"; import { ProposalVotingPower } from "@lib/api/types/get-treasury-proposal"; import { l2PublicClient } from "@lib/chains"; import { NextApiRequest, NextApiResponse } from "next"; @@ -17,18 +18,17 @@ const handler = async ( try { const { method } = req; if (method !== "GET") { - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } res.setHeader("Cache-Control", getCacheControlHeader("second")); const proposalId = req.query.proposalId?.toString(); if (!proposalId) { - throw new Error("Missing proposalId"); + return badRequest(res, "Missing proposalId"); } const address = req.query.address?.toString(); if (!(!!address && isAddress(address))) { - throw new Error("Missing address"); + return badRequest(res, "Invalid address format"); } const livepeerGovernorAddress = await getLivepeerGovernorAddress(); @@ -88,8 +88,7 @@ const handler = async ( : await getVotes(delegateAddress), }); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/treasury/votes/[address]/index.tsx b/pages/api/treasury/votes/[address]/index.tsx index 420200bf..13857814 100644 --- a/pages/api/treasury/votes/[address]/index.tsx +++ b/pages/api/treasury/votes/[address]/index.tsx @@ -5,6 +5,7 @@ import { getBondingVotesAddress, getLivepeerGovernorAddress, } from "@lib/api/contracts"; +import { badRequest, internalError, methodNotAllowed } from "@lib/api/errors"; import { VotingPower } from "@lib/api/types/get-treasury-proposal"; import { l2PublicClient } from "@lib/chains"; import { NextApiRequest, NextApiResponse } from "next"; @@ -17,14 +18,13 @@ const handler = async ( try { const { method } = req; if (method !== "GET") { - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } res.setHeader("Cache-Control", getCacheControlHeader("second")); const address = req.query.address?.toString(); if (!(!!address && isAddress(address))) { - throw new Error("Missing address"); + return badRequest(res, "Invalid address format"); } const livepeerGovernorAddress = await getLivepeerGovernorAddress(); @@ -75,8 +75,7 @@ const handler = async ( : await getVotes(delegateAddress), }); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/treasury/votes/[address]/registered.tsx b/pages/api/treasury/votes/[address]/registered.tsx index b5915f80..bbe57c5c 100644 --- a/pages/api/treasury/votes/[address]/registered.tsx +++ b/pages/api/treasury/votes/[address]/registered.tsx @@ -5,6 +5,7 @@ import { getBondingManagerAddress, getBondingVotesAddress, } from "@lib/api/contracts"; +import { badRequest, internalError, methodNotAllowed } from "@lib/api/errors"; import { RegisteredToVote } from "@lib/api/types/get-treasury-proposal"; import { l2PublicClient } from "@lib/chains"; import { NextApiRequest, NextApiResponse } from "next"; @@ -17,13 +18,12 @@ const handler = async ( try { const { method } = req; if (method !== "GET") { - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } const address = req.query.address?.toString(); if (!(!!address && isAddress(address))) { - throw new Error("Missing address"); + return badRequest(res, "Invalid address format"); } const bondingManagerAddress = await getBondingManagerAddress(); @@ -85,8 +85,7 @@ const handler = async ( }, }); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/upload-ipfs.tsx b/pages/api/upload-ipfs.tsx index 91f1c7ad..56b25c15 100644 --- a/pages/api/upload-ipfs.tsx +++ b/pages/api/upload-ipfs.tsx @@ -1,3 +1,8 @@ +import { + externalApiError, + internalError, + methodNotAllowed, +} from "@lib/api/errors"; import { AddIpfs } from "@lib/api/types/add-ipfs"; import { NextApiRequest, NextApiResponse } from "next"; @@ -20,16 +25,19 @@ const handler = async ( body: JSON.stringify(req.body), } ); + + if (!fetchResult.ok) { + return externalApiError(res, "Pinata IPFS"); + } + const result = await fetchResult.json(); return res.status(200).json({ hash: result.IpfsHash }); } - res.setHeader("Allow", ["POST"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["POST"]); } catch (err) { - console.error(err); - return res.status(500).json(null); + return internalError(res, err); } }; diff --git a/pages/api/usage.tsx b/pages/api/usage.tsx index de42759f..34c5498d 100644 --- a/pages/api/usage.tsx +++ b/pages/api/usage.tsx @@ -1,4 +1,9 @@ import { getCacheControlHeader } from "@lib/api"; +import { + externalApiError, + internalError, + methodNotAllowed, +} from "@lib/api/errors"; import { DayData, HomeChartData, @@ -76,7 +81,7 @@ const chartDataHandler = async ( errorBody ); - return res.status(500).json(null); + return externalApiError(res, "livepeer.com usage API"); } const parsedDayData = await response @@ -85,7 +90,7 @@ const chartDataHandler = async ( if (!parsedDayData.success) { console.error(parsedDayData.error); - return res.status(500).json(null); + return internalError(res, new Error("Failed to parse usage data")); } const mergedDayData: DayData[] = [ @@ -106,141 +111,132 @@ const chartDataHandler = async ( .sort((a, b) => (a.dateS > b.dateS ? 1 : -1)) .filter((s) => s.activeTranscoderCount); - try { - let startIndexWeekly = -1; - let currentWeek = -1; - - const weeklyData: WeeklyData[] = []; - - for (const day of sortedDays) { - const week = dayjs.utc(dayjs.unix(day.dateS)).week(); - if (week !== currentWeek) { - currentWeek = week; - startIndexWeekly++; - - weeklyData.push({ - date: day.dateS, - weeklyVolumeUsd: 0, - weeklyVolumeEth: 0, - weeklyUsageMinutes: 0, - }); - } - - weeklyData[startIndexWeekly].weeklyVolumeUsd += day.volumeUsd; - weeklyData[startIndexWeekly].weeklyVolumeEth += day.volumeEth; - weeklyData[startIndexWeekly].weeklyUsageMinutes += - day.feeDerivedMinutes; - } + let startIndexWeekly = -1; + let currentWeek = -1; - // const currentWeekData = weeklyData[weeklyData.length - 1]; - const oneWeekBackData = weeklyData[weeklyData.length - 2]; - const twoWeekBackData = weeklyData[weeklyData.length - 3]; + const weeklyData: WeeklyData[] = []; - const currentDayData = sortedDays[sortedDays.length - 1]; - const oneDayBackData = sortedDays[sortedDays.length - 2]; - // const twoDayBackData = sortedDays[sortedDays.length - 3]; + for (const day of sortedDays) { + const week = dayjs.utc(dayjs.unix(day.dateS)).week(); + if (week !== currentWeek) { + currentWeek = week; + startIndexWeekly++; - const dailyUsageChange = getPercentChange( - currentDayData.feeDerivedMinutes, - oneDayBackData.feeDerivedMinutes - ); + weeklyData.push({ + date: day.dateS, + weeklyVolumeUsd: 0, + weeklyVolumeEth: 0, + weeklyUsageMinutes: 0, + }); + } - const dailyVolumeEthChange = getPercentChange( - currentDayData.volumeEth, - oneDayBackData.volumeEth - ); + weeklyData[startIndexWeekly].weeklyVolumeUsd += day.volumeUsd; + weeklyData[startIndexWeekly].weeklyVolumeEth += day.volumeEth; + weeklyData[startIndexWeekly].weeklyUsageMinutes += + day.feeDerivedMinutes; + } - const dailyVolumeUsdChange = getPercentChange( - currentDayData.volumeUsd, - oneDayBackData.volumeUsd - ); + // const currentWeekData = weeklyData[weeklyData.length - 1]; + const oneWeekBackData = weeklyData[weeklyData.length - 2]; + const twoWeekBackData = weeklyData[weeklyData.length - 3]; - const weeklyVolumeUsdChange = getPercentChange( - oneWeekBackData?.weeklyVolumeUsd ?? 0, - twoWeekBackData?.weeklyVolumeUsd ?? 0 - ); + const currentDayData = sortedDays[sortedDays.length - 1]; + const oneDayBackData = sortedDays[sortedDays.length - 2]; + // const twoDayBackData = sortedDays[sortedDays.length - 3]; - const weeklyVolumeEthChange = getPercentChange( - oneWeekBackData?.weeklyVolumeEth ?? 0, - twoWeekBackData?.weeklyVolumeEth ?? 0 - ); + const dailyUsageChange = getPercentChange( + currentDayData.feeDerivedMinutes, + oneDayBackData.feeDerivedMinutes + ); - const weeklyUsageChange = getPercentChange( - oneWeekBackData?.weeklyUsageMinutes ?? 0, - twoWeekBackData?.weeklyUsageMinutes ?? 0 - ); + const dailyVolumeEthChange = getPercentChange( + currentDayData.volumeEth, + oneDayBackData.volumeEth + ); - const participationRateChange = getPercentChange( - currentDayData?.participationRate, - oneDayBackData?.participationRate - ); - const inflationChange = getPercentChange( - currentDayData?.inflation, - oneDayBackData?.inflation - ); - const activeTranscoderCountChange = getPercentChange( - currentDayData?.activeTranscoderCount, - oneDayBackData?.activeTranscoderCount - ); - const delegatorsCountChange = getPercentChange( - currentDayData?.delegatorsCount, - oneDayBackData?.delegatorsCount - ); + const dailyVolumeUsdChange = getPercentChange( + currentDayData.volumeUsd, + oneDayBackData.volumeUsd + ); - const data: HomeChartData = { - dayData: sortedDays, - weeklyData: weeklyData, - oneDayVolumeUSD: currentDayData.volumeUsd, - oneDayVolumeETH: currentDayData.volumeEth, - oneWeekVolumeUSD: oneWeekBackData.weeklyVolumeUsd, - oneWeekVolumeETH: oneWeekBackData.weeklyVolumeEth, - // totalUsage: totalFeeDerivedMinutes + totalLivepeerComUsage, - oneDayUsage: currentDayData.feeDerivedMinutes ?? 0, - oneWeekUsage: oneWeekBackData.weeklyUsageMinutes ?? 0, - dailyUsageChange: dailyUsageChange ?? 0, - weeklyUsageChange: weeklyUsageChange ?? 0, - weeklyVolumeChangeUSD: weeklyVolumeUsdChange, - weeklyVolumeChangeETH: weeklyVolumeEthChange, - volumeChangeUSD: dailyVolumeUsdChange, - volumeChangeETH: dailyVolumeEthChange, - participationRateChange: participationRateChange, - inflationChange: inflationChange, - delegatorsCountChange: delegatorsCountChange, - activeTranscoderCountChange: activeTranscoderCountChange, - - participationRate: - sortedDays[sortedDays.length - 1].participationRate, - inflation: sortedDays[sortedDays.length - 1].inflation, - activeTranscoderCount: - sortedDays[sortedDays.length - 1].activeTranscoderCount, - delegatorsCount: sortedDays[sortedDays.length - 1].delegatorsCount, - }; - - if ( - Number( - data?.dayData?.[(data?.dayData?.length ?? 1) - 1] - ?.activeTranscoderCount - ) <= 0 - ) { - data.dayData = data.dayData.slice(0, -1); - } + const weeklyVolumeUsdChange = getPercentChange( + oneWeekBackData?.weeklyVolumeUsd ?? 0, + twoWeekBackData?.weeklyVolumeUsd ?? 0 + ); - res.setHeader("Cache-Control", getCacheControlHeader("day")); + const weeklyVolumeEthChange = getPercentChange( + oneWeekBackData?.weeklyVolumeEth ?? 0, + twoWeekBackData?.weeklyVolumeEth ?? 0 + ); + + const weeklyUsageChange = getPercentChange( + oneWeekBackData?.weeklyUsageMinutes ?? 0, + twoWeekBackData?.weeklyUsageMinutes ?? 0 + ); - return res.status(200).json(data); - } catch (e) { - console.error(e); - return res.status(500).json(null); + const participationRateChange = getPercentChange( + currentDayData?.participationRate, + oneDayBackData?.participationRate + ); + const inflationChange = getPercentChange( + currentDayData?.inflation, + oneDayBackData?.inflation + ); + const activeTranscoderCountChange = getPercentChange( + currentDayData?.activeTranscoderCount, + oneDayBackData?.activeTranscoderCount + ); + const delegatorsCountChange = getPercentChange( + currentDayData?.delegatorsCount, + oneDayBackData?.delegatorsCount + ); + + const data: HomeChartData = { + dayData: sortedDays, + weeklyData: weeklyData, + oneDayVolumeUSD: currentDayData.volumeUsd, + oneDayVolumeETH: currentDayData.volumeEth, + oneWeekVolumeUSD: oneWeekBackData.weeklyVolumeUsd, + oneWeekVolumeETH: oneWeekBackData.weeklyVolumeEth, + // totalUsage: totalFeeDerivedMinutes + totalLivepeerComUsage, + oneDayUsage: currentDayData.feeDerivedMinutes ?? 0, + oneWeekUsage: oneWeekBackData.weeklyUsageMinutes ?? 0, + dailyUsageChange: dailyUsageChange ?? 0, + weeklyUsageChange: weeklyUsageChange ?? 0, + weeklyVolumeChangeUSD: weeklyVolumeUsdChange, + weeklyVolumeChangeETH: weeklyVolumeEthChange, + volumeChangeUSD: dailyVolumeUsdChange, + volumeChangeETH: dailyVolumeEthChange, + participationRateChange: participationRateChange, + inflationChange: inflationChange, + delegatorsCountChange: delegatorsCountChange, + activeTranscoderCountChange: activeTranscoderCountChange, + + participationRate: sortedDays[sortedDays.length - 1].participationRate, + inflation: sortedDays[sortedDays.length - 1].inflation, + activeTranscoderCount: + sortedDays[sortedDays.length - 1].activeTranscoderCount, + delegatorsCount: sortedDays[sortedDays.length - 1].delegatorsCount, + }; + + if ( + Number( + data?.dayData?.[(data?.dayData?.length ?? 1) - 1] + ?.activeTranscoderCount + ) <= 0 + ) { + data.dayData = data.dayData.slice(0, -1); } + + res.setHeader("Cache-Control", getCacheControlHeader("day")); + + return res.status(200).json(data); } - res.setHeader("Allow", ["GET"]); - return res.status(405).end(`Method ${method} Not Allowed`); + return methodNotAllowed(res, method ?? "unknown", ["GET"]); } catch (err) { - console.error(err); + return internalError(res, err); } - - return res.status(500).json(null); }; export default chartDataHandler; diff --git a/utils/ipfs.ts b/utils/ipfs.ts index b321ee02..4cd9097d 100644 --- a/utils/ipfs.ts +++ b/utils/ipfs.ts @@ -8,6 +8,12 @@ export const addIpfs = async (content: object): Promise => { }, body: JSON.stringify(content), }); + + if (!fetchResult.ok) { + const error = await fetchResult.json().catch(() => null); + throw new Error(error?.error || "Failed to upload to IPFS"); + } + const result = (await fetchResult.json()) as AddIpfs; return result.hash; };