Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions pages/api/cow-order.ts
Original file line number Diff line number Diff line change
@@ -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) });
}
}
49 changes: 49 additions & 0 deletions pages/api/cow-orders.ts
Original file line number Diff line number Diff line change
@@ -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) });
}
}
15 changes: 8 additions & 7 deletions src/hooks/useTransactionHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ 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';
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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -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;
Expand Down
67 changes: 67 additions & 0 deletions src/utils/cowOrderbook.ts
Original file line number Diff line number Diff line change
@@ -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<number, string> = {
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 <T>(path: string, params: Record<string, string>) => {
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<T>;
};

export const fetchCowOrders = ({
chainId,
owner,
limit,
offset,
}: {
chainId: number;
owner: string;
limit: number;
offset: number;
}) =>
fetchCowOrderbookProxy<CowOrderbookOrder[]>('/api/cow-orders/', {
chainId: String(chainId),
owner,
limit: String(limit),
offset: String(offset),
});

export const fetchCowOrder = ({ chainId, orderUid }: { chainId: number; orderUid: string }) =>
fetchCowOrderbookProxy<CowOrderbookOrder>('/api/cow-order/', {
chainId: String(chainId),
orderUid,
});