From a8c677deb86aab26d7f195860a39a4eb6956838c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Wed, 17 Jun 2026 00:11:35 +0200 Subject: [PATCH] fix: proxy CoW history requests --- pages/api/cow-order.ts | 38 ++++++++++++++++ pages/api/cow-orders.ts | 49 +++++++++++++++++++++ src/hooks/useTransactionHistory.tsx | 15 ++++--- src/utils/cowOrderbook.ts | 67 +++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 pages/api/cow-order.ts create mode 100644 pages/api/cow-orders.ts create mode 100644 src/utils/cowOrderbook.ts diff --git a/pages/api/cow-order.ts b/pages/api/cow-order.ts new file mode 100644 index 0000000000..947f022b87 --- /dev/null +++ b/pages/api/cow-order.ts @@ -0,0 +1,38 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getCowOrderbookChainSlug } from 'src/utils/cowOrderbook'; + +const COW_API_BASE_URL = 'https://api.cow.fi'; +const ORDER_UID_REGEX = /^0x[a-fA-F0-9]{112,}$/; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const chainId = Number(req.query.chainId); + const orderUid = Array.isArray(req.query.orderUid) ? req.query.orderUid[0] : req.query.orderUid; + const chainSlug = getCowOrderbookChainSlug(chainId); + + if (!chainSlug) { + return res.status(400).json({ error: 'Unsupported CoW orderbook chain' }); + } + + if (!orderUid || !ORDER_UID_REGEX.test(orderUid)) { + return res.status(400).json({ error: 'Invalid order UID' }); + } + + try { + const response = await fetch(`${COW_API_BASE_URL}/${chainSlug}/api/v1/orders/${orderUid}`, { + headers: { Accept: 'application/json' }, + }); + const body = await response.text(); + + res.setHeader('Cache-Control', 'no-store'); + res.setHeader('Content-Type', response.headers.get('content-type') ?? 'application/json'); + + return res.status(response.status).send(body); + } catch (error) { + console.error('CoW order proxy error:', error); + return res.status(500).json({ error: 'Internal server error', details: String(error) }); + } +} diff --git a/pages/api/cow-orders.ts b/pages/api/cow-orders.ts new file mode 100644 index 0000000000..fc50fafe4e --- /dev/null +++ b/pages/api/cow-orders.ts @@ -0,0 +1,49 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getCowOrderbookChainSlug } from 'src/utils/cowOrderbook'; + +const COW_API_BASE_URL = 'https://api.cow.fi'; +const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/; + +const parsePositiveInteger = (value: string | string[] | undefined, fallback: number) => { + const parsed = Number(Array.isArray(value) ? value[0] : value); + return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const chainId = Number(req.query.chainId); + const owner = Array.isArray(req.query.owner) ? req.query.owner[0] : req.query.owner; + const limit = Math.min(parsePositiveInteger(req.query.limit, 50), 100); + const offset = parsePositiveInteger(req.query.offset, 0); + const chainSlug = getCowOrderbookChainSlug(chainId); + + if (!chainSlug) { + return res.status(400).json({ error: 'Unsupported CoW orderbook chain' }); + } + + if (!owner || !ADDRESS_REGEX.test(owner)) { + return res.status(400).json({ error: 'Invalid owner address' }); + } + + try { + const url = new URL(`${COW_API_BASE_URL}/${chainSlug}/api/v1/account/${owner}/orders`); + url.searchParams.set('limit', String(limit)); + url.searchParams.set('offset', String(offset)); + + const response = await fetch(url, { + headers: { Accept: 'application/json' }, + }); + const body = await response.text(); + + res.setHeader('Cache-Control', 'no-store'); + res.setHeader('Content-Type', response.headers.get('content-type') ?? 'application/json'); + + return res.status(response.status).send(body); + } catch (error) { + console.error('CoW orders proxy error:', error); + return res.status(500).json({ error: 'Internal server error', details: String(error) }); + } +} diff --git a/src/hooks/useTransactionHistory.tsx b/src/hooks/useTransactionHistory.tsx index 13bac5a23a..6a10fcc3c6 100644 --- a/src/hooks/useTransactionHistory.tsx +++ b/src/hooks/useTransactionHistory.tsx @@ -6,7 +6,6 @@ import { useUserTransactionHistory, } from '@aave/react'; import { Cursor } from '@aave/types'; -import { OrderBookApi } from '@cowprotocol/cow-sdk'; import { useInfiniteQuery } from '@tanstack/react-query'; import { useEffect, useMemo, useRef, useState } from 'react'; import { isChainIdSupportedByCoWProtocol } from 'src/components/transactions/Swap/constants/cow.constants'; @@ -14,7 +13,6 @@ import { APP_CODE_PER_SWAP_TYPE, APP_CODE_VALUES, } from 'src/components/transactions/Swap/constants/shared.constants'; -import { COW_ENV } from 'src/components/transactions/Swap/helpers/cow'; import { SwapType } from 'src/components/transactions/Swap/types'; import { getTransactionAction, getTransactionId } from 'src/modules/history/helpers'; import { @@ -35,6 +33,7 @@ import { ERC20Service } from 'src/services/Erc20Service'; import { useRootStore } from 'src/store/root'; import { queryKeysFactory } from 'src/ui-config/queries'; import { TOKEN_LIST } from 'src/ui-config/TokenList'; +import { fetchCowOrder, fetchCowOrders } from 'src/utils/cowOrderbook'; import { getProvider } from 'src/utils/marketsAndNetworksConfig'; import { CowAdapterEntry, @@ -248,12 +247,15 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool const chainId = currentMarketData.chainId; let apiTxns: TransactionHistoryItemUnion[] = []; - if (isChainIdSupportedByCoWProtocol(chainId)) { - const orderBookApi = new OrderBookApi({ chainId: chainId, env: COW_ENV }); - const orders = await orderBookApi.getOrders({ + if (account && isChainIdSupportedByCoWProtocol(chainId)) { + const orders = await fetchCowOrders({ + chainId, owner: account, limit: first, offset: skip, + }).catch((error) => { + console.error('Error fetching CoW orders history', error); + return []; }); const filteredCowAaveOrders = ( @@ -423,8 +425,7 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool let status = e.status; try { if (isChainIdSupportedByCoWProtocol(chainId)) { - const orderBookApi = new OrderBookApi({ chainId, env: COW_ENV }); - const order = await orderBookApi.getOrder(e.orderId, { chainId }); + const order = await fetchCowOrder({ chainId, orderUid: e.orderId }); // Prefer executed amounts if non-zero if (order?.executedSellAmount && order.executedSellAmount !== '0') { srcAmount = order.executedSellAmount; diff --git a/src/utils/cowOrderbook.ts b/src/utils/cowOrderbook.ts new file mode 100644 index 0000000000..73abfbb1e2 --- /dev/null +++ b/src/utils/cowOrderbook.ts @@ -0,0 +1,67 @@ +import type { OrderStatus } from '@cowprotocol/cow-sdk'; + +export type CowOrderbookOrder = { + uid: string; + creationDate: string; + sellToken: string; + buyToken: string; + sellAmount: string; + buyAmount: string; + executedSellAmount?: string; + executedBuyAmount?: string; + fullAppData?: string; + status: OrderStatus; +}; + +const COW_API_CHAIN_SLUGS: Record = { + 1: 'mainnet', + 100: 'xdai', + 42161: 'arbitrum_one', + 8453: 'base', + 11155111: 'sepolia', + 43114: 'avalanche', + 137: 'polygon', + 56: 'bnb', + 59144: 'linea', + 9745: 'plasma', + 57073: 'ink', +}; + +export const getCowOrderbookChainSlug = (chainId: number) => { + return COW_API_CHAIN_SLUGS[chainId]; +}; + +const fetchCowOrderbookProxy = async (path: string, params: Record) => { + const urlParams = new URLSearchParams(params); + const response = await fetch(`${path}?${urlParams.toString()}`); + + if (!response.ok) { + throw new Error(`CoW orderbook proxy request failed: ${response.status}`); + } + + return response.json() as Promise; +}; + +export const fetchCowOrders = ({ + chainId, + owner, + limit, + offset, +}: { + chainId: number; + owner: string; + limit: number; + offset: number; +}) => + fetchCowOrderbookProxy('/api/cow-orders/', { + chainId: String(chainId), + owner, + limit: String(limit), + offset: String(offset), + }); + +export const fetchCowOrder = ({ chainId, orderUid }: { chainId: number; orderUid: string }) => + fetchCowOrderbookProxy('/api/cow-order/', { + chainId: String(chainId), + orderUid, + });